Introduction
Although the product offers out-of-the-box scanning capabilities, additional development is required to deploy them as an independent feature in a production environment. In the following section, I will demonstrate how I utilized the existing functionalities to rapidly develop a working scanning solution, along with a summary of the challenges faced during development.
Requirements
Shop floor operators currently need to navigate the full MES interface to perform the most frequent and time-critical operations at a Resource: Check-In, Check-Out, Dispatch, Track-In, and Track-Out, etc. On the shop floor, operators work with handheld Zebra devices that have a built-in scanner but a small screen, making the standard MES interface impractical to use efficiently. A lightweight, scan-driven Operator Cockpit tailored for the Zebra device enables operators to perform these core operations quickly, accurately, and without relying on a desktop terminal, reducing wait times, minimizing navigation steps, and keeping operators focused on the physical work at hand.
The main requirement points are as follows:
-
Scan to retrieve Resource or Material:
- Resource Scan: Scanning a Resource must query and display all associated Materials. By default, only Materials in
Queuedstatus are shown. Other statuses can be queried via filters, and the status filter dropdown should only display the applicable/specific status values. - Material Scan: If a Material is scanned, it should be selectable and deselectable.
- Resource Scan: Scanning a Resource must query and display all associated Materials. By default, only Materials in
-
Barcode Input: Support for manual barcode entry.
Image: Mobile Cockpit Page
1. How to ensure a Resource is scanned first?
Question:
Since a scan could be either a Resource or a Material, but scanning a Resource first is a prerequisite for scanning a Material to be meaningful, how can we ensure that the first scan is always a Resource?
Solution:
This feature implements the scanning logic via DEE. Upon the first scan, the result is forwarded to DEE, which evaluates the input through the following code to determine if it is a Resource, a Material, or an error.
/// <summary>
/// Gets the Resource or Material by name or barcode.
/// </summary>
/// <param name="input">The input.</param>
/// <returns>Resource or Material.</returns>
public (IResource, IMaterial) GetResourceOrMaterialByBarcode(string barcode)
{
long? materialId = null;
long? resourceId = null;
IMaterial material = _entityFactory.Create<IMaterial>();
IResource resource = _entityFactory.Create<IResource>();
#region Resource query
if (resource.ObjectExists(barcode))
{
resource.Load(barcode);
}
else if (material.ObjectExists(barcode))
{
material.Load(barcode);
if (material.UniversalState != UniversalState.Terminated && !material.IsTemplate)
{
material.LoadReferences([Cmf.Navigo.Common.Constants.Product, Cmf.Navigo.Common.Constants.Step]);
}
}
else
{
IEntityType materialEntityType = _entityFactory.Create<IEntityType>();
materialEntityType.Load(Cmf.Navigo.Common.Constants.Material);
IEntityType resourceEntityType = _entityFactory.Create<IEntityType>();
resourceEntityType.Load(Cmf.Navigo.Common.Constants.Resource);
bool isToSearchForBarcode = materialEntityType.AllowBarcodeIds || resourceEntityType.AllowBarcodeIds;
if (isToSearchForBarcode)
{
IGenericTable entityTagGenericTable = _entityFactory.Create<IGenericTable>();
entityTagGenericTable.Load("EntityTag");
IFilterCollection entityTagFilters = new FilterCollection
{
new Filter
{
Name = "Scope",
Operator = FieldOperator.IsEqualTo,
Value = (int) EntityTagScope.BarcodeId,
LogicalOperator = LogicalOperator.AND
},
new Filter
{
Name = "Tag",
Operator = FieldOperator.IsEqualTo,
Value = barcode,
},
};
entityTagGenericTable.LoadData(entityTagFilters);
if (entityTagGenericTable.HasData)
{
DataSet entityTagDataSet = NgpDataSet.ToDataSet(entityTagGenericTable.Data);
DataRow row = entityTagDataSet.Tables[0].Rows[0];
string entityType = row.Field<string>("EntityType");
if (entityType == Cmf.Navigo.Common.Constants.Material)
{
materialId = row.Field<long>("EntityId");
}
else if (entityType == Cmf.Navigo.Common.Constants.Resource)
{
resourceId = row.Field<long>("EntityId");
}
if (materialId.HasValue && material.ObjectExists(materialId.Value))
{
material.Load(materialId.Value);
if (material.UniversalState != UniversalState.Terminated && !material.IsTemplate)
{
material.LoadReferences([Cmf.Navigo.Common.Constants.Product, Cmf.Navigo.Common.Constants.Step]);
}
}
else if (resourceId.HasValue && resource.ObjectExists(resourceId.Value))
{
resource.Load(resourceId.Value);
}
}
}
}
#endregion Barcode query
return (resource, material);
}
The page also passes the Resource parameter to the DEE. During the initial scan, this parameter is null. If the scanned information is not a Resource, an error will be returned. If a valid Resource is scanned, it will be returned and stored. For all subsequent scans, the Resource parameter passed to the DEE will be populated.
NOTE
When passing the Resource parameter to DEE, you must pass theResource Nameinstead of the Resource entity object. Passing the object is prohibited because DEE reloads and returns a new Resource instance on every call. Even if it represents the same Resource, the object reference changes, which triggers continuous calls to DEE. Therefore, pass theResource Nameinstead. Since it is a static string that remains unchanged as long as the Resource is not switched, it will not trigger redundant DEE calls.
2. How can a Material be selected or deselected via scanning?
This feature uses a Grid component to display data. The Grid component has a Selected input parameter and a Changed Selection output parameter, both of which are array types. The Selected input parameter is used to set the selected or unselected state of the data in the Grid, while the Changed Selection output parameter retrieves the currently selected data.
We need to pass both the Changed Selection output parameter and the scanned Material to DEE simultaneously. Within DEE, the Changed Selection array is evaluated: if the scanned Material is present in the array, it will be removed; if it is not present, it will be added. Finally, the updated Changed Selection array is returned from DEE to the page and reassigned to the Selected input parameter. This mechanism enables the select and deselect functionality via scanning.
NOTE
When passing theChanged Selectionoutput parameter to DEE, we need to convert all Materials in the array into a single string. This can be achieved by concatenating the Material Names with a semicolon(‘;’) as the delimiter. This prevents DEE from being called in a continuous loop. If theChanged Selectionarray object were passed directly, any changes to theSelectedinput parameter would trigger a change in theChanged Selectionoutput object, resulting in an infinite loop of DEE calls.
Solution:
A dedicated Converter component was created to concatenate the Material array into a single string using semicolons(‘;’) as delimiters. Upon receiving this parameter, DEE splits the string back by semicolons to determine whether the scanned Material is present in the array.
@Converter({
name: $localize`:@@cmf-meddev-core/handle-materials-to-name-string#NAME:Handle Materials To Name String Converter`,
input: [
<ComplexType>{
type: null,
CollectionType: CollectionType.Array,
},
],
output: SimpleType.String,
})
@Pipe({
standalone: true,
name: 'handleMaterialsToNameString',
})
export class HandleMaterialsToNameStringConverter implements PipeTransform {
transform(value: any[], ...args: any[]): any {
let result = '';
if (value && value.length > 0) {
value.forEach((n) => {
if (n instanceof Cmf.Navigo.BusinessObjects.Material) {
result += n.Name + ';';
}
});
}
return result;
}
}
Image: Material Name Converter
Trivial Issue:
In MES system v11.3.3, when operating in Mobile mode, scanning to select an item does not actually select the corresponding data in the interface. The result is shown in the figure below:
Image: Automatic select Issue
After communicating with the Product team, this has been confirmed as a product bug. However, the issus can be resolved using the following workaround: The results to be displayed in the Grid must be passed to DEE simultaneously. After logical processing, the Selected parameter is output. It is crucial to ensure that the Selected parameter shares the exact same reference as the data source displayed in the Grid. In other words, the Selected parameter must not be passed out after Grid’s data source has been loaded. This approach will resolve the current auto-selection issue.
Image: Selected Materials
3. How can add a insert barcode button?
User also require the option to enter information manually instead of scanning.
The Barcode Identification page under the Manufacturing menu has already implemented this feature.
Image: Barcode Identification
Since this page also built using UIPage, we can use it as a direct reference. This feature utilizes the native system button Manufacturing.BarcodeIdentification.ManualInsert to open a dialog box for barcode entry.
Image: Insert Barcode Button
It then uses the Barcode Actions widget to capture the input. The Barcode Actions widget provides a simple way to scan barcode(line or QR codes) that match specific objects within the system. Since we are only targeting Resource and Material, we just need to provide these two options in the Entities tab.
Image: Barcode Actions
Question:
- Since the
Barcode Actionswidget does not need to be displayed on the page and is only used to capture input, we need to hide it from the UI. - The current feature uses the system
Barcodetask to retrieve scanned content as a string, which is then passed to DEE. However, theBarcode Actionswidget outputs an entity. We need to convert this output into a string to maintain a consistent data format before passing it to DEE in the same manner.
Solution:
-
To address the first issue, we can use the following Less code in the file
/Cmf.MedDev.HTML/src/styles.lessto hide theBarcode Actionscomponent.cmf-core-dashboards-page[data-name="Mobile Cockpit"] div[data-content-path="cmf-core-dashboards/BarCodeIdentification"] { display:none; } -
For the second issue, we need to use a Converter
Get Entity Nameto retrieve the Name of the entity outputted by theBarcode Actionswidget.
Image: Get Entity Name Converter
Others
Since the title and subtitle of the UIPage cannot be displayed in Mobile mode, we need to develop a new widget to display this information. The front-end code is as follows:
Image: Entity Additional Info in ResourceView
HTML:
<div class="base-page-header">
<div class="title-bar base-page-title">
<cmf-core-controls-page-title>
<div class="page-icon icon-mes-et-sm-resource"></div>
<div class="page-label">{{ title }}</div>
<div class="page-sublabel">{{ subTitle }}</div>
<div class="page-title-additional-info" [innerHTML]="safeHtml"></div>
</cmf-core-controls-page-title>
</div>
</div>
TypeScript:
...
@Widget({
name: $localize`:@@cmf-meddev-core/entity-additional-info-widget#NAME:Entity Additional Info Widget`,
iconClass: 'icon-core-st-lg-generic',
settingsComponent: {
component: EntityAdditionalInfoWidgetSettingsComponent,
},
inputs: {
title: {
type: WidgetValueType.String,
},
subTitle: {
type: WidgetValueType.String,
},
additionalInfo: {
type: WidgetValueType.String,
},
},
})
@Component({
standalone: true,
selector: 'cmf-meddev-core-entity-additional-info-widget',
imports: [CommonModule],
templateUrl: './entity-additional-info-widget.component.html',
styleUrl: './entity-additional-info-widget.component.less',
inputs: ['title', 'subTitle', 'additionalInfo'],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class EntityAdditionalInfoWidget
extends WidgetGeneric
implements WidgetRepresentation
{
/**
* Defines how the widget will be represented
*/
public uiWidgetViewMode: WidgetViewMode = WidgetViewMode.Normal;
@Input() public title: string = '';
@Input() public subTitle: string = '';
@Input() public additionalInfo: string = '';
/**
* Constructor
*/
constructor(
viewContainerRef: ViewContainerRef,
elementRef: ElementRef,
feedback: FeedbackService,
private sanitizer: DomSanitizer,
) {
super(viewContainerRef, elementRef, feedback);
}
get safeHtml(): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(this.additionalInfo);
}
}
Less:
cmf-core-controls-base-page .base-page-header {
background-color: #fafafa;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
Image: Entity Additional Info Widget
It looks like this:
Image: Entity Additional Info Widget









