@adcops/autocore-react 3.1.1 → 3.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +7 -0
- package/dist/components/FileSelect.d.ts.map +1 -1
- package/dist/components/FileSelect.js +1 -1
- package/dist/core/AutoCoreTagContext.d.ts +19 -0
- package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
- package/dist/core/AutoCoreTagContext.js +1 -1
- package/dist/themes/adc-dark/blue/theme.css +305 -0
- package/dist/themes/adc-dark/blue/theme.css.map +1 -1
- package/package.json +3 -3
- package/src/components/FileSelect.tsx +31 -11
- package/src/core/AutoCoreTagContext.tsx +483 -44
- package/src/themes/adc-dark/_extensions.scss +327 -0
- package/docs/.nojekyll +0 -1
- package/docs/assets/hierarchy.js +0 -1
- package/docs/assets/highlight.css +0 -134
- package/docs/assets/icons.js +0 -18
- package/docs/assets/icons.svg +0 -1
- package/docs/assets/main.js +0 -60
- package/docs/assets/navigation.js +0 -1
- package/docs/assets/search.js +0 -1
- package/docs/assets/style.css +0 -1633
- package/docs/classes/components_CodeEditor.CodeEditor.html +0 -135
- package/docs/classes/components_Indicator.Indicator.html +0 -122
- package/docs/classes/components_IndicatorRect.IndicatorRect.html +0 -121
- package/docs/classes/components_JogPanel.JogPanel.html +0 -136
- package/docs/classes/components_Lamp.Lamp.html +0 -122
- package/docs/classes/components_OskDialog.OskDialog.html +0 -125
- package/docs/classes/components_TextInput.TextInput.html +0 -125
- package/docs/classes/components_ValueDisplay.ValueDisplay.html +0 -148
- package/docs/classes/components_ValueIndicator.ValueIndicator.html +0 -126
- package/docs/classes/core_ValueSimulator.ValueSimulator.html +0 -51
- package/docs/classes/hub_HubBase.HubBase.html +0 -106
- package/docs/classes/hub_HubSimulate.HubSimulate.html +0 -75
- package/docs/classes/hub_HubTauri.HubTauri.html +0 -93
- package/docs/classes/hub_HubWebSocket.HubWebSocket.html +0 -112
- package/docs/documents/core_AutoCoreTagContext.AutoCoreTagContext.html +0 -148
- package/docs/enums/components_JogPanel.JogDistanceAction.html +0 -5
- package/docs/enums/components_JogPanel.JogPanelAction.html +0 -18
- package/docs/enums/components_JogPanel.JogSpeedAction.html +0 -5
- package/docs/enums/core_ActionMode.ActionMode.html +0 -6
- package/docs/enums/core_IndicatorColor.IndicatorColor.html +0 -23
- package/docs/functions/assets.BlocklyLogo.html +0 -1
- package/docs/functions/assets.Distance.html +0 -1
- package/docs/functions/assets.JogLong.html +0 -1
- package/docs/functions/assets.JogMedium.html +0 -1
- package/docs/functions/assets.JogShort.html +0 -1
- package/docs/functions/assets.PythonLogo.html +0 -1
- package/docs/functions/assets.Rotation3D.html +0 -1
- package/docs/functions/assets.RotationCcw.html +0 -1
- package/docs/functions/assets.RotationCcwA.html +0 -1
- package/docs/functions/assets.RotationCcwB.html +0 -1
- package/docs/functions/assets.RotationCcwC.html +0 -1
- package/docs/functions/assets.RotationCw.html +0 -1
- package/docs/functions/assets.RotationCwA.html +0 -1
- package/docs/functions/assets.RotationCwB.html +0 -1
- package/docs/functions/assets.RotationCwC.html +0 -1
- package/docs/functions/assets.Run.html +0 -1
- package/docs/functions/assets.Speed.html +0 -1
- package/docs/functions/assets.SpeedFast.html +0 -1
- package/docs/functions/assets.SpeedMedium.html +0 -1
- package/docs/functions/assets.SpeedNone.html +0 -1
- package/docs/functions/assets.SpeedSlow.html +0 -1
- package/docs/functions/assets.Walk.html +0 -1
- package/docs/functions/components_BlocklyEditor.createCustomToolbox.html +0 -6
- package/docs/functions/core_UniqueId.UniqueId.html +0 -9
- package/docs/functions/core_hoc.hocAddSubscription.html +0 -6
- package/docs/functions/hooks_adsHooks.useAdsRegisterSymbols.html +0 -16
- package/docs/functions/hooks_adsHooks.useAdsTapValue.html +0 -8
- package/docs/functions/hooks_adsHooks.useAdsWriteScaledValue.html +0 -18
- package/docs/functions/hooks_adsHooks.useAdsWriteValue.html +0 -9
- package/docs/functions/hooks_commandHooks.useRegisterSymbols.html +0 -16
- package/docs/functions/hooks_commandHooks.useTapValue.html +0 -10
- package/docs/functions/hooks_commandHooks.useWriteScaledValue.html +0 -18
- package/docs/functions/hooks_commandHooks.useWriteValue.html +0 -11
- package/docs/functions/hooks_useAutoCoreTag.ts.makeAutoCoreTagHooks.html +0 -12
- package/docs/functions/hooks_useScaledValue.useScaledValue.html +0 -18
- package/docs/functions/hub.createHub.html +0 -3
- package/docs/hierarchy.html +0 -1
- package/docs/index.html +0 -148
- package/docs/interfaces/components_IndicatorButton.IndicatorButtonProps.html +0 -654
- package/docs/interfaces/components_IndicatorRect.IndicatorRectProps.html +0 -37
- package/docs/interfaces/components_JogPanel.JogPanelButtonDefinition.html +0 -5
- package/docs/interfaces/components_ToggleGroup.ToggleGroupProps.html +0 -644
- package/docs/interfaces/core_AutoCoreTagTypes.BaseContextValue.html +0 -12
- package/docs/interfaces/core_AutoCoreTagTypes.ScaleConfig.html +0 -13
- package/docs/interfaces/core_EventEmitterContext.Action.html +0 -8
- package/docs/interfaces/core_EventEmitterContext.EventEmitterContextType.html +0 -33
- package/docs/interfaces/core_EventEmitterContext.State.html +0 -8
- package/docs/interfaces/core_EventEmitterContext.Subscription.html +0 -6
- package/docs/interfaces/core_IndicatorButtonState.IndicatorButtonState.html +0 -10
- package/docs/interfaces/core_PositionContext.IPositionContext.html +0 -17
- package/docs/interfaces/hub_CommandMessage.CommandMessage.html +0 -6
- package/docs/interfaces/hub_CommandMessage.CommandMessageResult.html +0 -4
- package/docs/modules/assets.html +0 -1
- package/docs/modules/assets_BlocklyLogo.html +0 -1
- package/docs/modules/assets_Distance.html +0 -1
- package/docs/modules/assets_JogLong.html +0 -1
- package/docs/modules/assets_JogMedium.html +0 -1
- package/docs/modules/assets_JogShort.html +0 -1
- package/docs/modules/assets_PythonLogo.html +0 -1
- package/docs/modules/assets_Rotation3D.html +0 -1
- package/docs/modules/assets_RotationCcw.html +0 -1
- package/docs/modules/assets_RotationCcwA.html +0 -1
- package/docs/modules/assets_RotationCcwB.html +0 -1
- package/docs/modules/assets_RotationCcwC.html +0 -1
- package/docs/modules/assets_RotationCw.html +0 -1
- package/docs/modules/assets_RotationCwA.html +0 -1
- package/docs/modules/assets_RotationCwB.html +0 -1
- package/docs/modules/assets_RotationCwC.html +0 -1
- package/docs/modules/assets_Run.html +0 -1
- package/docs/modules/assets_Speed.html +0 -1
- package/docs/modules/assets_SpeedFast.html +0 -1
- package/docs/modules/assets_SpeedMedium.html +0 -1
- package/docs/modules/assets_SpeedNone.html +0 -1
- package/docs/modules/assets_SpeedSlow.html +0 -1
- package/docs/modules/assets_Walk.html +0 -1
- package/docs/modules/components_AutoCoreDevPanel.html +0 -20
- package/docs/modules/components_BlocklyEditor.html +0 -1
- package/docs/modules/components_CodeEditor.html +0 -1
- package/docs/modules/components_FileList.html +0 -1
- package/docs/modules/components_FileSelect.html +0 -1
- package/docs/modules/components_FitText.html +0 -1
- package/docs/modules/components_Indicator.html +0 -1
- package/docs/modules/components_IndicatorButton.html +0 -1
- package/docs/modules/components_IndicatorRect.html +0 -1
- package/docs/modules/components_JogPanel.html +0 -1
- package/docs/modules/components_Lamp.html +0 -1
- package/docs/modules/components_Osk.html +0 -1
- package/docs/modules/components_OskDialog.html +0 -1
- package/docs/modules/components_ProgressBarWithValue.html +0 -1
- package/docs/modules/components_TextInput.html +0 -1
- package/docs/modules/components_ToggleGroup.html +0 -1
- package/docs/modules/components_ValueDisplay.html +0 -1
- package/docs/modules/components_ValueIndicator.html +0 -1
- package/docs/modules/components_ValueInput.html +0 -1
- package/docs/modules/core_ActionMode.html +0 -1
- package/docs/modules/core_AutoCoreTagContext.html +0 -11
- package/docs/modules/core_AutoCoreTagTypes.html +0 -1
- package/docs/modules/core_EventEmitterContext.html +0 -53
- package/docs/modules/core_IndicatorButtonState.html +0 -1
- package/docs/modules/core_IndicatorColor.html +0 -1
- package/docs/modules/core_MaskPatterns.html +0 -1
- package/docs/modules/core_NumerableTypes.html +0 -1
- package/docs/modules/core_PositionContext.html +0 -1
- package/docs/modules/core_UniqueId.html +0 -1
- package/docs/modules/core_ValueSimulator.html +0 -1
- package/docs/modules/core_hoc.html +0 -1
- package/docs/modules/hooks.html +0 -1
- package/docs/modules/hooks_adsHooks.html +0 -1
- package/docs/modules/hooks_commandHooks.html +0 -1
- package/docs/modules/hooks_useAutoCoreTag.ts.html +0 -52
- package/docs/modules/hooks_useScaledValue.html +0 -1
- package/docs/modules/hub.html +0 -1
- package/docs/modules/hub_CommandMessage.html +0 -1
- package/docs/modules/hub_HubBase.html +0 -1
- package/docs/modules/hub_HubSimulate.html +0 -1
- package/docs/modules/hub_HubTauri.html +0 -1
- package/docs/modules/hub_HubWebSocket.html +0 -1
- package/docs/modules.html +0 -23
- package/docs/types/components_IndicatorButton.IndicatorButtonOptionsType.html +0 -1
- package/docs/types/core_AutoCoreTagTypes.ExtractByTag.html +0 -2
- package/docs/types/core_AutoCoreTagTypes.PrimitiveKind.html +0 -1
- package/docs/types/core_AutoCoreTagTypes.TagConfig.html +0 -16
- package/docs/types/core_AutoCoreTagTypes.TagValueMap.html +0 -1
- package/docs/types/core_AutoCoreTagTypes.TagValueOf.html +0 -1
- package/docs/types/core_EventEmitterContext.EmitterDispatchFunction.html +0 -3
- package/docs/types/core_EventEmitterContext.EmitterSubscribeFunction.html +0 -3
- package/docs/types/core_EventEmitterContext.EmitterUnsubscribeFunction.html +0 -3
- package/docs/types/core_NumerableTypes.NumerableFormatOptions.html +0 -4
- package/docs/types/core_hoc.HocAddSubscriptionProps.html +0 -6
- package/docs/variables/components_AutoCoreDevPanel.AutoCoreDevPanel.html +0 -43
- package/docs/variables/components_BlocklyEditor.BlocklyEditor.html +0 -13
- package/docs/variables/components_BlocklyEditor.StandardToolbox.html +0 -1
- package/docs/variables/components_FileList.FileList.html +0 -23
- package/docs/variables/components_FileSelect.FileSelect.html +0 -1
- package/docs/variables/components_FitText.FitText.html +0 -4
- package/docs/variables/components_IndicatorButton.IndicatorButton.html +0 -1
- package/docs/variables/components_JogPanel.DefaultLinearJogButtons.html +0 -2
- package/docs/variables/components_JogPanel.DefaultRotationJogButtons.html +0 -2
- package/docs/variables/components_Osk.Osk.html +0 -1
- package/docs/variables/components_ProgressBarWithValue.ProgressBarWithValue.html +0 -1
- package/docs/variables/components_ToggleGroup.ToggleGroup.html +0 -1
- package/docs/variables/components_ValueInput.ValueInput.html +0 -4
- package/docs/variables/core_AutoCoreTagContext.AutoCoreTagContext.html +0 -1
- package/docs/variables/core_AutoCoreTagContext.AutoCoreTagProvider.html +0 -7
- package/docs/variables/core_EventEmitterContext.EventEmitterContext.html +0 -64
- package/docs/variables/core_EventEmitterContext.EventEmitterProvider.html +0 -10
- package/docs/variables/core_MaskPatterns.PrimeReactMaskPatterns.html +0 -14
- package/docs/variables/core_MaskPatterns.RegExMaskPatterns.html +0 -15
- package/docs/variables/core_PositionContext.DimensionsContext.html +0 -6
- package/docs/variables/hooks_useScaledValue.kMillimeters2Inches.html +0 -2
- package/docs/variables/hooks_useScaledValue.kNewtons2Pounds.html +0 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright (C) 2025 Automated Design Corp.. All Rights Reserved.
|
|
3
3
|
* Created Date: 2025-09-05 07:35:46
|
|
4
4
|
* -----
|
|
5
|
-
* Last Modified:
|
|
5
|
+
* Last Modified: 2026-01-17 10:32:56
|
|
6
6
|
* -----
|
|
7
7
|
*
|
|
8
8
|
*/
|
|
@@ -123,8 +123,27 @@ import type {
|
|
|
123
123
|
ScaleConfig,
|
|
124
124
|
} from "./AutoCoreTagTypes";
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Runtime type for the values map - allows any tag name to map to any value type.
|
|
128
|
+
* This is the generic type used when the specific tag configuration is not known at compile time.
|
|
129
|
+
*/
|
|
126
130
|
type VMapRuntime = Record<string, unknown>;
|
|
127
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The React Context that holds all AutoCore tag state and operations.
|
|
134
|
+
*
|
|
135
|
+
* This context provides:
|
|
136
|
+
* - `values`: Display values (with scaling/codecs applied)
|
|
137
|
+
* - `rawValues`: Raw controller values as received from server
|
|
138
|
+
* - `isLoading`: Whether initial data loading is complete
|
|
139
|
+
* - `write`: Function to write a value to a tag
|
|
140
|
+
* - `tap`: Function to pulse a boolean tag (true → delay → false)
|
|
141
|
+
* - `scales`: Current scale configurations
|
|
142
|
+
* - `updateScale`: Function to change a scale factor/label
|
|
143
|
+
*
|
|
144
|
+
* @see AutoCoreTagProvider for the provider implementation
|
|
145
|
+
* @see makeAutoCoreTagHooks for creating typed hooks from this context
|
|
146
|
+
*/
|
|
128
147
|
export const AutoCoreTagContext = createContext<BaseContextValue<VMapRuntime>>({
|
|
129
148
|
values: {},
|
|
130
149
|
rawValues: {},
|
|
@@ -135,9 +154,24 @@ export const AutoCoreTagContext = createContext<BaseContextValue<VMapRuntime>>({
|
|
|
135
154
|
updateScale: async () => { },
|
|
136
155
|
});
|
|
137
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Standard response envelope from the AutoCore server.
|
|
159
|
+
* All server responses follow this structure.
|
|
160
|
+
*
|
|
161
|
+
* @template T - The type of data contained in the response
|
|
162
|
+
* @property success - Whether the operation succeeded
|
|
163
|
+
* @property valid - Whether the response data is valid
|
|
164
|
+
* @property data - The actual response payload
|
|
165
|
+
*/
|
|
138
166
|
type HubEnvelope<T> = { success?: boolean; valid?: boolean; data?: T };
|
|
139
167
|
|
|
140
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Utility function to create a promise that resolves after a specified delay.
|
|
170
|
+
* Used for tap() pulse timing and retry delays.
|
|
171
|
+
*
|
|
172
|
+
* @param ms - Number of milliseconds to sleep
|
|
173
|
+
* @returns Promise that resolves after the delay
|
|
174
|
+
*/
|
|
141
175
|
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
142
176
|
|
|
143
177
|
|
|
@@ -294,36 +328,81 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
294
328
|
|
|
295
329
|
const [isLoading, setIsLoading] = useState(true);
|
|
296
330
|
|
|
297
|
-
/**
|
|
331
|
+
/**
|
|
332
|
+
* Converts a raw controller value to a display value.
|
|
333
|
+
*
|
|
334
|
+
* This function applies transformations in the following order:
|
|
335
|
+
* 1. **Numeric scaling**: If the tag has a scale group and the value is a number,
|
|
336
|
+
* multiply by the scale factor (e.g., mm → inches)
|
|
337
|
+
* 2. **JSON codec**: If the tag has a codec with `fromServer`, apply it to decode
|
|
338
|
+
* the server representation (e.g., JSON string → object)
|
|
339
|
+
* 3. **Pass-through**: If no transformations apply, return the raw value as-is
|
|
340
|
+
*
|
|
341
|
+
* @param tag - The tag configuration containing valueType, scale group, and optional codec
|
|
342
|
+
* @param raw - The raw value received from the controller/server
|
|
343
|
+
* @returns The display value suitable for showing in the UI
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* // For a position tag with scale factor 0.03937 (mm to inches)
|
|
347
|
+
* toDisplay(positionTag, 254) // returns 10.0 (inches)
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* // For a JSON tag with codec
|
|
351
|
+
* toDisplay(jsonTag, '{"name":"test"}') // returns { name: "test" }
|
|
352
|
+
*/
|
|
298
353
|
const toDisplay = useCallback((tag: TagConfig, raw: unknown): unknown => {
|
|
299
354
|
const { valueType, scale, codec } = tag;
|
|
300
355
|
|
|
301
|
-
// 1) numeric scaling
|
|
356
|
+
// 1) numeric scaling: multiply raw by scale factor
|
|
302
357
|
if (valueType === "number" && typeof raw === "number" && scale) {
|
|
303
358
|
const s = scaleRef.current[scale];
|
|
304
359
|
const factor = s?.scale ?? 1;
|
|
305
360
|
return raw * factor;
|
|
306
361
|
}
|
|
307
362
|
|
|
308
|
-
// 2) codec for json (optional)
|
|
363
|
+
// 2) codec for json (optional): decode server representation
|
|
309
364
|
if (valueType === "json" && codec?.fromServer) {
|
|
310
365
|
try { return codec.fromServer(raw); } catch { /* fall through */ }
|
|
311
366
|
}
|
|
312
367
|
|
|
313
|
-
// 3) pass-through
|
|
368
|
+
// 3) pass-through: no transformation needed
|
|
314
369
|
return raw;
|
|
315
370
|
}, []);
|
|
316
371
|
|
|
317
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* Converts a display value back to a raw controller value for writing.
|
|
374
|
+
*
|
|
375
|
+
* This is the inverse of `toDisplay`. It reverses transformations so that
|
|
376
|
+
* writes from the UI are sent to the server in controller units.
|
|
377
|
+
*
|
|
378
|
+
* Transformations are applied in reverse order:
|
|
379
|
+
* 1. **JSON codec**: If the tag has a codec with `toServer`, apply it to encode
|
|
380
|
+
* the value for the server (e.g., object → JSON string)
|
|
381
|
+
* 2. **Inverse scaling**: If the tag has a scale group and the value is a number,
|
|
382
|
+
* divide by the scale factor to convert back to controller units
|
|
383
|
+
*
|
|
384
|
+
* @param tag - The tag configuration containing valueType, scale group, and optional codec
|
|
385
|
+
* @param display - The display value from the UI (in user-facing units)
|
|
386
|
+
* @returns The raw value to send to the controller/server
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* // For a position tag with scale factor 0.03937 (mm to inches)
|
|
390
|
+
* // User enters 10 inches, need to send 254 mm to controller
|
|
391
|
+
* toServer(positionTag, 10.0) // returns 254.0 (mm)
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* // For a JSON tag with codec
|
|
395
|
+
* toServer(jsonTag, { name: "test" }) // returns '{"name":"test"}'
|
|
396
|
+
*/
|
|
318
397
|
const toServer = useCallback((tag: TagConfig, display: unknown): unknown => {
|
|
319
398
|
const { valueType, scale, codec } = tag;
|
|
320
399
|
|
|
321
|
-
// invert codec first (json)
|
|
400
|
+
// 1) invert codec first (json): encode for server
|
|
322
401
|
if (valueType === "json" && codec?.toServer) {
|
|
323
402
|
try { display = codec.toServer(display as any); } catch { /* fall through */ }
|
|
324
403
|
}
|
|
325
404
|
|
|
326
|
-
// inverse numeric scaling
|
|
405
|
+
// 2) inverse numeric scaling: divide by scale factor
|
|
327
406
|
if (valueType === "number" && typeof display === "number" && scale) {
|
|
328
407
|
const s = scaleRef.current[scale];
|
|
329
408
|
const factor = s?.scale ?? 1;
|
|
@@ -333,7 +412,26 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
333
412
|
return display;
|
|
334
413
|
}, []);
|
|
335
414
|
|
|
336
|
-
/**
|
|
415
|
+
/**
|
|
416
|
+
* Recomputes display values for all tags that use a specific scale group.
|
|
417
|
+
*
|
|
418
|
+
* This function is called when a scale factor changes. It:
|
|
419
|
+
* 1. Finds all tags that belong to the specified scale group
|
|
420
|
+
* 2. For each tag, retrieves the current RAW value (not the old display value)
|
|
421
|
+
* 3. Applies the NEW scale factor to compute updated display values
|
|
422
|
+
*
|
|
423
|
+
* **Important**: This always recomputes from RAW values to avoid cumulative
|
|
424
|
+
* precision errors. If we scaled from display values, repeated scale changes
|
|
425
|
+
* would accumulate floating-point errors.
|
|
426
|
+
*
|
|
427
|
+
* @param scaleName - The name of the scale group to recompute (e.g., "position", "load")
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* // When user switches from mm to inches:
|
|
431
|
+
* // 1. Scale factor changes from 1.0 to 0.03937
|
|
432
|
+
* // 2. rescaleFromRaw("position") is called
|
|
433
|
+
* // 3. All position tags are recomputed: raw_mm * 0.03937 = display_inches
|
|
434
|
+
*/
|
|
337
435
|
const rescaleFromRaw = useCallback((scaleName: string) => {
|
|
338
436
|
const affected = tags.filter(t => t.scale === scaleName);
|
|
339
437
|
if (!affected.length) return;
|
|
@@ -350,20 +448,80 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
350
448
|
});
|
|
351
449
|
}, [tags, toDisplay]);
|
|
352
450
|
|
|
353
|
-
/**
|
|
451
|
+
/**
|
|
452
|
+
* Handles incoming value updates from the server.
|
|
453
|
+
*
|
|
454
|
+
* This is the callback invoked when the server broadcasts a new value for a tag.
|
|
455
|
+
* It performs two operations:
|
|
456
|
+
*
|
|
457
|
+
* 1. **Store raw value**: Saves the unmodified controller value in `rawValues`.
|
|
458
|
+
* This is the source of truth that scaling is computed from.
|
|
459
|
+
*
|
|
460
|
+
* 2. **Compute display value**: Applies scaling/codecs via `toDisplay()` and
|
|
461
|
+
* stores the result in `values` for UI consumption.
|
|
462
|
+
*
|
|
463
|
+
* Both updates are done immutably - if the value hasn't changed, React state
|
|
464
|
+
* is not updated (preventing unnecessary re-renders).
|
|
465
|
+
*
|
|
466
|
+
* @param tag - The tag configuration for the updated value
|
|
467
|
+
* @param raw - The raw value received from the server (in controller units)
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* // Server broadcasts: MODBUS/simulator.holding_0 with value 42
|
|
471
|
+
* // EventEmitter callback receives data: { value: 42 }
|
|
472
|
+
* handleTagUpdate(holdingTag, 42);
|
|
473
|
+
* // rawValues["holdingReg0"] = 42
|
|
474
|
+
* // values["holdingReg0"] = 42 (or scaled if scale configured)
|
|
475
|
+
*/
|
|
354
476
|
const handleTagUpdate = useCallback((tag: TagConfig, raw: unknown) => {
|
|
355
|
-
//
|
|
477
|
+
// Store the raw controller value (source of truth for scaling)
|
|
356
478
|
setRawValues(prev =>
|
|
357
479
|
prev[tag.tagName] === raw ? prev : { ...prev, [tag.tagName]: raw }
|
|
358
480
|
);
|
|
359
|
-
//
|
|
481
|
+
// Compute and store the display value (with scaling/codecs applied)
|
|
360
482
|
const display = toDisplay(tag, raw);
|
|
361
483
|
setValues(prev =>
|
|
362
484
|
prev[tag.tagName] === display ? prev : { ...prev, [tag.tagName]: display }
|
|
363
485
|
);
|
|
364
486
|
}, [toDisplay]);
|
|
365
487
|
|
|
366
|
-
/**
|
|
488
|
+
/**
|
|
489
|
+
* Performs eager (one-time) reads for non-ADS domain tags.
|
|
490
|
+
*
|
|
491
|
+
* This function fetches initial values for tags that don't use the ADS protocol.
|
|
492
|
+
* It uses a worker pool pattern to read multiple tags concurrently while
|
|
493
|
+
* respecting rate limits.
|
|
494
|
+
*
|
|
495
|
+
* **Strategy for each tag:**
|
|
496
|
+
* 1. First, try `refresh` command - this asks the server to broadcast the current
|
|
497
|
+
* value (if the domain supports it). This is preferred because it uses the
|
|
498
|
+
* normal broadcast path.
|
|
499
|
+
* 2. If `refresh` fails, fall back to `read_value` - a direct request/response
|
|
500
|
+
* read of the tag value.
|
|
501
|
+
*
|
|
502
|
+
* **Rate limiting:**
|
|
503
|
+
* - Uses configurable concurrency (default 4 parallel workers)
|
|
504
|
+
* - Adds delay + random jitter between requests to avoid overwhelming the server
|
|
505
|
+
*
|
|
506
|
+
* **Error handling:**
|
|
507
|
+
* - Missing keys are common on fresh systems and are silently ignored
|
|
508
|
+
* - Other errors are caught but don't stop the worker pool
|
|
509
|
+
*
|
|
510
|
+
* @param domain - The domain to read from (e.g., "GNV", "MODBUS", "DATASTORE")
|
|
511
|
+
* @param tagList - List of tags to read values for
|
|
512
|
+
* @param opts - Optional configuration for rate limiting
|
|
513
|
+
* @param opts.concurrency - Number of parallel workers (default: 4)
|
|
514
|
+
* @param opts.minDelayMs - Minimum delay between requests per worker (default: 20ms)
|
|
515
|
+
* @param opts.jitterMs - Random additional delay range (default: 0-40ms)
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* // Read all GNV tags with default rate limiting
|
|
519
|
+
* await eagerPullNonADS("GNV", gnvTags);
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* // Read MODBUS tags with custom concurrency
|
|
523
|
+
* await eagerPullNonADS("MODBUS", modbusTags, { concurrency: 2, minDelayMs: 50 });
|
|
524
|
+
*/
|
|
367
525
|
const eagerPullNonADS = useCallback(
|
|
368
526
|
async (
|
|
369
527
|
domain: string,
|
|
@@ -374,21 +532,28 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
374
532
|
const minDelayMs = opts?.minDelayMs ?? 20;
|
|
375
533
|
const jitterMs = opts?.jitterMs ?? 40;
|
|
376
534
|
|
|
535
|
+
// Shared index counter for work-stealing pattern
|
|
377
536
|
let index = 0;
|
|
537
|
+
|
|
538
|
+
// Create worker pool - each worker pulls tags from shared queue
|
|
378
539
|
const workers = Array.from({ length: concurrency }, () => (async () => {
|
|
379
540
|
while (true) {
|
|
541
|
+
// Atomically grab next tag index
|
|
380
542
|
const i = index++;
|
|
381
|
-
if (i >= tagList.length) return;
|
|
543
|
+
if (i >= tagList.length) return; // No more work
|
|
382
544
|
const tag = tagList[i];
|
|
383
545
|
|
|
384
546
|
try {
|
|
547
|
+
// Strategy 1: Try refresh (asks server to broadcast current value)
|
|
385
548
|
let usedPublishPath = false;
|
|
386
549
|
try {
|
|
387
550
|
await invoke(domain, "refresh", { topic: tag.symbolName });
|
|
388
551
|
usedPublishPath = true;
|
|
389
|
-
} catch { /* fall through */ }
|
|
552
|
+
} catch { /* refresh not supported, fall through to read_value */ }
|
|
390
553
|
|
|
554
|
+
// Strategy 2: Fall back to direct read_value
|
|
391
555
|
if (!usedPublishPath) {
|
|
556
|
+
// Build domain-specific payload (GNV uses group:"ux")
|
|
392
557
|
const payload =
|
|
393
558
|
domain.toUpperCase() === "GNV"
|
|
394
559
|
? { group: "ux", key: tag.symbolName }
|
|
@@ -407,39 +572,70 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
407
572
|
}
|
|
408
573
|
}
|
|
409
574
|
} catch (outerErr) {
|
|
410
|
-
//
|
|
411
|
-
// console.warn(`eager pull worker error for ${domain}/${tag.symbolName}`, outerErr);
|
|
575
|
+
// Never throw out of the worker loop - one failure shouldn't stop others
|
|
412
576
|
} finally {
|
|
577
|
+
// Rate limiting: delay + jitter before next request
|
|
413
578
|
const extra = Math.floor(Math.random() * jitterMs);
|
|
414
579
|
await sleep(minDelayMs + extra);
|
|
415
580
|
}
|
|
416
581
|
}
|
|
417
582
|
})());
|
|
418
583
|
|
|
584
|
+
// Wait for all workers to complete
|
|
419
585
|
await Promise.all(workers);
|
|
420
586
|
},
|
|
421
587
|
[invoke, handleTagUpdate]
|
|
422
588
|
);
|
|
423
589
|
|
|
424
|
-
/**
|
|
590
|
+
/**
|
|
591
|
+
* Fetches server-stored scale configurations before loading tag values.
|
|
592
|
+
*
|
|
593
|
+
* Scales may be stored on the server (typically in the GNV domain) so that
|
|
594
|
+
* unit preferences persist across sessions. This function reads those stored
|
|
595
|
+
* scale values before any tag values are loaded.
|
|
596
|
+
*
|
|
597
|
+
* **Why this runs first:**
|
|
598
|
+
* If we loaded tag values before knowing the correct scale factor, the initial
|
|
599
|
+
* display would show wrong units, then "jump" when the scale arrived. By loading
|
|
600
|
+
* scales first, the first display of tag values is already in correct units.
|
|
601
|
+
*
|
|
602
|
+
* **Server format:**
|
|
603
|
+
* The server stores scales as JSON objects: `{ name, scale, label }`
|
|
604
|
+
* For example: `{ name: "position", scale: 0.03937, label: "in" }`
|
|
605
|
+
*
|
|
606
|
+
* @example Scale configuration with serverTag
|
|
607
|
+
* ```typescript
|
|
608
|
+
* const scales = {
|
|
609
|
+
* position: {
|
|
610
|
+
* name: "position",
|
|
611
|
+
* scale: 1.0, // default until server value loaded
|
|
612
|
+
* label: "mm", // default until server value loaded
|
|
613
|
+
* serverTag: { domain: "GNV", symbolName: "position_units" }
|
|
614
|
+
* }
|
|
615
|
+
* };
|
|
616
|
+
* ```
|
|
617
|
+
*/
|
|
425
618
|
const pullServerScales = useCallback(async () => {
|
|
426
619
|
for (const [scaleName, cfg] of Object.entries(scales)) {
|
|
620
|
+
// Skip scales that don't have server storage configured
|
|
427
621
|
if (!cfg.serverTag) continue;
|
|
428
622
|
const { domain, symbolName } = cfg.serverTag;
|
|
429
623
|
|
|
430
624
|
try {
|
|
625
|
+
// Read the scale configuration from server (GNV-style payload)
|
|
431
626
|
const respUnknown = await invoke(domain, "read_value", {
|
|
432
627
|
group: "ux",
|
|
433
628
|
key: symbolName,
|
|
434
629
|
});
|
|
435
630
|
|
|
436
|
-
//
|
|
631
|
+
// Parse response envelope
|
|
437
632
|
const env = respUnknown as HubEnvelope<string>;
|
|
438
633
|
if (env.data && (env.success ?? true) && (env.valid ?? true)) {
|
|
439
|
-
|
|
634
|
+
// Server stores as JSON string, parse it
|
|
440
635
|
const env_data = JSON.parse(env.data) as ScaleConfig;
|
|
441
636
|
if (env_data && typeof env_data.scale === "number" ) {
|
|
442
637
|
const { scale, label } = env_data;
|
|
638
|
+
// Update local scale state
|
|
443
639
|
setScaleValues(prev => ({
|
|
444
640
|
...prev,
|
|
445
641
|
[scaleName]: {
|
|
@@ -448,22 +644,73 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
448
644
|
label: label ?? prev[scaleName]?.label ?? "---",
|
|
449
645
|
},
|
|
450
646
|
}));
|
|
647
|
+
// Recompute any existing display values with new scale
|
|
451
648
|
rescaleFromRaw(scaleName);
|
|
452
649
|
}
|
|
453
650
|
}
|
|
454
651
|
} catch {
|
|
455
|
-
//
|
|
652
|
+
// Scale not present on server yet - use defaults from props
|
|
456
653
|
}
|
|
457
654
|
}
|
|
458
655
|
}, [invoke, scales, rescaleFromRaw]);
|
|
459
656
|
|
|
460
|
-
|
|
657
|
+
/**
|
|
658
|
+
* ==========================================================================
|
|
659
|
+
* MAIN SUBSCRIPTION WIRING EFFECT
|
|
660
|
+
* ==========================================================================
|
|
661
|
+
*
|
|
662
|
+
* This useEffect is the heart of the AutoCoreTagProvider. It sets up all
|
|
663
|
+
* the plumbing to receive live tag updates from the server.
|
|
664
|
+
*
|
|
665
|
+
* IMPORTANT CONCEPTS:
|
|
666
|
+
*
|
|
667
|
+
* 1. **Local EventEmitter subscriptions** (subscribe() function):
|
|
668
|
+
* - These register callbacks in the browser to receive broadcast messages
|
|
669
|
+
* - When the server sends a broadcast, the WebSocket layer receives it and
|
|
670
|
+
* the EventEmitter dispatches it to matching subscribers
|
|
671
|
+
* - This is CLIENT-SIDE ONLY - it does NOT tell the server anything
|
|
672
|
+
*
|
|
673
|
+
* 2. **Server-side subscriptions** (invoke with "subscribe" or "register_symbol"):
|
|
674
|
+
* - These tell the server "please start sending me updates for X"
|
|
675
|
+
* - Without this, the server doesn't know to broadcast anything
|
|
676
|
+
* - ADS: Uses "register_symbol" which implicitly subscribes
|
|
677
|
+
* - Other domains (MODBUS, etc.): Need explicit "subscribe" command
|
|
678
|
+
*
|
|
679
|
+
* DATA FLOW:
|
|
680
|
+
*
|
|
681
|
+
* [Server changes value]
|
|
682
|
+
* ↓
|
|
683
|
+
* [Server broadcasts if client is subscribed] ← requires server-side subscription
|
|
684
|
+
* ↓
|
|
685
|
+
* [WebSocket receives broadcast]
|
|
686
|
+
* ↓
|
|
687
|
+
* [EventEmitter dispatches to local subscribers] ← requires local subscription
|
|
688
|
+
* ↓
|
|
689
|
+
* [handleTagUpdate callback fires]
|
|
690
|
+
* ↓
|
|
691
|
+
* [rawValues and values state updated]
|
|
692
|
+
* ↓
|
|
693
|
+
* [React re-renders with new values]
|
|
694
|
+
*/
|
|
461
695
|
useEffect(() => {
|
|
696
|
+
// Track if component is still mounted (for async safety)
|
|
462
697
|
let mounted = true;
|
|
698
|
+
// Collect subscription IDs for cleanup on unmount
|
|
463
699
|
const subscriptions: number[] = [];
|
|
464
700
|
|
|
701
|
+
/**
|
|
702
|
+
* Main registration and subscription function.
|
|
703
|
+
*
|
|
704
|
+
* This orchestrates the entire setup process in a specific order to ensure
|
|
705
|
+
* values display correctly from the first render.
|
|
706
|
+
*/
|
|
465
707
|
const registerAndSubscribe = async () => {
|
|
466
|
-
|
|
708
|
+
|
|
709
|
+
// ──────────────────────────────────────────────────────────────────
|
|
710
|
+
// STEP 0: Group tags by domain
|
|
711
|
+
// ──────────────────────────────────────────────────────────────────
|
|
712
|
+
// We process tags domain-by-domain because each domain has different
|
|
713
|
+
// registration and subscription requirements.
|
|
467
714
|
const tagsByDomain = new Map<string, TagConfig[]>();
|
|
468
715
|
for (const tag of tags) {
|
|
469
716
|
const list = tagsByDomain.get(tag.domain);
|
|
@@ -471,12 +718,27 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
471
718
|
}
|
|
472
719
|
|
|
473
720
|
try {
|
|
474
|
-
//
|
|
721
|
+
// ──────────────────────────────────────────────────────────────
|
|
722
|
+
// STEP 1: Load server-stored scale factors FIRST
|
|
723
|
+
// ──────────────────────────────────────────────────────────────
|
|
724
|
+
// Why first? So when tag values arrive, they're already displayed
|
|
725
|
+
// in the correct units. Otherwise values would "jump" when scales load.
|
|
475
726
|
await pullServerScales();
|
|
476
727
|
|
|
477
|
-
//
|
|
728
|
+
// ──────────────────────────────────────────────────────────────
|
|
729
|
+
// STEP 2: Process each domain
|
|
730
|
+
// ──────────────────────────────────────────────────────────────
|
|
478
731
|
for (const [domain, tagList] of tagsByDomain.entries()) {
|
|
732
|
+
|
|
733
|
+
// ──────────────────────────────────────────────────────────
|
|
734
|
+
// STEP 2a: Subscribe to onchange notification
|
|
735
|
+
// ──────────────────────────────────────────────────────────
|
|
479
736
|
if (domain.toUpperCase() === "ADS") {
|
|
737
|
+
// ADS (Beckhoff TwinCAT) requires explicit symbol registration.
|
|
738
|
+
// This tells the ADS server "I want to access this PLC variable."
|
|
739
|
+
// The registration also sets up server-side subscriptions for
|
|
740
|
+
// the ADS notification system.
|
|
741
|
+
|
|
480
742
|
for (const tag of tagList) {
|
|
481
743
|
try {
|
|
482
744
|
const invokeOptions = tag.options
|
|
@@ -488,8 +750,29 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
488
750
|
}
|
|
489
751
|
}
|
|
490
752
|
}
|
|
753
|
+
else {
|
|
754
|
+
// For all other modules, use autocore-server standard subscription logic.
|
|
755
|
+
// This will be standardized in future versions of autocore-server, but for
|
|
756
|
+
// now we use the original autocore-server protocol system.
|
|
757
|
+
for ( const tag of tagList ) {
|
|
758
|
+
await invoke (domain, "subscribe", {data_filter: tag.symbolName});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
491
761
|
|
|
492
|
-
//
|
|
762
|
+
// ──────────────────────────────────────────────────────────
|
|
763
|
+
// STEP 2b: Set up LOCAL EventEmitter subscriptions
|
|
764
|
+
// ──────────────────────────────────────────────────────────
|
|
765
|
+
// This registers callbacks in the browser's EventEmitter.
|
|
766
|
+
// When a broadcast arrives via WebSocket, it will be dispatched
|
|
767
|
+
// to these callbacks.
|
|
768
|
+
//
|
|
769
|
+
// IMPORTANT: This is CLIENT-SIDE ONLY!
|
|
770
|
+
// It does NOT tell the server to start sending updates.
|
|
771
|
+
// For ADS, register_symbol (above) handles that.
|
|
772
|
+
// For other domains, we need to send a "subscribe" command.
|
|
773
|
+
//
|
|
774
|
+
// Topic format: "DOMAIN/symbolName"
|
|
775
|
+
// Example: "MODBUS/simulator.holding_0"
|
|
493
776
|
for (const tag of tagList) {
|
|
494
777
|
const id = subscribe(`${domain}/${tag.symbolName}`, (data) => {
|
|
495
778
|
if (!mounted) return;
|
|
@@ -498,21 +781,35 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
498
781
|
subscriptions.push(id);
|
|
499
782
|
}
|
|
500
783
|
|
|
501
|
-
//
|
|
784
|
+
// ──────────────────────────────────────────────────────────
|
|
785
|
+
// STEP 2c: Eager reads for non-ADS domains
|
|
786
|
+
// ──────────────────────────────────────────────────────────
|
|
787
|
+
// For domains other than ADS, we do a one-time read to get
|
|
788
|
+
// initial values. ADS has its own "refresh" mechanism.
|
|
502
789
|
if (eagerRead && domain.toUpperCase() !== "ADS") {
|
|
503
790
|
await eagerPullNonADS(domain, tagList, { concurrency: 4, minDelayMs: 20, jitterMs: 40 });
|
|
504
791
|
}
|
|
505
792
|
}
|
|
506
793
|
|
|
507
|
-
//
|
|
794
|
+
// ──────────────────────────────────────────────────────────────
|
|
795
|
+
// STEP 3: ADS one-shot refresh
|
|
796
|
+
// ──────────────────────────────────────────────────────────────
|
|
797
|
+
// Tells the ADS server to immediately publish current values for
|
|
798
|
+
// all registered symbols. This gives us initial values quickly.
|
|
508
799
|
if (eagerRead) {
|
|
509
800
|
await invoke("ADS", "refresh", {});
|
|
510
801
|
}
|
|
511
802
|
} finally {
|
|
803
|
+
// Mark loading complete (with small delay for UI smoothness)
|
|
512
804
|
if (mounted) setTimeout(() => mounted && setIsLoading(false), 100);
|
|
513
805
|
}
|
|
514
806
|
};
|
|
515
807
|
|
|
808
|
+
/**
|
|
809
|
+
* Wrapper to ensure registerAndSubscribe only runs once.
|
|
810
|
+
* Uses startedRef to prevent duplicate initialization from React strict mode
|
|
811
|
+
* or rapid re-renders.
|
|
812
|
+
*/
|
|
516
813
|
const safeRegister = async () => {
|
|
517
814
|
if (!mounted || startedRef.current) return;
|
|
518
815
|
startedRef.current = true;
|
|
@@ -520,16 +817,24 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
520
817
|
finally { if (mounted) setTimeout(() => mounted && setIsLoading(false), 100); }
|
|
521
818
|
};
|
|
522
819
|
|
|
820
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
821
|
+
// INITIALIZATION: Wait for WebSocket connection before registering
|
|
822
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
523
823
|
if (!isConnected()) {
|
|
824
|
+
// Not connected yet - wait for connection event
|
|
524
825
|
const id = subscribe("HUB/connected", () => {
|
|
525
826
|
unsubscribe(id);
|
|
526
827
|
void safeRegister();
|
|
527
828
|
});
|
|
528
829
|
subscriptions.push(id);
|
|
529
830
|
} else {
|
|
831
|
+
// Already connected - register immediately
|
|
530
832
|
void safeRegister();
|
|
531
833
|
}
|
|
532
834
|
|
|
835
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
836
|
+
// CLEANUP: Unsubscribe from all local subscriptions on unmount
|
|
837
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
533
838
|
return () => {
|
|
534
839
|
mounted = false;
|
|
535
840
|
subscriptions.forEach(unsubscribe);
|
|
@@ -537,7 +842,11 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
537
842
|
};
|
|
538
843
|
}, [subscribe, unsubscribe, isConnected, invoke, eagerRead, tags, pullServerScales, eagerPullNonADS, handleTagUpdate]);
|
|
539
844
|
|
|
540
|
-
/**
|
|
845
|
+
/**
|
|
846
|
+
* Memoized list of scale configurations that have server-side storage.
|
|
847
|
+
* Used to set up live subscriptions for scale changes (e.g., when another
|
|
848
|
+
* user changes unit preferences).
|
|
849
|
+
*/
|
|
541
850
|
const scaleServerSubs = useMemo(
|
|
542
851
|
() =>
|
|
543
852
|
Object.entries(scales)
|
|
@@ -550,16 +859,31 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
550
859
|
[scales]
|
|
551
860
|
);
|
|
552
861
|
|
|
862
|
+
/**
|
|
863
|
+
* ==========================================================================
|
|
864
|
+
* SCALE BROADCAST SUBSCRIPTIONS
|
|
865
|
+
* ==========================================================================
|
|
866
|
+
*
|
|
867
|
+
* This effect sets up live subscriptions to server-stored scale values.
|
|
868
|
+
* When a scale changes on the server (e.g., another user switches from mm to inches),
|
|
869
|
+
* this subscription receives the update and recomputes all affected display values.
|
|
870
|
+
*
|
|
871
|
+
* Note: This is separate from pullServerScales() which does a one-time read.
|
|
872
|
+
* This effect keeps the UI in sync with server changes in real-time.
|
|
873
|
+
*/
|
|
553
874
|
useEffect(() => {
|
|
554
875
|
let mounted = true;
|
|
555
876
|
const subs: number[] = [];
|
|
556
877
|
|
|
557
878
|
for (const { scaleName, domain, symbolName } of scaleServerSubs) {
|
|
879
|
+
// Subscribe to broadcasts for this scale's server tag
|
|
558
880
|
const id = subscribe(`${domain}/${symbolName}`, (data) => {
|
|
559
881
|
if (!mounted) return;
|
|
560
882
|
const v = data?.value;
|
|
883
|
+
// Validate the received data has the expected shape
|
|
561
884
|
if (v && typeof v === "object" && typeof (v as any).scale === "number") {
|
|
562
885
|
const { scale, label } = v as { scale: number; label?: string };
|
|
886
|
+
// Update local scale state
|
|
563
887
|
setScaleValues(prev => ({
|
|
564
888
|
...prev,
|
|
565
889
|
[scaleName]: {
|
|
@@ -568,57 +892,112 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
568
892
|
label: label ?? prev[scaleName]?.label ?? "---",
|
|
569
893
|
},
|
|
570
894
|
}));
|
|
895
|
+
// Recompute all display values that use this scale
|
|
571
896
|
rescaleFromRaw(scaleName);
|
|
572
897
|
}
|
|
573
898
|
});
|
|
574
899
|
subs.push(id);
|
|
575
900
|
}
|
|
576
901
|
|
|
902
|
+
// Cleanup subscriptions on unmount
|
|
577
903
|
return () => {
|
|
578
904
|
mounted = false;
|
|
579
905
|
subs.forEach(unsubscribe);
|
|
580
906
|
};
|
|
581
907
|
}, [subscribe, unsubscribe, scaleServerSubs, rescaleFromRaw]);
|
|
582
908
|
|
|
583
|
-
/**
|
|
909
|
+
/**
|
|
910
|
+
* Writes a value to a tag on the server.
|
|
911
|
+
*
|
|
912
|
+
* This function:
|
|
913
|
+
* 1. Looks up the tag configuration by name
|
|
914
|
+
* 2. Converts display value → server value (inverse scaling, codec encoding)
|
|
915
|
+
* 3. Builds the appropriate payload for the tag's domain
|
|
916
|
+
* 4. Sends the write command to the server
|
|
917
|
+
*
|
|
918
|
+
* **Domain-specific payloads:**
|
|
919
|
+
* - ADS: `{ symbol_name: "...", value: ... }`
|
|
920
|
+
* - GNV: `{ group: "ux", key: "...", value: ... }`
|
|
921
|
+
* - Other: `{ key: "...", value: ... }`
|
|
922
|
+
*
|
|
923
|
+
* @param tagName - The tag name (as defined in TagConfig, not symbolName)
|
|
924
|
+
* @param displayValue - The value in display units (will be inverse-scaled)
|
|
925
|
+
*
|
|
926
|
+
* @example
|
|
927
|
+
* // Write 10 inches to a position tag (scale converts to 254mm for server)
|
|
928
|
+
* await write("pressPosition", 10.0);
|
|
929
|
+
*
|
|
930
|
+
* @example
|
|
931
|
+
* // Write a boolean
|
|
932
|
+
* await write("isMotorsOn", true);
|
|
933
|
+
*/
|
|
584
934
|
const write = useCallback(
|
|
585
935
|
async (tagName: string, displayValue: unknown) => {
|
|
936
|
+
// Find the tag configuration
|
|
586
937
|
const cfg = tags.find((t) => t.tagName === tagName);
|
|
587
938
|
if (!cfg) {
|
|
588
939
|
console.error(`write(): unknown tag '${tagName}'`);
|
|
589
940
|
return;
|
|
590
941
|
}
|
|
591
942
|
|
|
943
|
+
// Convert display value to server value (inverse scaling/codec)
|
|
592
944
|
const serverValue = toServer(cfg, displayValue);
|
|
593
945
|
|
|
946
|
+
// Build domain-specific payload
|
|
594
947
|
let payload: any;
|
|
595
948
|
const dom = cfg.domain.toUpperCase();
|
|
596
949
|
if (dom === "ADS") {
|
|
950
|
+
// ADS uses symbol_name
|
|
597
951
|
payload = { symbol_name: cfg.symbolName, value: serverValue };
|
|
598
952
|
} else if (dom === "GNV") {
|
|
953
|
+
// GNV uses group + key
|
|
599
954
|
payload = { group: "ux", key: cfg.symbolName, value: serverValue };
|
|
600
955
|
} else {
|
|
956
|
+
// Other domains (MODBUS, DATASTORE, etc.) use key
|
|
601
957
|
payload = { key: cfg.symbolName, value: serverValue };
|
|
602
958
|
}
|
|
603
959
|
|
|
960
|
+
// Send to server
|
|
604
961
|
await invoke(cfg.domain, "write_value", payload);
|
|
605
962
|
},
|
|
606
963
|
[tags, invoke, toServer]
|
|
607
964
|
);
|
|
608
965
|
|
|
609
|
-
/**
|
|
966
|
+
/**
|
|
967
|
+
* Pulses a boolean tag: writes true, waits 300ms, then writes false.
|
|
968
|
+
*
|
|
969
|
+
* This is commonly used for "momentary" buttons or triggers in automation
|
|
970
|
+
* systems where you need to send a rising edge rather than toggle state.
|
|
971
|
+
*
|
|
972
|
+
* **Only works with boolean tags** - logs a warning if called on other types.
|
|
973
|
+
*
|
|
974
|
+
* @param tagName - The tag name (must be a boolean type tag)
|
|
975
|
+
*
|
|
976
|
+
* @example
|
|
977
|
+
* // Pulse a "start cycle" button
|
|
978
|
+
* await tap("cycleStart");
|
|
979
|
+
* // Server receives: true at t=0, false at t=300ms
|
|
980
|
+
*
|
|
981
|
+
* @example
|
|
982
|
+
* // In a React component
|
|
983
|
+
* const { tap } = useAutoCoreTag("motorsEnable");
|
|
984
|
+
* return <button onClick={() => tap()}>Enable Motors</button>;
|
|
985
|
+
*/
|
|
610
986
|
const tap = useCallback(
|
|
611
987
|
async (tagName: string) => {
|
|
988
|
+
// Find the tag configuration
|
|
612
989
|
const cfg = tags.find((t) => t.tagName === tagName);
|
|
613
990
|
if (!cfg) {
|
|
614
991
|
console.error(`tap(): unknown tag '${tagName}'`);
|
|
615
992
|
return;
|
|
616
993
|
}
|
|
994
|
+
// Only boolean tags can be tapped
|
|
617
995
|
if (cfg.valueType !== "boolean") {
|
|
618
996
|
console.warn(`tap(): tag '${tagName}' is not a boolean type`);
|
|
619
997
|
return;
|
|
620
998
|
}
|
|
621
999
|
|
|
1000
|
+
// Helper to build domain-specific payload
|
|
622
1001
|
const dom = cfg.domain.toUpperCase();
|
|
623
1002
|
const mkPayload = (val: boolean) =>
|
|
624
1003
|
dom === "ADS"
|
|
@@ -627,6 +1006,7 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
627
1006
|
? { group: "ux", key: cfg.symbolName, value: val }
|
|
628
1007
|
: { key: cfg.symbolName, value: val };
|
|
629
1008
|
|
|
1009
|
+
// Pulse: true → wait → false
|
|
630
1010
|
await invoke(cfg.domain, "write_value", mkPayload(true));
|
|
631
1011
|
await sleep(300);
|
|
632
1012
|
await invoke(cfg.domain, "write_value", mkPayload(false));
|
|
@@ -634,7 +1014,39 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
634
1014
|
[tags, invoke]
|
|
635
1015
|
);
|
|
636
1016
|
|
|
637
|
-
/**
|
|
1017
|
+
/**
|
|
1018
|
+
* Updates a scale factor and label, optionally persisting to the server.
|
|
1019
|
+
*
|
|
1020
|
+
* This function:
|
|
1021
|
+
* 1. Validates the scale name exists
|
|
1022
|
+
* 2. If the scale has a serverTag, writes the new configuration to the server
|
|
1023
|
+
* (so other clients and future sessions use the same units)
|
|
1024
|
+
* 3. Updates local scale state
|
|
1025
|
+
* 4. Recomputes all affected display values from raw
|
|
1026
|
+
*
|
|
1027
|
+
* **Server format:** `{ name: "position", scale: 0.03937, label: "in" }`
|
|
1028
|
+
*
|
|
1029
|
+
* @param scaleName - The scale group name (e.g., "position", "load")
|
|
1030
|
+
* @param newScale - The new scale factor to apply (raw * scale = display)
|
|
1031
|
+
* @param newLabel - The new unit label to display (e.g., "in", "mm", "lbs")
|
|
1032
|
+
*
|
|
1033
|
+
* @example
|
|
1034
|
+
* // Switch position display from mm to inches
|
|
1035
|
+
* await updateScale("position", 0.03937, "in");
|
|
1036
|
+
*
|
|
1037
|
+
* @example
|
|
1038
|
+
* // In a React component with unit selector
|
|
1039
|
+
* const { scales, updateScale } = useScales();
|
|
1040
|
+
* return (
|
|
1041
|
+
* <select onChange={(e) => {
|
|
1042
|
+
* if (e.target.value === "mm") updateScale("position", 1.0, "mm");
|
|
1043
|
+
* if (e.target.value === "in") updateScale("position", 0.03937, "in");
|
|
1044
|
+
* }}>
|
|
1045
|
+
* <option value="mm">Millimeters</option>
|
|
1046
|
+
* <option value="in">Inches</option>
|
|
1047
|
+
* </select>
|
|
1048
|
+
* );
|
|
1049
|
+
*/
|
|
638
1050
|
const updateScale = useCallback(
|
|
639
1051
|
async (scaleName: string, newScale: number, newLabel: string) => {
|
|
640
1052
|
const cfg = scaleValues[scaleName];
|
|
@@ -643,7 +1055,7 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
643
1055
|
return;
|
|
644
1056
|
}
|
|
645
1057
|
|
|
646
|
-
// If scale has a server tag,
|
|
1058
|
+
// If scale has a server tag, persist to server for other clients/sessions
|
|
647
1059
|
if (cfg.serverTag) {
|
|
648
1060
|
await invoke(cfg.serverTag.domain, "write_value", {
|
|
649
1061
|
group: "ux",
|
|
@@ -652,44 +1064,71 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
652
1064
|
});
|
|
653
1065
|
}
|
|
654
1066
|
|
|
655
|
-
// Update local scale
|
|
1067
|
+
// Update local scale state
|
|
656
1068
|
setScaleValues(prev => ({
|
|
657
1069
|
...prev,
|
|
658
1070
|
[scaleName]: { ...prev[scaleName], scale: newScale, label: newLabel },
|
|
659
1071
|
}));
|
|
1072
|
+
|
|
1073
|
+
// Recompute all display values that use this scale (from raw values)
|
|
660
1074
|
rescaleFromRaw(scaleName);
|
|
661
1075
|
},
|
|
662
1076
|
[scaleValues, invoke, rescaleFromRaw]
|
|
663
1077
|
);
|
|
664
1078
|
|
|
665
|
-
/**
|
|
1079
|
+
/**
|
|
1080
|
+
* ==========================================================================
|
|
1081
|
+
* DISPLAY VALUES RECOMPUTATION EFFECT
|
|
1082
|
+
* ==========================================================================
|
|
1083
|
+
*
|
|
1084
|
+
* This effect ensures display values stay in sync when:
|
|
1085
|
+
* 1. Component mounts with initial/seeded raw values
|
|
1086
|
+
* 2. Scale factors change (scaleValues dependency)
|
|
1087
|
+
* 3. Tag configuration changes (tags dependency)
|
|
1088
|
+
*
|
|
1089
|
+
* It iterates all tags and recomputes display values from raw using the
|
|
1090
|
+
* current scale factors. This is a safety net that catches edge cases
|
|
1091
|
+
* where individual updates might have been missed.
|
|
1092
|
+
*
|
|
1093
|
+
* Note: Individual updates via handleTagUpdate are more efficient for
|
|
1094
|
+
* normal operation. This effect handles bulk recomputation scenarios.
|
|
1095
|
+
*/
|
|
666
1096
|
useEffect(() => {
|
|
667
|
-
//
|
|
1097
|
+
// Recompute every tag's display value from its raw value
|
|
668
1098
|
setValues(prev => {
|
|
669
1099
|
const next = { ...prev };
|
|
670
1100
|
for (const tag of tags) {
|
|
671
1101
|
const raw = rawRef.current[tag.tagName];
|
|
672
1102
|
if (raw === undefined) continue;
|
|
673
1103
|
const disp = toDisplay(tag, raw);
|
|
1104
|
+
// Only update if value actually changed (avoid unnecessary re-renders)
|
|
674
1105
|
if (next[tag.tagName] !== disp) next[tag.tagName] = disp;
|
|
675
1106
|
}
|
|
676
1107
|
return next;
|
|
677
1108
|
});
|
|
678
1109
|
}, [tags, toDisplay, scaleValues]);
|
|
679
1110
|
|
|
1111
|
+
/**
|
|
1112
|
+
* Memoized context value to prevent unnecessary re-renders.
|
|
1113
|
+
* Only recreates when one of the dependencies actually changes.
|
|
1114
|
+
*/
|
|
680
1115
|
const ctxValue = useMemo<BaseContextValue<VMapRuntime>>(
|
|
681
1116
|
() => ({
|
|
682
|
-
values,
|
|
683
|
-
rawValues,
|
|
684
|
-
isLoading,
|
|
685
|
-
write,
|
|
686
|
-
tap,
|
|
687
|
-
scales: scaleValues,
|
|
688
|
-
updateScale,
|
|
1117
|
+
values, // Display values (scaled/decoded) - what the UI shows
|
|
1118
|
+
rawValues, // Raw controller values - source of truth for scaling
|
|
1119
|
+
isLoading, // True until initial setup completes
|
|
1120
|
+
write, // Function to write a value to a tag
|
|
1121
|
+
tap, // Function to pulse a boolean tag
|
|
1122
|
+
scales: scaleValues, // Current scale configurations
|
|
1123
|
+
updateScale, // Function to change scale factor/label
|
|
689
1124
|
}),
|
|
690
1125
|
[values, rawValues, isLoading, write, tap, scaleValues, updateScale]
|
|
691
1126
|
);
|
|
692
1127
|
|
|
1128
|
+
/**
|
|
1129
|
+
* Render the context provider with all state and functions available
|
|
1130
|
+
* to child components via useContext(AutoCoreTagContext) or the typed hooks.
|
|
1131
|
+
*/
|
|
693
1132
|
return (
|
|
694
1133
|
<AutoCoreTagContext.Provider value={ctxValue}>
|
|
695
1134
|
{children}
|