@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.
Files changed (192) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/dist/components/FileSelect.d.ts.map +1 -1
  3. package/dist/components/FileSelect.js +1 -1
  4. package/dist/core/AutoCoreTagContext.d.ts +19 -0
  5. package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
  6. package/dist/core/AutoCoreTagContext.js +1 -1
  7. package/dist/themes/adc-dark/blue/theme.css +305 -0
  8. package/dist/themes/adc-dark/blue/theme.css.map +1 -1
  9. package/package.json +3 -3
  10. package/src/components/FileSelect.tsx +31 -11
  11. package/src/core/AutoCoreTagContext.tsx +483 -44
  12. package/src/themes/adc-dark/_extensions.scss +327 -0
  13. package/docs/.nojekyll +0 -1
  14. package/docs/assets/hierarchy.js +0 -1
  15. package/docs/assets/highlight.css +0 -134
  16. package/docs/assets/icons.js +0 -18
  17. package/docs/assets/icons.svg +0 -1
  18. package/docs/assets/main.js +0 -60
  19. package/docs/assets/navigation.js +0 -1
  20. package/docs/assets/search.js +0 -1
  21. package/docs/assets/style.css +0 -1633
  22. package/docs/classes/components_CodeEditor.CodeEditor.html +0 -135
  23. package/docs/classes/components_Indicator.Indicator.html +0 -122
  24. package/docs/classes/components_IndicatorRect.IndicatorRect.html +0 -121
  25. package/docs/classes/components_JogPanel.JogPanel.html +0 -136
  26. package/docs/classes/components_Lamp.Lamp.html +0 -122
  27. package/docs/classes/components_OskDialog.OskDialog.html +0 -125
  28. package/docs/classes/components_TextInput.TextInput.html +0 -125
  29. package/docs/classes/components_ValueDisplay.ValueDisplay.html +0 -148
  30. package/docs/classes/components_ValueIndicator.ValueIndicator.html +0 -126
  31. package/docs/classes/core_ValueSimulator.ValueSimulator.html +0 -51
  32. package/docs/classes/hub_HubBase.HubBase.html +0 -106
  33. package/docs/classes/hub_HubSimulate.HubSimulate.html +0 -75
  34. package/docs/classes/hub_HubTauri.HubTauri.html +0 -93
  35. package/docs/classes/hub_HubWebSocket.HubWebSocket.html +0 -112
  36. package/docs/documents/core_AutoCoreTagContext.AutoCoreTagContext.html +0 -148
  37. package/docs/enums/components_JogPanel.JogDistanceAction.html +0 -5
  38. package/docs/enums/components_JogPanel.JogPanelAction.html +0 -18
  39. package/docs/enums/components_JogPanel.JogSpeedAction.html +0 -5
  40. package/docs/enums/core_ActionMode.ActionMode.html +0 -6
  41. package/docs/enums/core_IndicatorColor.IndicatorColor.html +0 -23
  42. package/docs/functions/assets.BlocklyLogo.html +0 -1
  43. package/docs/functions/assets.Distance.html +0 -1
  44. package/docs/functions/assets.JogLong.html +0 -1
  45. package/docs/functions/assets.JogMedium.html +0 -1
  46. package/docs/functions/assets.JogShort.html +0 -1
  47. package/docs/functions/assets.PythonLogo.html +0 -1
  48. package/docs/functions/assets.Rotation3D.html +0 -1
  49. package/docs/functions/assets.RotationCcw.html +0 -1
  50. package/docs/functions/assets.RotationCcwA.html +0 -1
  51. package/docs/functions/assets.RotationCcwB.html +0 -1
  52. package/docs/functions/assets.RotationCcwC.html +0 -1
  53. package/docs/functions/assets.RotationCw.html +0 -1
  54. package/docs/functions/assets.RotationCwA.html +0 -1
  55. package/docs/functions/assets.RotationCwB.html +0 -1
  56. package/docs/functions/assets.RotationCwC.html +0 -1
  57. package/docs/functions/assets.Run.html +0 -1
  58. package/docs/functions/assets.Speed.html +0 -1
  59. package/docs/functions/assets.SpeedFast.html +0 -1
  60. package/docs/functions/assets.SpeedMedium.html +0 -1
  61. package/docs/functions/assets.SpeedNone.html +0 -1
  62. package/docs/functions/assets.SpeedSlow.html +0 -1
  63. package/docs/functions/assets.Walk.html +0 -1
  64. package/docs/functions/components_BlocklyEditor.createCustomToolbox.html +0 -6
  65. package/docs/functions/core_UniqueId.UniqueId.html +0 -9
  66. package/docs/functions/core_hoc.hocAddSubscription.html +0 -6
  67. package/docs/functions/hooks_adsHooks.useAdsRegisterSymbols.html +0 -16
  68. package/docs/functions/hooks_adsHooks.useAdsTapValue.html +0 -8
  69. package/docs/functions/hooks_adsHooks.useAdsWriteScaledValue.html +0 -18
  70. package/docs/functions/hooks_adsHooks.useAdsWriteValue.html +0 -9
  71. package/docs/functions/hooks_commandHooks.useRegisterSymbols.html +0 -16
  72. package/docs/functions/hooks_commandHooks.useTapValue.html +0 -10
  73. package/docs/functions/hooks_commandHooks.useWriteScaledValue.html +0 -18
  74. package/docs/functions/hooks_commandHooks.useWriteValue.html +0 -11
  75. package/docs/functions/hooks_useAutoCoreTag.ts.makeAutoCoreTagHooks.html +0 -12
  76. package/docs/functions/hooks_useScaledValue.useScaledValue.html +0 -18
  77. package/docs/functions/hub.createHub.html +0 -3
  78. package/docs/hierarchy.html +0 -1
  79. package/docs/index.html +0 -148
  80. package/docs/interfaces/components_IndicatorButton.IndicatorButtonProps.html +0 -654
  81. package/docs/interfaces/components_IndicatorRect.IndicatorRectProps.html +0 -37
  82. package/docs/interfaces/components_JogPanel.JogPanelButtonDefinition.html +0 -5
  83. package/docs/interfaces/components_ToggleGroup.ToggleGroupProps.html +0 -644
  84. package/docs/interfaces/core_AutoCoreTagTypes.BaseContextValue.html +0 -12
  85. package/docs/interfaces/core_AutoCoreTagTypes.ScaleConfig.html +0 -13
  86. package/docs/interfaces/core_EventEmitterContext.Action.html +0 -8
  87. package/docs/interfaces/core_EventEmitterContext.EventEmitterContextType.html +0 -33
  88. package/docs/interfaces/core_EventEmitterContext.State.html +0 -8
  89. package/docs/interfaces/core_EventEmitterContext.Subscription.html +0 -6
  90. package/docs/interfaces/core_IndicatorButtonState.IndicatorButtonState.html +0 -10
  91. package/docs/interfaces/core_PositionContext.IPositionContext.html +0 -17
  92. package/docs/interfaces/hub_CommandMessage.CommandMessage.html +0 -6
  93. package/docs/interfaces/hub_CommandMessage.CommandMessageResult.html +0 -4
  94. package/docs/modules/assets.html +0 -1
  95. package/docs/modules/assets_BlocklyLogo.html +0 -1
  96. package/docs/modules/assets_Distance.html +0 -1
  97. package/docs/modules/assets_JogLong.html +0 -1
  98. package/docs/modules/assets_JogMedium.html +0 -1
  99. package/docs/modules/assets_JogShort.html +0 -1
  100. package/docs/modules/assets_PythonLogo.html +0 -1
  101. package/docs/modules/assets_Rotation3D.html +0 -1
  102. package/docs/modules/assets_RotationCcw.html +0 -1
  103. package/docs/modules/assets_RotationCcwA.html +0 -1
  104. package/docs/modules/assets_RotationCcwB.html +0 -1
  105. package/docs/modules/assets_RotationCcwC.html +0 -1
  106. package/docs/modules/assets_RotationCw.html +0 -1
  107. package/docs/modules/assets_RotationCwA.html +0 -1
  108. package/docs/modules/assets_RotationCwB.html +0 -1
  109. package/docs/modules/assets_RotationCwC.html +0 -1
  110. package/docs/modules/assets_Run.html +0 -1
  111. package/docs/modules/assets_Speed.html +0 -1
  112. package/docs/modules/assets_SpeedFast.html +0 -1
  113. package/docs/modules/assets_SpeedMedium.html +0 -1
  114. package/docs/modules/assets_SpeedNone.html +0 -1
  115. package/docs/modules/assets_SpeedSlow.html +0 -1
  116. package/docs/modules/assets_Walk.html +0 -1
  117. package/docs/modules/components_AutoCoreDevPanel.html +0 -20
  118. package/docs/modules/components_BlocklyEditor.html +0 -1
  119. package/docs/modules/components_CodeEditor.html +0 -1
  120. package/docs/modules/components_FileList.html +0 -1
  121. package/docs/modules/components_FileSelect.html +0 -1
  122. package/docs/modules/components_FitText.html +0 -1
  123. package/docs/modules/components_Indicator.html +0 -1
  124. package/docs/modules/components_IndicatorButton.html +0 -1
  125. package/docs/modules/components_IndicatorRect.html +0 -1
  126. package/docs/modules/components_JogPanel.html +0 -1
  127. package/docs/modules/components_Lamp.html +0 -1
  128. package/docs/modules/components_Osk.html +0 -1
  129. package/docs/modules/components_OskDialog.html +0 -1
  130. package/docs/modules/components_ProgressBarWithValue.html +0 -1
  131. package/docs/modules/components_TextInput.html +0 -1
  132. package/docs/modules/components_ToggleGroup.html +0 -1
  133. package/docs/modules/components_ValueDisplay.html +0 -1
  134. package/docs/modules/components_ValueIndicator.html +0 -1
  135. package/docs/modules/components_ValueInput.html +0 -1
  136. package/docs/modules/core_ActionMode.html +0 -1
  137. package/docs/modules/core_AutoCoreTagContext.html +0 -11
  138. package/docs/modules/core_AutoCoreTagTypes.html +0 -1
  139. package/docs/modules/core_EventEmitterContext.html +0 -53
  140. package/docs/modules/core_IndicatorButtonState.html +0 -1
  141. package/docs/modules/core_IndicatorColor.html +0 -1
  142. package/docs/modules/core_MaskPatterns.html +0 -1
  143. package/docs/modules/core_NumerableTypes.html +0 -1
  144. package/docs/modules/core_PositionContext.html +0 -1
  145. package/docs/modules/core_UniqueId.html +0 -1
  146. package/docs/modules/core_ValueSimulator.html +0 -1
  147. package/docs/modules/core_hoc.html +0 -1
  148. package/docs/modules/hooks.html +0 -1
  149. package/docs/modules/hooks_adsHooks.html +0 -1
  150. package/docs/modules/hooks_commandHooks.html +0 -1
  151. package/docs/modules/hooks_useAutoCoreTag.ts.html +0 -52
  152. package/docs/modules/hooks_useScaledValue.html +0 -1
  153. package/docs/modules/hub.html +0 -1
  154. package/docs/modules/hub_CommandMessage.html +0 -1
  155. package/docs/modules/hub_HubBase.html +0 -1
  156. package/docs/modules/hub_HubSimulate.html +0 -1
  157. package/docs/modules/hub_HubTauri.html +0 -1
  158. package/docs/modules/hub_HubWebSocket.html +0 -1
  159. package/docs/modules.html +0 -23
  160. package/docs/types/components_IndicatorButton.IndicatorButtonOptionsType.html +0 -1
  161. package/docs/types/core_AutoCoreTagTypes.ExtractByTag.html +0 -2
  162. package/docs/types/core_AutoCoreTagTypes.PrimitiveKind.html +0 -1
  163. package/docs/types/core_AutoCoreTagTypes.TagConfig.html +0 -16
  164. package/docs/types/core_AutoCoreTagTypes.TagValueMap.html +0 -1
  165. package/docs/types/core_AutoCoreTagTypes.TagValueOf.html +0 -1
  166. package/docs/types/core_EventEmitterContext.EmitterDispatchFunction.html +0 -3
  167. package/docs/types/core_EventEmitterContext.EmitterSubscribeFunction.html +0 -3
  168. package/docs/types/core_EventEmitterContext.EmitterUnsubscribeFunction.html +0 -3
  169. package/docs/types/core_NumerableTypes.NumerableFormatOptions.html +0 -4
  170. package/docs/types/core_hoc.HocAddSubscriptionProps.html +0 -6
  171. package/docs/variables/components_AutoCoreDevPanel.AutoCoreDevPanel.html +0 -43
  172. package/docs/variables/components_BlocklyEditor.BlocklyEditor.html +0 -13
  173. package/docs/variables/components_BlocklyEditor.StandardToolbox.html +0 -1
  174. package/docs/variables/components_FileList.FileList.html +0 -23
  175. package/docs/variables/components_FileSelect.FileSelect.html +0 -1
  176. package/docs/variables/components_FitText.FitText.html +0 -4
  177. package/docs/variables/components_IndicatorButton.IndicatorButton.html +0 -1
  178. package/docs/variables/components_JogPanel.DefaultLinearJogButtons.html +0 -2
  179. package/docs/variables/components_JogPanel.DefaultRotationJogButtons.html +0 -2
  180. package/docs/variables/components_Osk.Osk.html +0 -1
  181. package/docs/variables/components_ProgressBarWithValue.ProgressBarWithValue.html +0 -1
  182. package/docs/variables/components_ToggleGroup.ToggleGroup.html +0 -1
  183. package/docs/variables/components_ValueInput.ValueInput.html +0 -4
  184. package/docs/variables/core_AutoCoreTagContext.AutoCoreTagContext.html +0 -1
  185. package/docs/variables/core_AutoCoreTagContext.AutoCoreTagProvider.html +0 -7
  186. package/docs/variables/core_EventEmitterContext.EventEmitterContext.html +0 -64
  187. package/docs/variables/core_EventEmitterContext.EventEmitterProvider.html +0 -10
  188. package/docs/variables/core_MaskPatterns.PrimeReactMaskPatterns.html +0 -14
  189. package/docs/variables/core_MaskPatterns.RegExMaskPatterns.html +0 -15
  190. package/docs/variables/core_PositionContext.DimensionsContext.html +0 -6
  191. package/docs/variables/hooks_useScaledValue.kMillimeters2Inches.html +0 -2
  192. 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: 2025-09-10 11:28:01
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
- /** Small sleep helper */
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
- /** Produce a display value from a raw value given the tag config + current scales. */
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
- /** Inverse of toDisplay for writes (display -> raw server value) */
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
- /** Recompute display values for all tags in this scale from **raw** */
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
- /** Subscribe handler for tag updates: buffer raw, compute display. */
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
- // write raw (immutably)
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
- // compute display from raw
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
- /** Eager pulls (non-ADS): read raw and run handleTagUpdate */
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
- // never throw out of the worker loop
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
- /** Pull server scale objects first thing (GNV envelope) */
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
- // narrow at runtime instead of forcing a direct cast
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
- // Ok if not present yet.
652
+ // Scale not present on server yet - use defaults from props
456
653
  }
457
654
  }
458
655
  }, [invoke, scales, rescaleFromRaw]);
459
656
 
460
- // Main subscription wiring
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
- // group tags by domain
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
- // 0) load scales first so first ADS values get correct factor
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
- // 1) ADS register (sequential is fine)
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
- // 2) subscribe
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
- // 3) eager reads (non-ADS)
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
- // 4) ADS one-shot refresh (after scales are known)
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
- /** Subscribe to server-driven scale tags (GNV object), recompute displays from raw. */
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
- /** Write with inverse scaling and proper domain envelopes. */
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
- /** Tap (boolean pulse) */
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
- /** Manual scale update (also writes to server if `serverTag` is present) */
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, write to server first (GNV-style)
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 + recompute displays from raw
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
- /** Derive initial display values from any seeded raw values (once scales are known) */
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
- // On mount or when scales change, recompute every numeric tag from raw
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}