@adcops/autocore-react 3.1.1 → 3.3.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 (252) hide show
  1. package/additional-docs/react_performance_notes.md +94 -0
  2. package/dist/components/AutoCoreDevPanel.d.ts.map +1 -1
  3. package/dist/components/AutoCoreDevPanel.js +1 -1
  4. package/dist/components/FileList.d.ts.map +1 -1
  5. package/dist/components/FileList.js +1 -1
  6. package/dist/components/FileSelect.d.ts.map +1 -1
  7. package/dist/components/FileSelect.js +1 -1
  8. package/dist/core/AutoCoreTagContext.d.ts +59 -185
  9. package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
  10. package/dist/core/AutoCoreTagContext.js +1 -1
  11. package/dist/core/AutoCoreTagTypes.d.ts +127 -6
  12. package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
  13. package/dist/core/CoreStreamTypes.d.ts +345 -0
  14. package/dist/core/CoreStreamTypes.d.ts.map +1 -0
  15. package/dist/core/CoreStreamTypes.js +1 -0
  16. package/dist/core/EventEmitterContext.d.ts +91 -473
  17. package/dist/core/EventEmitterContext.d.ts.map +1 -1
  18. package/dist/core/EventEmitterContext.js +1 -1
  19. package/dist/hooks/adsHooks.d.ts.map +1 -1
  20. package/dist/hooks/adsHooks.js +1 -1
  21. package/dist/hooks/commandHooks.d.ts +3 -3
  22. package/dist/hooks/commandHooks.d.ts.map +1 -1
  23. package/dist/hooks/commandHooks.js +1 -1
  24. package/dist/hooks/useAutoCoreTag.js +1 -1
  25. package/dist/hub/CommandMessage.d.ts +18 -9
  26. package/dist/hub/CommandMessage.d.ts.map +1 -1
  27. package/dist/hub/CommandMessage.js +1 -1
  28. package/dist/hub/DebugPanel.d.ts +31 -0
  29. package/dist/hub/DebugPanel.d.ts.map +1 -0
  30. package/dist/hub/DebugPanel.js +1 -0
  31. package/dist/hub/HubBase.d.ts +83 -129
  32. package/dist/hub/HubBase.d.ts.map +1 -1
  33. package/dist/hub/HubBase.js +1 -1
  34. package/dist/hub/HubSimulate.d.ts +41 -8
  35. package/dist/hub/HubSimulate.d.ts.map +1 -1
  36. package/dist/hub/HubSimulate.js +1 -1
  37. package/dist/hub/HubTauri.d.ts +24 -60
  38. package/dist/hub/HubTauri.d.ts.map +1 -1
  39. package/dist/hub/HubTauri.js +1 -1
  40. package/dist/hub/HubWebSocket.d.ts +33 -17
  41. package/dist/hub/HubWebSocket.d.ts.map +1 -1
  42. package/dist/hub/HubWebSocket.js +1 -1
  43. package/dist/hub/debug.d.ts +23 -0
  44. package/dist/hub/debug.d.ts.map +1 -0
  45. package/dist/hub/debug.js +1 -0
  46. package/dist/hub/index.d.ts +19 -4
  47. package/dist/hub/index.d.ts.map +1 -1
  48. package/dist/hub/index.js +1 -1
  49. package/package.json +8 -4
  50. package/src/components/AutoCoreDevPanel.tsx +14 -11
  51. package/src/components/FileList.tsx +5 -4
  52. package/src/components/FileSelect.tsx +2 -1
  53. package/src/core/ActionMode.ts +1 -1
  54. package/src/core/AutoCoreTagContext.tsx +247 -330
  55. package/src/core/AutoCoreTagTypes.ts +236 -104
  56. package/src/core/CoreStreamTypes.ts +512 -0
  57. package/src/core/EventEmitterContext.tsx +182 -520
  58. package/src/core/IndicatorButtonState.ts +1 -1
  59. package/src/core/hoc.tsx +1 -1
  60. package/src/hooks/adsHooks.tsx +21 -22
  61. package/src/hooks/commandHooks.tsx +23 -19
  62. package/src/hooks/index.ts +1 -1
  63. package/src/hooks/useAutoCoreTag.ts +2 -2
  64. package/src/hooks/useScaledValue.tsx +1 -1
  65. package/src/hub/CommandMessage.ts +71 -19
  66. package/src/hub/DebugPanel.ts +280 -0
  67. package/src/hub/HubBase.ts +147 -223
  68. package/src/hub/HubSimulate.ts +93 -24
  69. package/src/hub/HubTauri.ts +87 -96
  70. package/src/hub/HubWebSocket.ts +118 -140
  71. package/src/hub/debug.ts +211 -0
  72. package/src/hub/index.ts +49 -39
  73. package/docs/.nojekyll +0 -1
  74. package/docs/assets/hierarchy.js +0 -1
  75. package/docs/assets/highlight.css +0 -134
  76. package/docs/assets/icons.js +0 -18
  77. package/docs/assets/icons.svg +0 -1
  78. package/docs/assets/main.js +0 -60
  79. package/docs/assets/navigation.js +0 -1
  80. package/docs/assets/search.js +0 -1
  81. package/docs/assets/style.css +0 -1633
  82. package/docs/classes/components_CodeEditor.CodeEditor.html +0 -135
  83. package/docs/classes/components_Indicator.Indicator.html +0 -122
  84. package/docs/classes/components_IndicatorRect.IndicatorRect.html +0 -121
  85. package/docs/classes/components_JogPanel.JogPanel.html +0 -136
  86. package/docs/classes/components_Lamp.Lamp.html +0 -122
  87. package/docs/classes/components_OskDialog.OskDialog.html +0 -125
  88. package/docs/classes/components_TextInput.TextInput.html +0 -125
  89. package/docs/classes/components_ValueDisplay.ValueDisplay.html +0 -148
  90. package/docs/classes/components_ValueIndicator.ValueIndicator.html +0 -126
  91. package/docs/classes/core_ValueSimulator.ValueSimulator.html +0 -51
  92. package/docs/classes/hub_HubBase.HubBase.html +0 -106
  93. package/docs/classes/hub_HubSimulate.HubSimulate.html +0 -75
  94. package/docs/classes/hub_HubTauri.HubTauri.html +0 -93
  95. package/docs/classes/hub_HubWebSocket.HubWebSocket.html +0 -112
  96. package/docs/documents/core_AutoCoreTagContext.AutoCoreTagContext.html +0 -148
  97. package/docs/enums/components_JogPanel.JogDistanceAction.html +0 -5
  98. package/docs/enums/components_JogPanel.JogPanelAction.html +0 -18
  99. package/docs/enums/components_JogPanel.JogSpeedAction.html +0 -5
  100. package/docs/enums/core_ActionMode.ActionMode.html +0 -6
  101. package/docs/enums/core_IndicatorColor.IndicatorColor.html +0 -23
  102. package/docs/functions/assets.BlocklyLogo.html +0 -1
  103. package/docs/functions/assets.Distance.html +0 -1
  104. package/docs/functions/assets.JogLong.html +0 -1
  105. package/docs/functions/assets.JogMedium.html +0 -1
  106. package/docs/functions/assets.JogShort.html +0 -1
  107. package/docs/functions/assets.PythonLogo.html +0 -1
  108. package/docs/functions/assets.Rotation3D.html +0 -1
  109. package/docs/functions/assets.RotationCcw.html +0 -1
  110. package/docs/functions/assets.RotationCcwA.html +0 -1
  111. package/docs/functions/assets.RotationCcwB.html +0 -1
  112. package/docs/functions/assets.RotationCcwC.html +0 -1
  113. package/docs/functions/assets.RotationCw.html +0 -1
  114. package/docs/functions/assets.RotationCwA.html +0 -1
  115. package/docs/functions/assets.RotationCwB.html +0 -1
  116. package/docs/functions/assets.RotationCwC.html +0 -1
  117. package/docs/functions/assets.Run.html +0 -1
  118. package/docs/functions/assets.Speed.html +0 -1
  119. package/docs/functions/assets.SpeedFast.html +0 -1
  120. package/docs/functions/assets.SpeedMedium.html +0 -1
  121. package/docs/functions/assets.SpeedNone.html +0 -1
  122. package/docs/functions/assets.SpeedSlow.html +0 -1
  123. package/docs/functions/assets.Walk.html +0 -1
  124. package/docs/functions/components_BlocklyEditor.createCustomToolbox.html +0 -6
  125. package/docs/functions/core_UniqueId.UniqueId.html +0 -9
  126. package/docs/functions/core_hoc.hocAddSubscription.html +0 -6
  127. package/docs/functions/hooks_adsHooks.useAdsRegisterSymbols.html +0 -16
  128. package/docs/functions/hooks_adsHooks.useAdsTapValue.html +0 -8
  129. package/docs/functions/hooks_adsHooks.useAdsWriteScaledValue.html +0 -18
  130. package/docs/functions/hooks_adsHooks.useAdsWriteValue.html +0 -9
  131. package/docs/functions/hooks_commandHooks.useRegisterSymbols.html +0 -16
  132. package/docs/functions/hooks_commandHooks.useTapValue.html +0 -10
  133. package/docs/functions/hooks_commandHooks.useWriteScaledValue.html +0 -18
  134. package/docs/functions/hooks_commandHooks.useWriteValue.html +0 -11
  135. package/docs/functions/hooks_useAutoCoreTag.ts.makeAutoCoreTagHooks.html +0 -12
  136. package/docs/functions/hooks_useScaledValue.useScaledValue.html +0 -18
  137. package/docs/functions/hub.createHub.html +0 -3
  138. package/docs/hierarchy.html +0 -1
  139. package/docs/index.html +0 -148
  140. package/docs/interfaces/components_IndicatorButton.IndicatorButtonProps.html +0 -654
  141. package/docs/interfaces/components_IndicatorRect.IndicatorRectProps.html +0 -37
  142. package/docs/interfaces/components_JogPanel.JogPanelButtonDefinition.html +0 -5
  143. package/docs/interfaces/components_ToggleGroup.ToggleGroupProps.html +0 -644
  144. package/docs/interfaces/core_AutoCoreTagTypes.BaseContextValue.html +0 -12
  145. package/docs/interfaces/core_AutoCoreTagTypes.ScaleConfig.html +0 -13
  146. package/docs/interfaces/core_EventEmitterContext.Action.html +0 -8
  147. package/docs/interfaces/core_EventEmitterContext.EventEmitterContextType.html +0 -33
  148. package/docs/interfaces/core_EventEmitterContext.State.html +0 -8
  149. package/docs/interfaces/core_EventEmitterContext.Subscription.html +0 -6
  150. package/docs/interfaces/core_IndicatorButtonState.IndicatorButtonState.html +0 -10
  151. package/docs/interfaces/core_PositionContext.IPositionContext.html +0 -17
  152. package/docs/interfaces/hub_CommandMessage.CommandMessage.html +0 -6
  153. package/docs/interfaces/hub_CommandMessage.CommandMessageResult.html +0 -4
  154. package/docs/modules/assets.html +0 -1
  155. package/docs/modules/assets_BlocklyLogo.html +0 -1
  156. package/docs/modules/assets_Distance.html +0 -1
  157. package/docs/modules/assets_JogLong.html +0 -1
  158. package/docs/modules/assets_JogMedium.html +0 -1
  159. package/docs/modules/assets_JogShort.html +0 -1
  160. package/docs/modules/assets_PythonLogo.html +0 -1
  161. package/docs/modules/assets_Rotation3D.html +0 -1
  162. package/docs/modules/assets_RotationCcw.html +0 -1
  163. package/docs/modules/assets_RotationCcwA.html +0 -1
  164. package/docs/modules/assets_RotationCcwB.html +0 -1
  165. package/docs/modules/assets_RotationCcwC.html +0 -1
  166. package/docs/modules/assets_RotationCw.html +0 -1
  167. package/docs/modules/assets_RotationCwA.html +0 -1
  168. package/docs/modules/assets_RotationCwB.html +0 -1
  169. package/docs/modules/assets_RotationCwC.html +0 -1
  170. package/docs/modules/assets_Run.html +0 -1
  171. package/docs/modules/assets_Speed.html +0 -1
  172. package/docs/modules/assets_SpeedFast.html +0 -1
  173. package/docs/modules/assets_SpeedMedium.html +0 -1
  174. package/docs/modules/assets_SpeedNone.html +0 -1
  175. package/docs/modules/assets_SpeedSlow.html +0 -1
  176. package/docs/modules/assets_Walk.html +0 -1
  177. package/docs/modules/components_AutoCoreDevPanel.html +0 -20
  178. package/docs/modules/components_BlocklyEditor.html +0 -1
  179. package/docs/modules/components_CodeEditor.html +0 -1
  180. package/docs/modules/components_FileList.html +0 -1
  181. package/docs/modules/components_FileSelect.html +0 -1
  182. package/docs/modules/components_FitText.html +0 -1
  183. package/docs/modules/components_Indicator.html +0 -1
  184. package/docs/modules/components_IndicatorButton.html +0 -1
  185. package/docs/modules/components_IndicatorRect.html +0 -1
  186. package/docs/modules/components_JogPanel.html +0 -1
  187. package/docs/modules/components_Lamp.html +0 -1
  188. package/docs/modules/components_Osk.html +0 -1
  189. package/docs/modules/components_OskDialog.html +0 -1
  190. package/docs/modules/components_ProgressBarWithValue.html +0 -1
  191. package/docs/modules/components_TextInput.html +0 -1
  192. package/docs/modules/components_ToggleGroup.html +0 -1
  193. package/docs/modules/components_ValueDisplay.html +0 -1
  194. package/docs/modules/components_ValueIndicator.html +0 -1
  195. package/docs/modules/components_ValueInput.html +0 -1
  196. package/docs/modules/core_ActionMode.html +0 -1
  197. package/docs/modules/core_AutoCoreTagContext.html +0 -11
  198. package/docs/modules/core_AutoCoreTagTypes.html +0 -1
  199. package/docs/modules/core_EventEmitterContext.html +0 -53
  200. package/docs/modules/core_IndicatorButtonState.html +0 -1
  201. package/docs/modules/core_IndicatorColor.html +0 -1
  202. package/docs/modules/core_MaskPatterns.html +0 -1
  203. package/docs/modules/core_NumerableTypes.html +0 -1
  204. package/docs/modules/core_PositionContext.html +0 -1
  205. package/docs/modules/core_UniqueId.html +0 -1
  206. package/docs/modules/core_ValueSimulator.html +0 -1
  207. package/docs/modules/core_hoc.html +0 -1
  208. package/docs/modules/hooks.html +0 -1
  209. package/docs/modules/hooks_adsHooks.html +0 -1
  210. package/docs/modules/hooks_commandHooks.html +0 -1
  211. package/docs/modules/hooks_useAutoCoreTag.ts.html +0 -52
  212. package/docs/modules/hooks_useScaledValue.html +0 -1
  213. package/docs/modules/hub.html +0 -1
  214. package/docs/modules/hub_CommandMessage.html +0 -1
  215. package/docs/modules/hub_HubBase.html +0 -1
  216. package/docs/modules/hub_HubSimulate.html +0 -1
  217. package/docs/modules/hub_HubTauri.html +0 -1
  218. package/docs/modules/hub_HubWebSocket.html +0 -1
  219. package/docs/modules.html +0 -23
  220. package/docs/types/components_IndicatorButton.IndicatorButtonOptionsType.html +0 -1
  221. package/docs/types/core_AutoCoreTagTypes.ExtractByTag.html +0 -2
  222. package/docs/types/core_AutoCoreTagTypes.PrimitiveKind.html +0 -1
  223. package/docs/types/core_AutoCoreTagTypes.TagConfig.html +0 -16
  224. package/docs/types/core_AutoCoreTagTypes.TagValueMap.html +0 -1
  225. package/docs/types/core_AutoCoreTagTypes.TagValueOf.html +0 -1
  226. package/docs/types/core_EventEmitterContext.EmitterDispatchFunction.html +0 -3
  227. package/docs/types/core_EventEmitterContext.EmitterSubscribeFunction.html +0 -3
  228. package/docs/types/core_EventEmitterContext.EmitterUnsubscribeFunction.html +0 -3
  229. package/docs/types/core_NumerableTypes.NumerableFormatOptions.html +0 -4
  230. package/docs/types/core_hoc.HocAddSubscriptionProps.html +0 -6
  231. package/docs/variables/components_AutoCoreDevPanel.AutoCoreDevPanel.html +0 -43
  232. package/docs/variables/components_BlocklyEditor.BlocklyEditor.html +0 -13
  233. package/docs/variables/components_BlocklyEditor.StandardToolbox.html +0 -1
  234. package/docs/variables/components_FileList.FileList.html +0 -23
  235. package/docs/variables/components_FileSelect.FileSelect.html +0 -1
  236. package/docs/variables/components_FitText.FitText.html +0 -4
  237. package/docs/variables/components_IndicatorButton.IndicatorButton.html +0 -1
  238. package/docs/variables/components_JogPanel.DefaultLinearJogButtons.html +0 -2
  239. package/docs/variables/components_JogPanel.DefaultRotationJogButtons.html +0 -2
  240. package/docs/variables/components_Osk.Osk.html +0 -1
  241. package/docs/variables/components_ProgressBarWithValue.ProgressBarWithValue.html +0 -1
  242. package/docs/variables/components_ToggleGroup.ToggleGroup.html +0 -1
  243. package/docs/variables/components_ValueInput.ValueInput.html +0 -4
  244. package/docs/variables/core_AutoCoreTagContext.AutoCoreTagContext.html +0 -1
  245. package/docs/variables/core_AutoCoreTagContext.AutoCoreTagProvider.html +0 -7
  246. package/docs/variables/core_EventEmitterContext.EventEmitterContext.html +0 -64
  247. package/docs/variables/core_EventEmitterContext.EventEmitterProvider.html +0 -10
  248. package/docs/variables/core_MaskPatterns.PrimeReactMaskPatterns.html +0 -14
  249. package/docs/variables/core_MaskPatterns.RegExMaskPatterns.html +0 -15
  250. package/docs/variables/core_PositionContext.DimensionsContext.html +0 -6
  251. package/docs/variables/hooks_useScaledValue.kMillimeters2Inches.html +0 -2
  252. package/docs/variables/hooks_useScaledValue.kNewtons2Pounds.html +0 -2
@@ -2,7 +2,8 @@
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-29 09:31:42
6
+ * Modified By: ADC
6
7
  * -----
7
8
  *
8
9
  */
@@ -13,99 +14,50 @@
13
14
  * @document ../../additional-docs/AutCoreTagContext.md
14
15
  *
15
16
  * @summary
16
- * A React Context + Provider that:
17
- * - Buffers **raw** controller values for every tag (exactly as received)
18
- * - Derives **display** values from raw using current `scales` and optional tag `codec`
19
- * - Recomputes display values **from raw** whenever a scale changes (no lossy re-scaling)
20
- * - Inverse-scales on `write()` so the server always receives controller units
21
- * - Supports ADS one-shot refresh, GNV-style scale objects, and resilient eager reads
17
+ * A React Context + Provider that manages the state and synchronization of "Tags" (data points)
18
+ * between the React frontend and the AutoCore backend.
19
+ *
20
+ * It acts as the central data store for real-time values, handling:
21
+ * - **Buffering**: Stores raw controller values exactly as received.
22
+ * - **Scaling**: Converts raw values to display values (e.g., mm -> inches) based on configuration.
23
+ * - **Synchronization**: Subscribes to live updates from the server.
24
+ * - **Writing**: Converts display values back to raw controller units before sending.
22
25
  *
23
26
  * @remarks
24
- * ❖ **Two representations per tag**
25
- * - `rawValues[tagName]`: last controller value as-is (pre-scale, pre-codec)
26
- * - `values[tagName]`: app-visible value derived from raw (scales/codec applied)
27
- *
28
- * ❖ **Scaling**
29
- * - Provide `scales` keyed by scale group (e.g., `"position"`, `"load"`).
30
- * - Each scale may point at a server tag (`serverTag`) that publishes an object
31
- * `{ name, scale, label }` (GNV-style). When received, the Provider recomputes
32
- * *only* affected display values **from raw**.
33
- * - Manual `updateScale(name, scale, label)` writes the object back to the server
34
- * (if `serverTag` is configured) and recomputes from raw locally.
35
- *
36
- * ❖ **Lifecycle order (eagerRead = true)**
37
- * 1. Pull server scales first (so first ADS burst displays in correct units)
38
- * 2. Register ADS symbols and subscribe all domains
39
- * 3. Eager-pull non-ADS (refresh if available, else read_value)
40
- * 4. ADS `refresh` one-shot publish
41
- *
42
- * ❖ **Writes**
43
- * - `write(tagName, displayValue)` → (codec?) → inverse-scale → `write_value`
44
- * - `tap(tagName)` pulses booleans `true → (300ms) → false` with proper domain envelopes
27
+ * ❖ **Dual-State Representation**
28
+ * To prevent rounding errors and race conditions, we maintain two parallel states:
29
+ * 1. `rawValues[tagName]`: The exact value received from the controller (Source of Truth).
30
+ * 2. `values[tagName]`: The derived value shown in the UI (calculated from raw + scale).
31
+ *
32
+ * When a scale changes (e.g., user switches units), we recompute `values` from `rawValues`.
33
+ * We *never* rescale a previously scaled value.
45
34
  *
46
- * ❖ **Resilience**
47
- * - Eager reads tolerate “missing key” errors on fresh systems and keep going
48
- * - All re-scaling recomputes from **raw** to avoid cumulative precision/race issues
35
+ * ❖ **Key Features**
36
+ * - **Resilient Initialization**: Eagerly fetches initial values (via ADS refresh or read fallback).
37
+ * - **Server-Stored Scales**: Can load unit preferences from the server (GNV domain).
38
+ * - **Optimized Updates**: Uses stable references and memoization to minimize React re-renders.
39
+ * - **Type Safety**: Strongly typed context via generic `VMapRuntime`.
49
40
  *
50
41
  * @example Minimal wiring
51
42
  * ```tsx
52
- * import { AutoCoreTagProvider } from "@adcops/autocore-react/core/AutoCoreTagContext";
53
- * import type { TagConfig, ScaleConfig } from "@adcops/autocore-react/core/AutoCoreTagTypes";
54
- *
55
- * const acTagSpec = [
56
- * { tagName: "pressPosition", domain: "ADS", valueType: "number", symbolName: "AX.Press.Position", scale: "position" },
57
- * { tagName: "pressLoad", domain: "ADS", valueType: "number", symbolName: "AX.Press.Load", scale: "load" },
58
- * { tagName: "isMotorsOn", domain: "ADS", valueType: "boolean", symbolName: "GIO.isMotorsOn" },
59
- * ] as const satisfies readonly TagConfig[];
60
- *
61
- * const scales: Record<string, ScaleConfig> = {
62
- * position: {
63
- * name: "position",
64
- * scale: 1.0,
65
- * label: "mm",
66
- * serverTag: { domain: "GNV", symbolName: "position_units" }, // expects {name, scale, label}
67
- * },
68
- * load: {
69
- * name: "load",
70
- * scale: 1.0,
71
- * label: "N",
72
- * serverTag: { domain: "GNV", symbolName: "load_units" }, // expects {name, scale, label}
73
- * },
43
+ * // 1. Define your tags
44
+ * const tags = [
45
+ * { tagName: "position", fqdn: "ADS.Axis.Pos", valueType: "number", scale: "dist" },
46
+ * { tagName: "speed", fqdn: "ADS.Axis.Vel", valueType: "number", scale: "dist" }
47
+ * ];
48
+ *
49
+ * // 2. Define scales
50
+ * const scales = {
51
+ * dist: { name: "dist", scale: 1.0, label: "mm" }
74
52
  * };
75
- *
76
- * export function AppRoot() {
77
- * return (
78
- * <AutoCoreTagProvider tags={acTagSpec} scales={scales} eagerRead>
79
- * <App />
80
- * </AutoCoreTagProvider>
81
- * );
82
- * }
83
- * ```
84
- *
85
- * @example Consuming values with hooks
86
- * ```tsx
87
- * import { makeAutoCoreTagHooks } from "@adcops/autocore-react/hooks/useAutoCoreTag";
88
- * import { AutoCoreTagContext } from "@adcops/autocore-react/core/AutoCoreTagContext";
89
- *
90
- * export const AutoCoreHooks = makeAutoCoreTagHooks(AutoCoreTagContext, acTagSpec);
91
- *
92
- * function Dashboard() {
93
- * const { value: pos } = AutoCoreHooks.useAutoCoreTag("pressPosition"); // display units
94
- * const { value: load } = AutoCoreHooks.useAutoCoreTag("pressLoad");
95
- * const { scales, updateScale } = AutoCoreHooks.useScales();
96
- *
97
- * return (
98
- * <>
99
- * <div>Position: {pos} {scales.position.label}</div>
100
- * <div>Load: {load} {scales.load.label}</div>
101
- * <button onClick={() => updateScale("position", 1/25.4, "in")}>inches</button>
102
- * </>
103
- * );
104
- * }
53
+ *
54
+ * // 3. Wrap application
55
+ * <AutoCoreTagProvider tags={tags} scales={scales}>
56
+ * <App />
57
+ * </AutoCoreTagProvider>
105
58
  * ```
106
59
  */
107
60
 
108
-
109
61
  import React, {
110
62
  useRef,
111
63
  createContext,
@@ -120,11 +72,32 @@ import { EventEmitterContext } from "./EventEmitterContext";
120
72
  import type {
121
73
  BaseContextValue,
122
74
  TagConfig,
123
- ScaleConfig,
75
+ ScaleConfig
124
76
  } from "./AutoCoreTagTypes";
125
77
 
78
+ import { MessageType } from "../hub/CommandMessage";
79
+
80
+ /**
81
+ * Runtime type for the values map - allows any tag name to map to any value type.
82
+ * This is the generic type used when the specific tag configuration is not known at compile time.
83
+ */
126
84
  type VMapRuntime = Record<string, unknown>;
127
85
 
86
+ /**
87
+ * The React Context that holds all AutoCore tag state and operations.
88
+ *
89
+ * This context provides:
90
+ * - `values`: Display values (with scaling/codecs applied)
91
+ * - `rawValues`: Raw controller values as received from server
92
+ * - `isLoading`: Whether initial data loading is complete
93
+ * - `write`: Function to write a value to a tag
94
+ * - `tap`: Function to pulse a boolean tag (true → delay → false)
95
+ * - `scales`: Current scale configurations
96
+ * - `updateScale`: Function to change a scale factor/label
97
+ *
98
+ * @see AutoCoreTagProvider for the provider implementation
99
+ * @see makeAutoCoreTagHooks for creating typed hooks from this context
100
+ */
128
101
  export const AutoCoreTagContext = createContext<BaseContextValue<VMapRuntime>>({
129
102
  values: {},
130
103
  rawValues: {},
@@ -135,11 +108,33 @@ export const AutoCoreTagContext = createContext<BaseContextValue<VMapRuntime>>({
135
108
  updateScale: async () => { },
136
109
  });
137
110
 
111
+ /**
112
+ * Standard response envelope from the AutoCore server.
113
+ * All server responses follow this structure.
114
+ *
115
+ * @template T - The type of data contained in the response
116
+ * @property success - Whether the operation succeeded
117
+ * @property valid - Whether the response data is valid
118
+ * @property data - The actual response payload
119
+ */
138
120
  type HubEnvelope<T> = { success?: boolean; valid?: boolean; data?: T };
139
121
 
140
- /** Small sleep helper */
122
+ /**
123
+ * Utility function to create a promise that resolves after a specified delay.
124
+ * Used for tap() pulse timing and retry delays.
125
+ *
126
+ * @param ms - Number of milliseconds to sleep
127
+ * @returns Promise that resolves after the delay
128
+ */
141
129
  const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
142
130
 
131
+ /**
132
+ * Extracts the domain (first segment) from a Fully-Qualified Domain Name.
133
+ * @param fqdn - The full topic path (e.g., "ads.plc1.gio.bControlPowerOk")
134
+ * @returns The domain segment (e.g., "ads")
135
+ */
136
+
137
+
143
138
 
144
139
  /**
145
140
  * AutoCoreTagProvider
@@ -150,124 +145,33 @@ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
150
145
  * writes/taps with proper domain envelopes. Scaling is **always** applied to display
151
146
  * values only; raw values remain untouched and are the single source of truth.
152
147
  *
153
- * @props
154
- * - `tags: readonly TagConfig[]`
155
- * The full tag spec. Each tag declares `tagName`, `domain`, `symbolName`,
156
- * `valueType` ("boolean"|"number"|"string"|"json"), optional `scale` name, and
157
- * optional `codec` for JSON.
158
- *
159
- * - `scales?: Record<string, ScaleConfig>`
160
- * Map of scale groups (`position`, `load`, …). Each scale may include an optional
161
- * `serverTag` `{ domain, symbolName }`. When present, the provider reads that key
162
- * first and subscribes to updates. The server must publish a GNV-style object
163
- * `{ name, scale, label }` (stringified or object). When a new scale arrives,
164
- * the provider recomputes affected display values **from raw**.
165
- *
166
- * - `eagerRead?: boolean = true`
167
- * If true, the provider performs a non-ADS eager fetch pass (refresh if available,
168
- * otherwise read_value with backoff) and then performs a single ADS `refresh`.
169
- * Missing keys are common on clean systems—these are logged and skipped.
170
- *
171
- * - `children: ReactNode`
172
- * Your app subtree that consumes `AutoCoreTagContext`.
173
- *
174
- * @context value
175
- * - `values`: Partial<Record<tagName, unknown>> — app-visible (scaled/decoded)
176
- * - `rawValues`: Record<tagName, unknown> — last controller values as received
177
- * - `isLoading`: boolean — true until initial wiring completes
178
- * - `write(tagName, displayValue)`: Promise<void>
179
- * Serializes/encodes JSON if needed and inverse-scales numbers back to raw units.
180
- * Dispatches `write_value` with appropriate domain envelope:
181
- * - ADS: `{ symbol_name, value }`
182
- * - GNV: `{ group:"ux", key, value }`
183
- * - Other: `{ key, value }`
184
- * - `tap(tagName)`: Promise<void>
185
- * For boolean tags only. Pulses true → 300ms → false with the proper envelope.
186
- * - `scales`: Record<string, ScaleConfig> — current factors/labels
187
- * - `updateScale(name, scale, label)`: Promise<void>
188
- * Writes `{ name, scale, label }` to the configured `serverTag` (if present) and
189
- * recomputes display values **from raw** locally.
190
- *
191
- * @example With server-driven scales (GNV) and ADS data
192
- * ```tsx
193
- * const tags: readonly TagConfig[] = [
194
- * { tagName: "pressPosition", domain: "ADS", valueType: "number", symbolName: "AX.Press.Pos", scale: "position" },
195
- * { tagName: "pressLoad", domain: "ADS", valueType: "number", symbolName: "AX.Press.Load", scale: "load" },
196
- * { tagName: "isMotorsOn", domain: "ADS", valueType: "boolean", symbolName: "GIO.isMotorsOn" },
197
- * // JSON with codec example
198
- * { tagName: "jobMeta", domain: "GNV", valueType: "json", symbolName: "job_meta",
199
- * codec: {
200
- * fromServer: (raw) => (typeof raw === "string" ? JSON.parse(raw) : raw),
201
- * toServer: (val) => JSON.stringify(val),
202
- * }
203
- * },
204
- * ] as const;
205
- *
206
- * const scales: Record<string, ScaleConfig> = {
207
- * position: {
208
- * name: "position",
209
- * scale: 1, label: "mm",
210
- * serverTag: { domain: "GNV", symbolName: "position_units" } // expects {name, scale, label}
211
- * },
212
- * load: {
213
- * name: "load",
214
- * scale: 1, label: "N",
215
- * serverTag: { domain: "GNV", symbolName: "load_units" } // expects {name, scale, label}
216
- * }
217
- * };
218
- *
219
- * <AutoCoreTagProvider tags={tags} scales={scales} eagerRead>
220
- * <YourApp/>
221
- * </AutoCoreTagProvider>
222
- * ```
223
- *
224
- * @example Reading values, raw vs display, and writing
225
- * ```tsx
226
- * import { makeAutoCoreTagHooks } from "../hooks/useAutoCoreTag";
227
- * import { AutoCoreTagContext } from "../core/AutoCoreTagContext";
228
- *
229
- * const Hooks = makeAutoCoreTagHooks(AutoCoreTagContext, tags);
230
- *
231
- * function Status() {
232
- * const { value: pos, isLoading } = Hooks.useAutoCoreTag("pressPosition"); // scaled
233
- * const { value: rawPos } = (() => {
234
- * // access raw via context when needed for debugging
235
- * const ctx = React.useContext(AutoCoreTagContext);
236
- * return { value: ctx.rawValues["pressPosition"] };
237
- * })();
238
- *
239
- * const { scales, updateScale } = Hooks.useScales();
240
- *
241
- * return (
242
- * <>
243
- * <div>Pos: {pos} {scales.position.label} (raw: {String(rawPos)})</div>
244
- * <button onClick={() => updateScale("position", 1/25.4, "in")}>inches</button>
245
- * </>
246
- * );
247
- * }
248
- * ```
249
- *
250
- * @example Tapping booleans and writing numbers in display units
251
- * ```tsx
252
- * const { tap } = Hooks.useAutoCoreTag("isMotorsOn");
253
- * const { write } = Hooks.useAutoCoreTag("pressPosition");
254
- *
255
- * <button onClick={() => tap()}>Motors TAP</button>
256
- * <button onClick={() => write(10.0)}>Jog to 10 (display units)</button>
257
- * ```
148
+ * @param props.tags - List of tag configurations (tagName, fqdn, etc.)
149
+ * @param props.scales - Map of scale definitions (name, factor, label)
150
+ * @param props.eagerRead - If true, automatically fetches initial values on mount
258
151
  */
259
152
  export const AutoCoreTagProvider: React.FC<{
260
153
  children: ReactNode;
261
154
  tags: readonly TagConfig[];
262
155
  scales?: Record<string, ScaleConfig>;
263
156
  eagerRead?: boolean;
264
- }> = ({ children, tags, scales = {}, eagerRead = true }) => {
157
+ }> = ({ children, tags, scales, eagerRead = true }) => {
265
158
  const startedRef = useRef(false);
266
159
 
267
- const { invoke, isConnected, subscribe, unsubscribe } =
160
+ // PERFORMANCE: Memoize default scales to ensure reference stability.
161
+ // If 'scales' prop is undefined, we use this stable empty object.
162
+ // This prevents infinite re-render loops caused by creating a new object on every render.
163
+ const defaultScales = useMemo<Record<string, ScaleConfig>>(() => ({}), []);
164
+ const actualScales: Record<string, ScaleConfig> = scales ?? defaultScales;
165
+
166
+ const { invoke, read, write: hubWrite, serverSubscribe, isConnected, subscribe, unsubscribe } =
268
167
  useContext(EventEmitterContext);
269
168
 
270
- /** Raw (controller) values as received from the server (pre-scale, pre-codec). */
169
+ /**
170
+ * Raw (controller) values as received from the server (pre-scale, pre-codec).
171
+ * This is the single source of truth for value data.
172
+ * Seeded with `initialValue` from tag config if available.
173
+ * Keyed by `tagName`.
174
+ */
271
175
  const [rawValues, setRawValues] = useState<Record<string, unknown>>(() => {
272
176
  const seed: Record<string, unknown> = {};
273
177
  for (const t of tags) {
@@ -279,14 +183,24 @@ export const AutoCoreTagProvider: React.FC<{
279
183
  return seed;
280
184
  });
281
185
 
282
- /** App-visible display values, computed from raw using scales/codecs. */
186
+ /**
187
+ * App-visible display values, computed from raw using scales/codecs.
188
+ * This is what the UI consumes.
189
+ * Keyed by `tagName`.
190
+ */
283
191
  const [values, setValues] = useState<Record<string, unknown>>({});
284
192
 
285
- /** Scales (current factor/label) */
193
+ /**
194
+ * Current state of scales (factor/label).
195
+ * Can be updated by the server or user interaction.
196
+ */
286
197
  const [scaleValues, setScaleValues] =
287
- useState<Record<string, ScaleConfig>>(scales);
198
+ useState<Record<string, ScaleConfig>>(actualScales);
288
199
 
289
- /** Keep refs for subscription callbacks without re-subscribing */
200
+ /**
201
+ * Refs to access latest state inside callbacks without adding them to dependency arrays.
202
+ * This helps prevent re-subscribing when state changes.
203
+ */
290
204
  const scaleRef = useRef(scaleValues);
291
205
  const rawRef = useRef(rawValues);
292
206
  useEffect(() => { scaleRef.current = scaleValues; }, [scaleValues]);
@@ -294,36 +208,44 @@ export const AutoCoreTagProvider: React.FC<{
294
208
 
295
209
  const [isLoading, setIsLoading] = useState(true);
296
210
 
297
- /** Produce a display value from a raw value given the tag config + current scales. */
211
+ /**
212
+ * Converts a raw controller value to a display value.
213
+ *
214
+ * Pipeline: Raw -> [Codec Decode] -> [Scale Multiply] -> Display
215
+ */
298
216
  const toDisplay = useCallback((tag: TagConfig, raw: unknown): unknown => {
299
217
  const { valueType, scale, codec } = tag;
300
218
 
301
- // 1) numeric scaling
219
+ // 1) numeric scaling: multiply raw by scale factor
302
220
  if (valueType === "number" && typeof raw === "number" && scale) {
303
221
  const s = scaleRef.current[scale];
304
222
  const factor = s?.scale ?? 1;
305
223
  return raw * factor;
306
224
  }
307
225
 
308
- // 2) codec for json (optional)
226
+ // 2) codec for json (optional): decode server representation
309
227
  if (valueType === "json" && codec?.fromServer) {
310
228
  try { return codec.fromServer(raw); } catch { /* fall through */ }
311
229
  }
312
230
 
313
- // 3) pass-through
231
+ // 3) pass-through: no transformation needed
314
232
  return raw;
315
233
  }, []);
316
234
 
317
- /** Inverse of toDisplay for writes (display -> raw server value) */
235
+ /**
236
+ * Converts a display value back to a raw controller value.
237
+ *
238
+ * Pipeline: Display -> [Scale Divide] -> [Codec Encode] -> Raw
239
+ */
318
240
  const toServer = useCallback((tag: TagConfig, display: unknown): unknown => {
319
241
  const { valueType, scale, codec } = tag;
320
242
 
321
- // invert codec first (json)
243
+ // 1) invert codec first (json): encode for server
322
244
  if (valueType === "json" && codec?.toServer) {
323
245
  try { display = codec.toServer(display as any); } catch { /* fall through */ }
324
246
  }
325
247
 
326
- // inverse numeric scaling
248
+ // 2) inverse numeric scaling: divide by scale factor
327
249
  if (valueType === "number" && typeof display === "number" && scale) {
328
250
  const s = scaleRef.current[scale];
329
251
  const factor = s?.scale ?? 1;
@@ -333,7 +255,13 @@ export const AutoCoreTagProvider: React.FC<{
333
255
  return display;
334
256
  }, []);
335
257
 
336
- /** Recompute display values for all tags in this scale from **raw** */
258
+ /**
259
+ * Recomputes display values for all tags in a specific scale group.
260
+ * Called when a scale factor changes.
261
+ *
262
+ * CRITICAL: Computes from `rawValues` (Source of Truth), not existing `values`.
263
+ * This prevents floating point error accumulation from repeated scaling.
264
+ */
337
265
  const rescaleFromRaw = useCallback((scaleName: string) => {
338
266
  const affected = tags.filter(t => t.scale === scaleName);
339
267
  if (!affected.length) return;
@@ -350,20 +278,30 @@ export const AutoCoreTagProvider: React.FC<{
350
278
  });
351
279
  }, [tags, toDisplay]);
352
280
 
353
- /** Subscribe handler for tag updates: buffer raw, compute display. */
281
+ /**
282
+ * Handles incoming value updates from the server.
283
+ *
284
+ * 1. Updates `rawValues` (Source of Truth).
285
+ * 2. Computes and updates `values` (Display).
286
+ * 3. Uses functional state updates to ensure atomicity.
287
+ * 4. Keys by `tagName` for consistent access by hooks.
288
+ */
354
289
  const handleTagUpdate = useCallback((tag: TagConfig, raw: unknown) => {
355
- // write raw (immutably)
290
+ // Store the raw controller value (source of truth for scaling)
356
291
  setRawValues(prev =>
357
292
  prev[tag.tagName] === raw ? prev : { ...prev, [tag.tagName]: raw }
358
293
  );
359
- // compute display from raw
294
+ // Compute and store the display value (with scaling/codecs applied)
360
295
  const display = toDisplay(tag, raw);
361
296
  setValues(prev =>
362
297
  prev[tag.tagName] === display ? prev : { ...prev, [tag.tagName]: display }
363
298
  );
364
299
  }, [toDisplay]);
365
300
 
366
- /** Eager pulls (non-ADS): read raw and run handleTagUpdate */
301
+ /**
302
+ * Eagerly fetches initial values for non-ADS domains (e.g., MODBUS).
303
+ * Uses a worker pool to fetch in parallel while respecting rate limits.
304
+ */
367
305
  const eagerPullNonADS = useCallback(
368
306
  async (
369
307
  domain: string,
@@ -375,6 +313,7 @@ export const AutoCoreTagProvider: React.FC<{
375
313
  const jitterMs = opts?.jitterMs ?? 40;
376
314
 
377
315
  let index = 0;
316
+
378
317
  const workers = Array.from({ length: concurrency }, () => (async () => {
379
318
  while (true) {
380
319
  const i = index++;
@@ -382,34 +321,24 @@ export const AutoCoreTagProvider: React.FC<{
382
321
  const tag = tagList[i];
383
322
 
384
323
  try {
324
+ // Try 'refresh' first (server broadcast), else 'read_value'
385
325
  let usedPublishPath = false;
386
326
  try {
387
- await invoke(domain, "refresh", { topic: tag.symbolName });
327
+ await invoke(tag.fqdn, MessageType.Request, { action: "refresh" });
388
328
  usedPublishPath = true;
389
329
  } catch { /* fall through */ }
390
330
 
391
331
  if (!usedPublishPath) {
392
- const payload =
393
- domain.toUpperCase() === "GNV"
394
- ? { group: "ux", key: tag.symbolName }
395
- : { key: tag.symbolName };
396
-
397
- if (tag.options) (payload as any).options = tag.options;
398
-
332
+ const payload = domain.toUpperCase() === "GNV" ? { group: "ux" } : {};
399
333
  try {
400
- const resp: any = await invoke(domain, "read_value", payload);
401
- // Server always returns { success, valid, data }
402
- if (resp?.success && resp?.valid) {
334
+ const resp: any = await read(tag.fqdn, payload);
335
+ if (resp?.success) {
403
336
  handleTagUpdate(tag, resp.data);
404
337
  }
405
- } catch (err) {
406
- // Missing key on fresh systems is normal; keep going.
407
- }
338
+ } catch (err) { /* ignore missing keys on init */ }
408
339
  }
409
- } catch (outerErr) {
410
- // never throw out of the worker loop
411
- // console.warn(`eager pull worker error for ${domain}/${tag.symbolName}`, outerErr);
412
- } finally {
340
+ } catch (outerErr) { /* ignore */ }
341
+ finally {
413
342
  const extra = Math.floor(Math.random() * jitterMs);
414
343
  await sleep(minDelayMs + extra);
415
344
  }
@@ -418,25 +347,24 @@ export const AutoCoreTagProvider: React.FC<{
418
347
 
419
348
  await Promise.all(workers);
420
349
  },
421
- [invoke, handleTagUpdate]
350
+ [invoke, read, handleTagUpdate]
422
351
  );
423
352
 
424
- /** Pull server scale objects first thing (GNV envelope) */
353
+ /**
354
+ * Reads scale configurations from the server (if configured).
355
+ * Ensures display units match the server's persisted configuration.
356
+ */
425
357
  const pullServerScales = useCallback(async () => {
426
- for (const [scaleName, cfg] of Object.entries(scales)) {
358
+ // Use actualScales to ensure we iterate over a stable object
359
+ for (const [scaleName, cfg] of Object.entries(actualScales)) {
427
360
  if (!cfg.serverTag) continue;
428
361
  const { domain, symbolName } = cfg.serverTag;
429
362
 
430
363
  try {
431
- const respUnknown = await invoke(domain, "read_value", {
432
- group: "ux",
433
- key: symbolName,
434
- });
435
-
436
- // narrow at runtime instead of forcing a direct cast
364
+ const respUnknown = await read(`${domain}.${symbolName}`, { group: "ux" });
437
365
  const env = respUnknown as HubEnvelope<string>;
366
+
438
367
  if (env.data && (env.success ?? true) && (env.valid ?? true)) {
439
-
440
368
  const env_data = JSON.parse(env.data) as ScaleConfig;
441
369
  if (env_data && typeof env_data.scale === "number" ) {
442
370
  const { scale, label } = env_data;
@@ -451,68 +379,57 @@ export const AutoCoreTagProvider: React.FC<{
451
379
  rescaleFromRaw(scaleName);
452
380
  }
453
381
  }
454
- } catch {
455
- // Ok if not present yet.
456
- }
382
+ } catch { /* Scale not present, use default */ }
457
383
  }
458
- }, [invoke, scales, rescaleFromRaw]);
459
-
460
- // Main subscription wiring
384
+ }, [read, actualScales, rescaleFromRaw]);
385
+
386
+ /**
387
+ * ==========================================================================
388
+ * MAIN SUBSCRIPTION WIRING EFFECT
389
+ * ==========================================================================
390
+ * Orchestrates the initialization sequence:
391
+ * 1. Load server scales.
392
+ * 2. Subscribe to tags (Server & Local).
393
+ * 3. Perform one-shot reads (ADS refresh).
394
+ */
461
395
  useEffect(() => {
462
396
  let mounted = true;
463
397
  const subscriptions: number[] = [];
464
398
 
465
399
  const registerAndSubscribe = async () => {
466
- // group tags by domain
467
- const tagsByDomain = new Map<string, TagConfig[]>();
468
- for (const tag of tags) {
469
- const list = tagsByDomain.get(tag.domain);
470
- if (list) list.push(tag); else tagsByDomain.set(tag.domain, [tag]);
471
- }
472
-
473
400
  try {
474
- // 0) load scales first so first ADS values get correct factor
401
+ // 1. Load scales first so initial values are correct
475
402
  await pullServerScales();
476
403
 
477
- // 1) ADS register (sequential is fine)
478
- for (const [domain, tagList] of tagsByDomain.entries()) {
479
- if (domain.toUpperCase() === "ADS") {
480
- for (const tag of tagList) {
481
- try {
482
- const invokeOptions = tag.options
483
- ? { symbol_name: tag.symbolName, options: tag.options }
484
- : { symbol_name: tag.symbolName };
485
- await invoke("ADS", "register_symbol", invokeOptions);
486
- } catch (ex) {
487
- console.error(`Failed to register symbol ${tag.symbolName}: ${ex}`);
488
- }
489
- }
490
- }
491
-
492
- // 2) subscribe
493
- for (const tag of tagList) {
494
- const id = subscribe(`${domain}/${tag.symbolName}`, (data) => {
495
- if (!mounted) return;
496
- handleTagUpdate(tag, data?.value);
497
- });
498
- subscriptions.push(id);
499
- }
500
-
501
- // 3) eager reads (non-ADS)
502
- if (eagerRead && domain.toUpperCase() !== "ADS") {
503
- await eagerPullNonADS(domain, tagList, { concurrency: 4, minDelayMs: 20, jitterMs: 40 });
504
- }
404
+ // 2. Subscribe to all tags
405
+ for (const tag of tags) {
406
+ console.log(`Subscribe on [${tag.tagName}] ${tag.fqdn} ...`);
407
+
408
+ // A. Server Subscription: Tell backend to start sending updates.
409
+ // We pass tagName so the Hub can map the FQDN back to the local name.
410
+ await serverSubscribe(tag.tagName, tag.fqdn, tag.subscriptionOptions);
411
+
412
+ // B. Local Subscription: Listen for updates on the Event Bus.
413
+ // The Hub will receive FQDN messages, map them to tagName, and dispatch.
414
+ const id = subscribe(tag.tagName, (data) => {
415
+ if (!mounted) return;
416
+ // data is the raw value payload directly
417
+ handleTagUpdate(tag, data);
418
+ });
419
+ subscriptions.push(id);
505
420
  }
506
421
 
507
- // 4) ADS one-shot refresh (after scales are known)
508
- if (eagerRead) {
509
- await invoke("ADS", "refresh", {});
510
- }
422
+ // // 3. ADS Refresh: Ask ADS to broadcast current values immediately.
423
+ // if (eagerRead) {
424
+ // await invoke("ADS.refresh", MessageType.Request, {});
425
+ // }
426
+
511
427
  } finally {
512
428
  if (mounted) setTimeout(() => mounted && setIsLoading(false), 100);
513
429
  }
514
430
  };
515
431
 
432
+ // Prevents double-execution in React Strict Mode
516
433
  const safeRegister = async () => {
517
434
  if (!mounted || startedRef.current) return;
518
435
  startedRef.current = true;
@@ -520,6 +437,7 @@ export const AutoCoreTagProvider: React.FC<{
520
437
  finally { if (mounted) setTimeout(() => mounted && setIsLoading(false), 100); }
521
438
  };
522
439
 
440
+ // Wait for connection before registering
523
441
  if (!isConnected()) {
524
442
  const id = subscribe("HUB/connected", () => {
525
443
  unsubscribe(id);
@@ -535,29 +453,36 @@ export const AutoCoreTagProvider: React.FC<{
535
453
  subscriptions.forEach(unsubscribe);
536
454
  startedRef.current = false;
537
455
  };
538
- }, [subscribe, unsubscribe, isConnected, invoke, eagerRead, tags, pullServerScales, eagerPullNonADS, handleTagUpdate]);
456
+ }, [subscribe, unsubscribe, isConnected, invoke, serverSubscribe, eagerRead, tags, pullServerScales, eagerPullNonADS, handleTagUpdate]);
539
457
 
540
- /** Subscribe to server-driven scale tags (GNV object), recompute displays from raw. */
458
+ /**
459
+ * Memoized list of scales that need server subscriptions.
460
+ * Used for the separate effect that listens for live scale changes.
461
+ */
541
462
  const scaleServerSubs = useMemo(
542
463
  () =>
543
- Object.entries(scales)
464
+ Object.entries(actualScales)
544
465
  .filter(([, cfg]) => cfg.serverTag)
545
466
  .map(([scaleName, cfg]) => ({
546
467
  scaleName,
547
468
  domain: cfg.serverTag!.domain,
548
469
  symbolName: cfg.serverTag!.symbolName,
549
470
  })),
550
- [scales]
471
+ [actualScales]
551
472
  );
552
473
 
474
+ /**
475
+ * Effect to listen for live scale changes from the server.
476
+ * If another user changes units, this updates our local state immediately.
477
+ */
553
478
  useEffect(() => {
554
479
  let mounted = true;
555
480
  const subs: number[] = [];
556
481
 
557
482
  for (const { scaleName, domain, symbolName } of scaleServerSubs) {
558
- const id = subscribe(`${domain}/${symbolName}`, (data) => {
483
+ const id = subscribe(`${domain}.${symbolName}`, (data) => {
559
484
  if (!mounted) return;
560
- const v = data?.value;
485
+ const v = data?.value; // Scales come as { value: { scale, label } } usually
561
486
  if (v && typeof v === "object" && typeof (v as any).scale === "number") {
562
487
  const { scale, label } = v as { scale: number; label?: string };
563
488
  setScaleValues(prev => ({
@@ -580,7 +505,12 @@ export const AutoCoreTagProvider: React.FC<{
580
505
  };
581
506
  }, [subscribe, unsubscribe, scaleServerSubs, rescaleFromRaw]);
582
507
 
583
- /** Write with inverse scaling and proper domain envelopes. */
508
+ /**
509
+ * Writes a value to the server.
510
+ * 1. Finds tag config by `tagName`.
511
+ * 2. Inverse-scales the value (Display -> Raw).
512
+ * 3. Sends update to server using `fqdn`.
513
+ */
584
514
  const write = useCallback(
585
515
  async (tagName: string, displayValue: unknown) => {
586
516
  const cfg = tags.find((t) => t.tagName === tagName);
@@ -590,23 +520,14 @@ export const AutoCoreTagProvider: React.FC<{
590
520
  }
591
521
 
592
522
  const serverValue = toServer(cfg, displayValue);
593
-
594
- let payload: any;
595
- const dom = cfg.domain.toUpperCase();
596
- if (dom === "ADS") {
597
- payload = { symbol_name: cfg.symbolName, value: serverValue };
598
- } else if (dom === "GNV") {
599
- payload = { group: "ux", key: cfg.symbolName, value: serverValue };
600
- } else {
601
- payload = { key: cfg.symbolName, value: serverValue };
602
- }
603
-
604
- await invoke(cfg.domain, "write_value", payload);
523
+ await hubWrite(cfg.fqdn, serverValue);
605
524
  },
606
- [tags, invoke, toServer]
525
+ [tags, hubWrite, toServer]
607
526
  );
608
527
 
609
- /** Tap (boolean pulse) */
528
+ /**
529
+ * Momentary pulse for boolean tags (True -> Wait 300ms -> False).
530
+ */
610
531
  const tap = useCallback(
611
532
  async (tagName: string) => {
612
533
  const cfg = tags.find((t) => t.tagName === tagName);
@@ -619,22 +540,16 @@ export const AutoCoreTagProvider: React.FC<{
619
540
  return;
620
541
  }
621
542
 
622
- const dom = cfg.domain.toUpperCase();
623
- const mkPayload = (val: boolean) =>
624
- dom === "ADS"
625
- ? { symbol_name: cfg.symbolName, value: val }
626
- : dom === "GNV"
627
- ? { group: "ux", key: cfg.symbolName, value: val }
628
- : { key: cfg.symbolName, value: val };
629
-
630
- await invoke(cfg.domain, "write_value", mkPayload(true));
543
+ await hubWrite(cfg.fqdn, true);
631
544
  await sleep(300);
632
- await invoke(cfg.domain, "write_value", mkPayload(false));
545
+ await hubWrite(cfg.fqdn, false);
633
546
  },
634
- [tags, invoke]
547
+ [tags, hubWrite]
635
548
  );
636
549
 
637
- /** Manual scale update (also writes to server if `serverTag` is present) */
550
+ /**
551
+ * Updates a scale factor locally and optionally persists to server.
552
+ */
638
553
  const updateScale = useCallback(
639
554
  async (scaleName: string, newScale: number, newLabel: string) => {
640
555
  const cfg = scaleValues[scaleName];
@@ -643,28 +558,26 @@ export const AutoCoreTagProvider: React.FC<{
643
558
  return;
644
559
  }
645
560
 
646
- // If scale has a server tag, write to server first (GNV-style)
647
561
  if (cfg.serverTag) {
648
- await invoke(cfg.serverTag.domain, "write_value", {
649
- group: "ux",
650
- key: cfg.serverTag.symbolName,
651
- value: { name: scaleName, scale: newScale, label: newLabel },
652
- });
562
+ const topic = `${cfg.serverTag.domain}.${cfg.serverTag.symbolName}`;
563
+ await hubWrite(topic, { name: scaleName, scale: newScale, label: newLabel });
653
564
  }
654
565
 
655
- // Update local scale + recompute displays from raw
656
566
  setScaleValues(prev => ({
657
567
  ...prev,
658
568
  [scaleName]: { ...prev[scaleName], scale: newScale, label: newLabel },
659
569
  }));
570
+
660
571
  rescaleFromRaw(scaleName);
661
572
  },
662
- [scaleValues, invoke, rescaleFromRaw]
573
+ [scaleValues, hubWrite, rescaleFromRaw]
663
574
  );
664
575
 
665
- /** Derive initial display values from any seeded raw values (once scales are known) */
576
+ /**
577
+ * Safety net: Recomputes all display values whenever scales or tags change.
578
+ * Ensures UI consistency even if individual updates were missed.
579
+ */
666
580
  useEffect(() => {
667
- // On mount or when scales change, recompute every numeric tag from raw
668
581
  setValues(prev => {
669
582
  const next = { ...prev };
670
583
  for (const tag of tags) {
@@ -677,10 +590,14 @@ export const AutoCoreTagProvider: React.FC<{
677
590
  });
678
591
  }, [tags, toDisplay, scaleValues]);
679
592
 
593
+ /**
594
+ * Construct context value. Memoized to prevent consumers from re-rendering
595
+ * unless actual data changes.
596
+ */
680
597
  const ctxValue = useMemo<BaseContextValue<VMapRuntime>>(
681
598
  () => ({
682
- values,
683
- rawValues,
599
+ values, // Display values (scaled)
600
+ rawValues, // Raw values (unscaled)
684
601
  isLoading,
685
602
  write,
686
603
  tap,
@@ -695,4 +612,4 @@ export const AutoCoreTagProvider: React.FC<{
695
612
  {children}
696
613
  </AutoCoreTagContext.Provider>
697
614
  );
698
- };
615
+ };