@adcops/autocore-react 3.3.8 → 3.3.10

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 (215) hide show
  1. package/LICENSE +58 -58
  2. package/additional-docs/AutoCoreTagContext.md +441 -441
  3. package/additional-docs/ButtonApiSpecs.md +48 -48
  4. package/additional-docs/GlobalEventEmitter.md +243 -243
  5. package/additional-docs/general_recommendations.md +22 -22
  6. package/additional-docs/react_performance_notes.md +94 -94
  7. package/dist/assets/svg/blockly_logo.svg +82 -82
  8. package/dist/assets/svg/distance.svg +40 -40
  9. package/dist/assets/svg/python_logo.svg +246 -246
  10. package/dist/assets/svg/rotation_ccw.svg +50 -50
  11. package/dist/assets/svg/rotation_ccw_a.svg +57 -57
  12. package/dist/assets/svg/rotation_ccw_b.svg +57 -57
  13. package/dist/assets/svg/rotation_ccw_c.svg +57 -57
  14. package/dist/assets/svg/rotation_cw.svg +49 -49
  15. package/dist/assets/svg/rotation_cw_a.svg +30 -30
  16. package/dist/assets/svg/rotation_cw_b.svg +30 -30
  17. package/dist/assets/svg/rotation_cw_c.svg +30 -30
  18. package/dist/assets/svg/speed.svg +39 -39
  19. package/dist/components/BlocklyEditor.css +93 -93
  20. package/dist/components/JogPanel.css +41 -41
  21. package/dist/components/ProgressBarWithValue.css +27 -27
  22. package/dist/components/ValueIndicator.css +31 -31
  23. package/dist/components/osk.css +123 -123
  24. package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
  25. package/dist/core/AutoCoreTagContext.js +1 -1
  26. package/dist/hub/HubBase.d.ts +3 -3
  27. package/dist/hub/HubBase.d.ts.map +1 -1
  28. package/dist/hub/HubBase.js +1 -1
  29. package/package.json +104 -104
  30. package/readme.md +343 -343
  31. package/src/assets/BlocklyLogo.tsx +27 -27
  32. package/src/assets/Distance.tsx +18 -18
  33. package/src/assets/JogLong.tsx +13 -13
  34. package/src/assets/JogMedium.tsx +13 -13
  35. package/src/assets/JogShort.tsx +13 -13
  36. package/src/assets/PythonLogo.tsx +83 -83
  37. package/src/assets/Rotation3D.tsx +13 -13
  38. package/src/assets/RotationCcw.tsx +33 -33
  39. package/src/assets/RotationCcwA.tsx +45 -45
  40. package/src/assets/RotationCcwB.tsx +45 -45
  41. package/src/assets/RotationCcwC.tsx +45 -45
  42. package/src/assets/RotationCw.tsx +31 -31
  43. package/src/assets/RotationCwA.tsx +42 -42
  44. package/src/assets/RotationCwB.tsx +42 -42
  45. package/src/assets/RotationCwC.tsx +42 -42
  46. package/src/assets/Run.tsx +13 -13
  47. package/src/assets/Speed.tsx +18 -18
  48. package/src/assets/SpeedFast.tsx +13 -13
  49. package/src/assets/SpeedMedium.tsx +13 -13
  50. package/src/assets/SpeedNone.tsx +13 -13
  51. package/src/assets/SpeedSlow.tsx +13 -13
  52. package/src/assets/Walk.tsx +13 -13
  53. package/src/assets/index.ts +22 -22
  54. package/src/assets/svg/blockly_logo.svg +82 -82
  55. package/src/assets/svg/distance.svg +40 -40
  56. package/src/assets/svg/python_logo.svg +246 -246
  57. package/src/assets/svg/rotation_ccw.svg +50 -50
  58. package/src/assets/svg/rotation_ccw_a.svg +57 -57
  59. package/src/assets/svg/rotation_ccw_b.svg +57 -57
  60. package/src/assets/svg/rotation_ccw_c.svg +57 -57
  61. package/src/assets/svg/rotation_cw.svg +49 -49
  62. package/src/assets/svg/rotation_cw_a.svg +30 -30
  63. package/src/assets/svg/rotation_cw_b.svg +30 -30
  64. package/src/assets/svg/rotation_cw_c.svg +30 -30
  65. package/src/assets/svg/speed.svg +39 -39
  66. package/src/components/AutoCoreDevPanel.tsx +414 -414
  67. package/src/components/BlocklyEditor.css +93 -93
  68. package/src/components/BlocklyEditor.tsx +609 -609
  69. package/src/components/CodeEditor.tsx +155 -155
  70. package/src/components/FileList.tsx +390 -390
  71. package/src/components/FileSelect.tsx +128 -128
  72. package/src/components/FitText.tsx +35 -35
  73. package/src/components/Indicator.tsx +188 -188
  74. package/src/components/IndicatorButton.tsx +214 -214
  75. package/src/components/IndicatorRect.tsx +172 -172
  76. package/src/components/JogPanel.css +41 -41
  77. package/src/components/JogPanel.tsx +461 -461
  78. package/src/components/Lamp.tsx +243 -243
  79. package/src/components/Osk.tsx +192 -192
  80. package/src/components/OskDialog.tsx +164 -164
  81. package/src/components/ProgressBarWithValue.css +27 -27
  82. package/src/components/ProgressBarWithValue.tsx +48 -48
  83. package/src/components/TextInput.tsx +195 -195
  84. package/src/components/ToggleGroup.tsx +322 -322
  85. package/src/components/ValueDisplay.tsx +236 -236
  86. package/src/components/ValueIndicator.css +31 -31
  87. package/src/components/ValueIndicator.tsx +135 -135
  88. package/src/components/ValueInput.tsx +368 -368
  89. package/src/components/osk.css +123 -123
  90. package/src/core/ActionMode.ts +19 -19
  91. package/src/core/AutoCoreTagContext.tsx +625 -614
  92. package/src/core/AutoCoreTagTypes.ts +334 -334
  93. package/src/core/CoreStreamTypes.ts +512 -512
  94. package/src/core/EventEmitterContext.tsx +434 -434
  95. package/src/core/IndicatorButtonState.ts +34 -34
  96. package/src/core/IndicatorColor.ts +35 -35
  97. package/src/core/MaskPatterns.ts +87 -87
  98. package/src/core/NumerableTypes.ts +80 -80
  99. package/src/core/PositionContext.ts +59 -59
  100. package/src/core/UniqueId.ts +41 -41
  101. package/src/core/ValueSimulator.ts +166 -166
  102. package/src/core/hoc.tsx +65 -65
  103. package/src/hooks/adsHooks.tsx +287 -287
  104. package/src/hooks/commandHooks.tsx +300 -300
  105. package/src/hooks/index.ts +12 -12
  106. package/src/hooks/useAutoCoreTag.ts +103 -103
  107. package/src/hooks/useScaledValue.tsx +99 -99
  108. package/src/hub/CommandMessage.ts +89 -89
  109. package/src/hub/DebugPanel.ts +307 -307
  110. package/src/hub/HubBase.ts +249 -236
  111. package/src/hub/HubSimulate.ts +124 -124
  112. package/src/hub/HubTauri.ts +140 -140
  113. package/src/hub/HubWebSocket.ts +250 -250
  114. package/src/hub/debug.ts +211 -211
  115. package/src/hub/index.ts +81 -81
  116. package/src/themes/adc-dark/_extensions.scss +166 -166
  117. package/src/themes/adc-dark/_variables.scss +913 -913
  118. package/src/themes/adc-dark/blue/_fonts.scss +23 -23
  119. package/src/themes/adc-dark/blue/adc_theme.scss +31 -31
  120. package/src/themes/adc-dark/blue/theme.scss +14 -14
  121. package/src/themes/theme-base/_colors.scss +17 -17
  122. package/src/themes/theme-base/_common.scss +74 -74
  123. package/src/themes/theme-base/_components.scss +111 -111
  124. package/src/themes/theme-base/_mixins.scss +243 -243
  125. package/src/themes/theme-base/components/button/_button.scss +644 -644
  126. package/src/themes/theme-base/components/button/_speeddial.scss +91 -91
  127. package/src/themes/theme-base/components/button/_splitbutton.scss +358 -358
  128. package/src/themes/theme-base/components/data/_carousel.scss +39 -39
  129. package/src/themes/theme-base/components/data/_datascroller.scss +47 -47
  130. package/src/themes/theme-base/components/data/_datatable.scss +388 -388
  131. package/src/themes/theme-base/components/data/_dataview.scss +47 -47
  132. package/src/themes/theme-base/components/data/_filter.scss +137 -137
  133. package/src/themes/theme-base/components/data/_orderlist.scss +86 -86
  134. package/src/themes/theme-base/components/data/_organizationchart.scss +50 -50
  135. package/src/themes/theme-base/components/data/_paginator.scss +91 -91
  136. package/src/themes/theme-base/components/data/_picklist.scss +73 -73
  137. package/src/themes/theme-base/components/data/_timeline.scss +38 -38
  138. package/src/themes/theme-base/components/data/_tree.scss +184 -184
  139. package/src/themes/theme-base/components/data/_treetable.scss +431 -431
  140. package/src/themes/theme-base/components/file/_fileupload.scss +41 -41
  141. package/src/themes/theme-base/components/input/_autocomplete.scss +94 -94
  142. package/src/themes/theme-base/components/input/_calendar.scss +251 -251
  143. package/src/themes/theme-base/components/input/_cascadeselect.scss +107 -107
  144. package/src/themes/theme-base/components/input/_checkbox.scss +181 -181
  145. package/src/themes/theme-base/components/input/_chips.scss +102 -102
  146. package/src/themes/theme-base/components/input/_colorpicker.scss +17 -17
  147. package/src/themes/theme-base/components/input/_dropdown.scss +252 -252
  148. package/src/themes/theme-base/components/input/_editor.scss +122 -122
  149. package/src/themes/theme-base/components/input/_iconfield.scss +9 -9
  150. package/src/themes/theme-base/components/input/_inputgroup.scss +74 -74
  151. package/src/themes/theme-base/components/input/_inputicon.scss +14 -14
  152. package/src/themes/theme-base/components/input/_inputnumber.scss +4 -4
  153. package/src/themes/theme-base/components/input/_inputotp.scss +10 -10
  154. package/src/themes/theme-base/components/input/_inputswitch.scss +99 -99
  155. package/src/themes/theme-base/components/input/_inputtext.scss +101 -101
  156. package/src/themes/theme-base/components/input/_listbox.scss +138 -138
  157. package/src/themes/theme-base/components/input/_mention.scss +30 -30
  158. package/src/themes/theme-base/components/input/_multiselect.scss +278 -278
  159. package/src/themes/theme-base/components/input/_password.scss +32 -32
  160. package/src/themes/theme-base/components/input/_radiobutton.scss +169 -169
  161. package/src/themes/theme-base/components/input/_rating.scss +80 -80
  162. package/src/themes/theme-base/components/input/_selectbutton.scss +49 -49
  163. package/src/themes/theme-base/components/input/_slider.scss +49 -49
  164. package/src/themes/theme-base/components/input/_togglebutton.scss +99 -99
  165. package/src/themes/theme-base/components/input/_treeselect.scss +151 -151
  166. package/src/themes/theme-base/components/input/_tristatecheckbox.scss +46 -46
  167. package/src/themes/theme-base/components/menu/_breadcrumb.scss +42 -42
  168. package/src/themes/theme-base/components/menu/_contextmenu.scss +39 -39
  169. package/src/themes/theme-base/components/menu/_dock.scss +109 -109
  170. package/src/themes/theme-base/components/menu/_megamenu.scss +141 -141
  171. package/src/themes/theme-base/components/menu/_menu.scss +33 -33
  172. package/src/themes/theme-base/components/menu/_menubar.scss +216 -216
  173. package/src/themes/theme-base/components/menu/_panelmenu.scss +153 -153
  174. package/src/themes/theme-base/components/menu/_slidemenu.scss +60 -60
  175. package/src/themes/theme-base/components/menu/_steps.scss +57 -57
  176. package/src/themes/theme-base/components/menu/_tabmenu.scss +50 -50
  177. package/src/themes/theme-base/components/menu/_tieredmenu.scss +43 -43
  178. package/src/themes/theme-base/components/messages/_inlinemessage.scss +69 -69
  179. package/src/themes/theme-base/components/messages/_message.scss +107 -107
  180. package/src/themes/theme-base/components/messages/_toast.scss +100 -100
  181. package/src/themes/theme-base/components/misc/_avatar.scss +33 -33
  182. package/src/themes/theme-base/components/misc/_badge.scss +76 -76
  183. package/src/themes/theme-base/components/misc/_chip.scss +38 -38
  184. package/src/themes/theme-base/components/misc/_inplace.scss +17 -17
  185. package/src/themes/theme-base/components/misc/_metergroup.scss +80 -80
  186. package/src/themes/theme-base/components/misc/_progressbar.scss +17 -17
  187. package/src/themes/theme-base/components/misc/_scrolltop.scss +24 -24
  188. package/src/themes/theme-base/components/misc/_skeleton.scss +7 -7
  189. package/src/themes/theme-base/components/misc/_tag.scss +39 -39
  190. package/src/themes/theme-base/components/misc/_terminal.scss +12 -12
  191. package/src/themes/theme-base/components/multimedia/_galleria.scss +153 -153
  192. package/src/themes/theme-base/components/multimedia/_image.scss +53 -53
  193. package/src/themes/theme-base/components/overlay/_confirmpopup.scss +72 -72
  194. package/src/themes/theme-base/components/overlay/_dialog.scss +78 -78
  195. package/src/themes/theme-base/components/overlay/_overlaypanel.scss +64 -64
  196. package/src/themes/theme-base/components/overlay/_sidebar.scss +23 -23
  197. package/src/themes/theme-base/components/overlay/_tooltip.scss +33 -33
  198. package/src/themes/theme-base/components/panel/_accordion.scss +118 -118
  199. package/src/themes/theme-base/components/panel/_card.scss +30 -30
  200. package/src/themes/theme-base/components/panel/_divider.scss +30 -30
  201. package/src/themes/theme-base/components/panel/_fieldset.scss +47 -47
  202. package/src/themes/theme-base/components/panel/_panel.scss +47 -47
  203. package/src/themes/theme-base/components/panel/_scrollpanel.scss +10 -10
  204. package/src/themes/theme-base/components/panel/_splitter.scss +23 -23
  205. package/src/themes/theme-base/components/panel/_stepper.scss +136 -136
  206. package/src/themes/theme-base/components/panel/_tabview.scss +147 -147
  207. package/src/themes/theme-base/components/panel/_toolbar.scss +11 -11
  208. package/terser.config.cjs +25 -25
  209. package/todo.md +18 -18
  210. package/tools/build-themes.cjs +65 -65
  211. package/tools/copy-distribution-files.cjs +77 -77
  212. package/tools/minify.cjs +55 -55
  213. package/tsconfig.json +48 -48
  214. package/typedoc.json +12 -12
  215. package/.claude/settings.local.json +0 -7
@@ -1,615 +1,626 @@
1
- /*
2
- * Copyright (C) 2025 Automated Design Corp.. All Rights Reserved.
3
- * Created Date: 2025-09-05 07:35:46
4
- * -----
5
- * Last Modified: 2026-01-29 09:31:42
6
- * Modified By: ADC
7
- * -----
8
- *
9
- */
10
-
11
- /**
12
- * @module core/AutoCoreTagContext
13
- *
14
- * @document ../../additional-docs/AutCoreTagContext.md
15
- *
16
- * @summary
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.
25
- *
26
- * @remarks
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.
34
- *
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`.
40
- *
41
- * @example Minimal wiring
42
- * ```tsx
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" }
52
- * };
53
- *
54
- * // 3. Wrap application
55
- * <AutoCoreTagProvider tags={tags} scales={scales}>
56
- * <App />
57
- * </AutoCoreTagProvider>
58
- * ```
59
- */
60
-
61
- import React, {
62
- useRef,
63
- createContext,
64
- useCallback,
65
- useContext,
66
- useEffect,
67
- useMemo,
68
- useState,
69
- type ReactNode,
70
- } from "react";
71
- import { EventEmitterContext } from "./EventEmitterContext";
72
- import type {
73
- BaseContextValue,
74
- TagConfig,
75
- ScaleConfig
76
- } from "./AutoCoreTagTypes";
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
- */
84
- type VMapRuntime = Record<string, unknown>;
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
- */
101
- export const AutoCoreTagContext = createContext<BaseContextValue<VMapRuntime>>({
102
- values: {},
103
- rawValues: {},
104
- isLoading: true,
105
- write: async () => { },
106
- tap: async () => { },
107
- scales: {},
108
- updateScale: async () => { },
109
- });
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
- */
120
- type HubEnvelope<T> = { success?: boolean; valid?: boolean; data?: T };
121
-
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
- */
129
- const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
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
-
138
-
139
- /**
140
- * AutoCoreTagProvider
141
- *
142
- * @description
143
- * React Provider that wires up AutoCore tags to live server updates, buffers **raw**
144
- * controller values, exposes **display** values (scales/codec applied), and handles
145
- * writes/taps with proper domain envelopes. Scaling is **always** applied to display
146
- * values only; raw values remain untouched and are the single source of truth.
147
- *
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
151
- */
152
- export const AutoCoreTagProvider: React.FC<{
153
- children: ReactNode;
154
- tags: readonly TagConfig[];
155
- scales?: Record<string, ScaleConfig>;
156
- eagerRead?: boolean;
157
- }> = ({ children, tags, scales, eagerRead = true }) => {
158
- const startedRef = useRef(false);
159
-
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 } =
167
- useContext(EventEmitterContext);
168
-
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
- */
175
- const [rawValues, setRawValues] = useState<Record<string, unknown>>(() => {
176
- const seed: Record<string, unknown> = {};
177
- for (const t of tags) {
178
- if (t.initialValue !== undefined) {
179
- // Interpret initialValue as **raw**.
180
- seed[t.tagName] = t.initialValue;
181
- }
182
- }
183
- return seed;
184
- });
185
-
186
- /**
187
- * App-visible display values, computed from raw using scales/codecs.
188
- * This is what the UI consumes.
189
- * Keyed by `tagName`.
190
- */
191
- const [values, setValues] = useState<Record<string, unknown>>({});
192
-
193
- /**
194
- * Current state of scales (factor/label).
195
- * Can be updated by the server or user interaction.
196
- */
197
- const [scaleValues, setScaleValues] =
198
- useState<Record<string, ScaleConfig>>(actualScales);
199
-
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
- */
204
- const scaleRef = useRef(scaleValues);
205
- const rawRef = useRef(rawValues);
206
- useEffect(() => { scaleRef.current = scaleValues; }, [scaleValues]);
207
- useEffect(() => { rawRef.current = rawValues; }, [rawValues]);
208
-
209
- const [isLoading, setIsLoading] = useState(true);
210
-
211
- /**
212
- * Converts a raw controller value to a display value.
213
- *
214
- * Pipeline: Raw -> [Codec Decode] -> [Scale Multiply] -> Display
215
- */
216
- const toDisplay = useCallback((tag: TagConfig, raw: unknown): unknown => {
217
- const { valueType, scale, codec } = tag;
218
-
219
- // 1) numeric scaling: multiply raw by scale factor
220
- if (valueType === "number" && typeof raw === "number" && scale) {
221
- const s = scaleRef.current[scale];
222
- const factor = s?.scale ?? 1;
223
- return raw * factor;
224
- }
225
-
226
- // 2) codec for json (optional): decode server representation
227
- if (valueType === "json" && codec?.fromServer) {
228
- try { return codec.fromServer(raw); } catch { /* fall through */ }
229
- }
230
-
231
- // 3) pass-through: no transformation needed
232
- return raw;
233
- }, []);
234
-
235
- /**
236
- * Converts a display value back to a raw controller value.
237
- *
238
- * Pipeline: Display -> [Scale Divide] -> [Codec Encode] -> Raw
239
- */
240
- const toServer = useCallback((tag: TagConfig, display: unknown): unknown => {
241
- const { valueType, scale, codec } = tag;
242
-
243
- // 1) invert codec first (json): encode for server
244
- if (valueType === "json" && codec?.toServer) {
245
- try { display = codec.toServer(display as any); } catch { /* fall through */ }
246
- }
247
-
248
- // 2) inverse numeric scaling: divide by scale factor
249
- if (valueType === "number" && typeof display === "number" && scale) {
250
- const s = scaleRef.current[scale];
251
- const factor = s?.scale ?? 1;
252
- return display / factor;
253
- }
254
-
255
- return display;
256
- }, []);
257
-
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
- */
265
- const rescaleFromRaw = useCallback((scaleName: string) => {
266
- const affected = tags.filter(t => t.scale === scaleName);
267
- if (!affected.length) return;
268
-
269
- setValues(prev => {
270
- const next = { ...prev };
271
- for (const tag of affected) {
272
- const raw = rawRef.current[tag.tagName];
273
- if (typeof raw !== "number") continue;
274
- const disp = toDisplay(tag, raw);
275
- if (next[tag.tagName] !== disp) next[tag.tagName] = disp;
276
- }
277
- return next;
278
- });
279
- }, [tags, toDisplay]);
280
-
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
- */
289
- const handleTagUpdate = useCallback((tag: TagConfig, raw: unknown) => {
290
- // Store the raw controller value (source of truth for scaling)
291
- setRawValues(prev =>
292
- prev[tag.tagName] === raw ? prev : { ...prev, [tag.tagName]: raw }
293
- );
294
- // Compute and store the display value (with scaling/codecs applied)
295
- const display = toDisplay(tag, raw);
296
- setValues(prev =>
297
- prev[tag.tagName] === display ? prev : { ...prev, [tag.tagName]: display }
298
- );
299
- }, [toDisplay]);
300
-
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
- */
305
- const eagerPullNonADS = useCallback(
306
- async (
307
- domain: string,
308
- tagList: readonly TagConfig[],
309
- opts?: { concurrency?: number; minDelayMs?: number; jitterMs?: number }
310
- ) => {
311
- const concurrency = opts?.concurrency ?? 4;
312
- const minDelayMs = opts?.minDelayMs ?? 20;
313
- const jitterMs = opts?.jitterMs ?? 40;
314
-
315
- let index = 0;
316
-
317
- const workers = Array.from({ length: concurrency }, () => (async () => {
318
- while (true) {
319
- const i = index++;
320
- if (i >= tagList.length) return;
321
- const tag = tagList[i];
322
-
323
- try {
324
- // Try 'refresh' first (server broadcast), else 'read_value'
325
- let usedPublishPath = false;
326
- try {
327
- await invoke(tag.fqdn, MessageType.Request, { action: "refresh" });
328
- usedPublishPath = true;
329
- } catch { /* fall through */ }
330
-
331
- if (!usedPublishPath) {
332
- const payload = domain.toUpperCase() === "GNV" ? { group: "ux" } : {};
333
- try {
334
- const resp: any = await read(tag.fqdn, payload);
335
- if (resp?.success) {
336
- handleTagUpdate(tag, resp.data);
337
- }
338
- } catch (err) { /* ignore missing keys on init */ }
339
- }
340
- } catch (outerErr) { /* ignore */ }
341
- finally {
342
- const extra = Math.floor(Math.random() * jitterMs);
343
- await sleep(minDelayMs + extra);
344
- }
345
- }
346
- })());
347
-
348
- await Promise.all(workers);
349
- },
350
- [invoke, read, handleTagUpdate]
351
- );
352
-
353
- /**
354
- * Reads scale configurations from the server (if configured).
355
- * Ensures display units match the server's persisted configuration.
356
- */
357
- const pullServerScales = useCallback(async () => {
358
- // Use actualScales to ensure we iterate over a stable object
359
- for (const [scaleName, cfg] of Object.entries(actualScales)) {
360
- if (!cfg.serverTag) continue;
361
- const { domain, symbolName } = cfg.serverTag;
362
-
363
- try {
364
- const respUnknown = await read(`${domain}.${symbolName}`, { group: "ux" });
365
- const env = respUnknown as HubEnvelope<string>;
366
-
367
- if (env.data && (env.success ?? true) && (env.valid ?? true)) {
368
- const env_data = JSON.parse(env.data) as ScaleConfig;
369
- if (env_data && typeof env_data.scale === "number" ) {
370
- const { scale, label } = env_data;
371
- setScaleValues(prev => ({
372
- ...prev,
373
- [scaleName]: {
374
- ...prev[scaleName],
375
- scale,
376
- label: label ?? prev[scaleName]?.label ?? "---",
377
- },
378
- }));
379
- rescaleFromRaw(scaleName);
380
- }
381
- }
382
- } catch { /* Scale not present, use default */ }
383
- }
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
- */
395
- useEffect(() => {
396
- let mounted = true;
397
- const subscriptions: number[] = [];
398
-
399
- const registerAndSubscribe = async () => {
400
- try {
401
- // 1. Load scales first so initial values are correct
402
- await pullServerScales();
403
-
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);
420
- }
421
-
422
- // // 3. ADS Refresh: Ask ADS to broadcast current values immediately.
423
- // if (eagerRead) {
424
- // await invoke("ADS.refresh", MessageType.Request, {});
425
- // }
426
-
427
- } finally {
428
- if (mounted) setTimeout(() => mounted && setIsLoading(false), 100);
429
- }
430
- };
431
-
432
- // Prevents double-execution in React Strict Mode
433
- const safeRegister = async () => {
434
- if (!mounted || startedRef.current) return;
435
- startedRef.current = true;
436
- try { await registerAndSubscribe(); }
437
- finally { if (mounted) setTimeout(() => mounted && setIsLoading(false), 100); }
438
- };
439
-
440
- // Wait for connection before registering
441
- if (!isConnected()) {
442
- const id = subscribe("HUB/connected", () => {
443
- unsubscribe(id);
444
- void safeRegister();
445
- });
446
- subscriptions.push(id);
447
- } else {
448
- void safeRegister();
449
- }
450
-
451
- return () => {
452
- mounted = false;
453
- subscriptions.forEach(unsubscribe);
454
- startedRef.current = false;
455
- };
456
- }, [subscribe, unsubscribe, isConnected, invoke, serverSubscribe, eagerRead, tags, pullServerScales, eagerPullNonADS, handleTagUpdate]);
457
-
458
- /**
459
- * Memoized list of scales that need server subscriptions.
460
- * Used for the separate effect that listens for live scale changes.
461
- */
462
- const scaleServerSubs = useMemo(
463
- () =>
464
- Object.entries(actualScales)
465
- .filter(([, cfg]) => cfg.serverTag)
466
- .map(([scaleName, cfg]) => ({
467
- scaleName,
468
- domain: cfg.serverTag!.domain,
469
- symbolName: cfg.serverTag!.symbolName,
470
- })),
471
- [actualScales]
472
- );
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
- */
478
- useEffect(() => {
479
- let mounted = true;
480
- const subs: number[] = [];
481
-
482
- for (const { scaleName, domain, symbolName } of scaleServerSubs) {
483
- const id = subscribe(`${domain}.${symbolName}`, (data) => {
484
- if (!mounted) return;
485
- const v = data?.value; // Scales come as { value: { scale, label } } usually
486
- if (v && typeof v === "object" && typeof (v as any).scale === "number") {
487
- const { scale, label } = v as { scale: number; label?: string };
488
- setScaleValues(prev => ({
489
- ...prev,
490
- [scaleName]: {
491
- ...prev[scaleName],
492
- scale,
493
- label: label ?? prev[scaleName]?.label ?? "---",
494
- },
495
- }));
496
- rescaleFromRaw(scaleName);
497
- }
498
- });
499
- subs.push(id);
500
- }
501
-
502
- return () => {
503
- mounted = false;
504
- subs.forEach(unsubscribe);
505
- };
506
- }, [subscribe, unsubscribe, scaleServerSubs, rescaleFromRaw]);
507
-
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
- */
514
- const write = useCallback(
515
- async (tagName: string, displayValue: unknown) => {
516
- const cfg = tags.find((t) => t.tagName === tagName);
517
- if (!cfg) {
518
- console.error(`write(): unknown tag '${tagName}'`);
519
- return;
520
- }
521
-
522
- const serverValue = toServer(cfg, displayValue);
523
- await hubWrite(cfg.fqdn, serverValue);
524
- },
525
- [tags, hubWrite, toServer]
526
- );
527
-
528
- /**
529
- * Momentary pulse for boolean tags (True -> Wait 300ms -> False).
530
- */
531
- const tap = useCallback(
532
- async (tagName: string) => {
533
- const cfg = tags.find((t) => t.tagName === tagName);
534
- if (!cfg) {
535
- console.error(`tap(): unknown tag '${tagName}'`);
536
- return;
537
- }
538
- if (cfg.valueType !== "boolean") {
539
- console.warn(`tap(): tag '${tagName}' is not a boolean type`);
540
- return;
541
- }
542
-
543
- await hubWrite(cfg.fqdn, true);
544
- await sleep(300);
545
- await hubWrite(cfg.fqdn, false);
546
- },
547
- [tags, hubWrite]
548
- );
549
-
550
- /**
551
- * Updates a scale factor locally and optionally persists to server.
552
- */
553
- const updateScale = useCallback(
554
- async (scaleName: string, newScale: number, newLabel: string) => {
555
- const cfg = scaleValues[scaleName];
556
- if (!cfg) {
557
- console.error(`Scale '${scaleName}' not found`);
558
- return;
559
- }
560
-
561
- if (cfg.serverTag) {
562
- const topic = `${cfg.serverTag.domain}.${cfg.serverTag.symbolName}`;
563
- await hubWrite(topic, { name: scaleName, scale: newScale, label: newLabel });
564
- }
565
-
566
- setScaleValues(prev => ({
567
- ...prev,
568
- [scaleName]: { ...prev[scaleName], scale: newScale, label: newLabel },
569
- }));
570
-
571
- rescaleFromRaw(scaleName);
572
- },
573
- [scaleValues, hubWrite, rescaleFromRaw]
574
- );
575
-
576
- /**
577
- * Safety net: Recomputes all display values whenever scales or tags change.
578
- * Ensures UI consistency even if individual updates were missed.
579
- */
580
- useEffect(() => {
581
- setValues(prev => {
582
- const next = { ...prev };
583
- for (const tag of tags) {
584
- const raw = rawRef.current[tag.tagName];
585
- if (raw === undefined) continue;
586
- const disp = toDisplay(tag, raw);
587
- if (next[tag.tagName] !== disp) next[tag.tagName] = disp;
588
- }
589
- return next;
590
- });
591
- }, [tags, toDisplay, scaleValues]);
592
-
593
- /**
594
- * Construct context value. Memoized to prevent consumers from re-rendering
595
- * unless actual data changes.
596
- */
597
- const ctxValue = useMemo<BaseContextValue<VMapRuntime>>(
598
- () => ({
599
- values, // Display values (scaled)
600
- rawValues, // Raw values (unscaled)
601
- isLoading,
602
- write,
603
- tap,
604
- scales: scaleValues,
605
- updateScale,
606
- }),
607
- [values, rawValues, isLoading, write, tap, scaleValues, updateScale]
608
- );
609
-
610
- return (
611
- <AutoCoreTagContext.Provider value={ctxValue}>
612
- {children}
613
- </AutoCoreTagContext.Provider>
614
- );
1
+ /*
2
+ * Copyright (C) 2025 Automated Design Corp.. All Rights Reserved.
3
+ * Created Date: 2025-09-05 07:35:46
4
+ * -----
5
+ * Last Modified: 2026-01-29 09:31:42
6
+ * Modified By: ADC
7
+ * -----
8
+ *
9
+ */
10
+
11
+ /**
12
+ * @module core/AutoCoreTagContext
13
+ *
14
+ * @document ../../additional-docs/AutCoreTagContext.md
15
+ *
16
+ * @summary
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.
25
+ *
26
+ * @remarks
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.
34
+ *
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`.
40
+ *
41
+ * @example Minimal wiring
42
+ * ```tsx
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" }
52
+ * };
53
+ *
54
+ * // 3. Wrap application
55
+ * <AutoCoreTagProvider tags={tags} scales={scales}>
56
+ * <App />
57
+ * </AutoCoreTagProvider>
58
+ * ```
59
+ */
60
+
61
+ import React, {
62
+ useRef,
63
+ createContext,
64
+ useCallback,
65
+ useContext,
66
+ useEffect,
67
+ useMemo,
68
+ useState,
69
+ type ReactNode,
70
+ } from "react";
71
+ import { EventEmitterContext } from "./EventEmitterContext";
72
+ import type {
73
+ BaseContextValue,
74
+ TagConfig,
75
+ ScaleConfig
76
+ } from "./AutoCoreTagTypes";
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
+ */
84
+ type VMapRuntime = Record<string, unknown>;
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
+ */
101
+ export const AutoCoreTagContext = createContext<BaseContextValue<VMapRuntime>>({
102
+ values: {},
103
+ rawValues: {},
104
+ isLoading: true,
105
+ write: async () => { },
106
+ tap: async () => { },
107
+ scales: {},
108
+ updateScale: async () => { },
109
+ });
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
+ */
120
+ type HubEnvelope<T> = { success?: boolean; valid?: boolean; data?: T };
121
+
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
+ */
129
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
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
+
138
+
139
+ /**
140
+ * AutoCoreTagProvider
141
+ *
142
+ * @description
143
+ * React Provider that wires up AutoCore tags to live server updates, buffers **raw**
144
+ * controller values, exposes **display** values (scales/codec applied), and handles
145
+ * writes/taps with proper domain envelopes. Scaling is **always** applied to display
146
+ * values only; raw values remain untouched and are the single source of truth.
147
+ *
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
151
+ */
152
+ export const AutoCoreTagProvider: React.FC<{
153
+ children: ReactNode;
154
+ tags: readonly TagConfig[];
155
+ scales?: Record<string, ScaleConfig>;
156
+ eagerRead?: boolean;
157
+ }> = ({ children, tags, scales, eagerRead = true }) => {
158
+ const startedRef = useRef(false);
159
+
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 } =
167
+ useContext(EventEmitterContext);
168
+
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
+ */
175
+ const [rawValues, setRawValues] = useState<Record<string, unknown>>(() => {
176
+ const seed: Record<string, unknown> = {};
177
+ for (const t of tags) {
178
+ if (t.initialValue !== undefined) {
179
+ // Interpret initialValue as **raw**.
180
+ seed[t.tagName] = t.initialValue;
181
+ }
182
+ }
183
+ return seed;
184
+ });
185
+
186
+ /**
187
+ * App-visible display values, computed from raw using scales/codecs.
188
+ * This is what the UI consumes.
189
+ * Keyed by `tagName`.
190
+ */
191
+ const [values, setValues] = useState<Record<string, unknown>>({});
192
+
193
+ /**
194
+ * Current state of scales (factor/label).
195
+ * Can be updated by the server or user interaction.
196
+ */
197
+ const [scaleValues, setScaleValues] =
198
+ useState<Record<string, ScaleConfig>>(actualScales);
199
+
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
+ */
204
+ const scaleRef = useRef(scaleValues);
205
+ const rawRef = useRef(rawValues);
206
+ useEffect(() => { scaleRef.current = scaleValues; }, [scaleValues]);
207
+ useEffect(() => { rawRef.current = rawValues; }, [rawValues]);
208
+
209
+ const [isLoading, setIsLoading] = useState(true);
210
+
211
+ /**
212
+ * Converts a raw controller value to a display value.
213
+ *
214
+ * Pipeline: Raw -> [Codec Decode] -> [Scale Multiply] -> Display
215
+ */
216
+ const toDisplay = useCallback((tag: TagConfig, raw: unknown): unknown => {
217
+ const { valueType, scale, codec } = tag;
218
+
219
+ // 1) numeric scaling: multiply raw by scale factor
220
+ if (valueType === "number" && typeof raw === "number" && scale) {
221
+ const s = scaleRef.current[scale];
222
+ const factor = s?.scale ?? 1;
223
+ return raw * factor;
224
+ }
225
+
226
+ // 2) codec for json (optional): decode server representation
227
+ if (valueType === "json" && codec?.fromServer) {
228
+ try { return codec.fromServer(raw); } catch { /* fall through */ }
229
+ }
230
+
231
+ // 3) pass-through: no transformation needed
232
+ return raw;
233
+ }, []);
234
+
235
+ /**
236
+ * Converts a display value back to a raw controller value.
237
+ *
238
+ * Pipeline: Display -> [Scale Divide] -> [Codec Encode] -> Raw
239
+ */
240
+ const toServer = useCallback((tag: TagConfig, display: unknown): unknown => {
241
+ const { valueType, scale, codec } = tag;
242
+
243
+ // 1) invert codec first (json): encode for server
244
+ if (valueType === "json" && codec?.toServer) {
245
+ try { display = codec.toServer(display as any); } catch { /* fall through */ }
246
+ }
247
+
248
+ // 2) inverse numeric scaling: divide by scale factor
249
+ if (valueType === "number" && typeof display === "number" && scale) {
250
+ const s = scaleRef.current[scale];
251
+ const factor = s?.scale ?? 1;
252
+ return display / factor;
253
+ }
254
+
255
+ return display;
256
+ }, []);
257
+
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
+ */
265
+ const rescaleFromRaw = useCallback((scaleName: string) => {
266
+ const affected = tags.filter(t => t.scale === scaleName);
267
+ if (!affected.length) return;
268
+
269
+ setValues(prev => {
270
+ const next = { ...prev };
271
+ for (const tag of affected) {
272
+ const raw = rawRef.current[tag.tagName];
273
+ if (typeof raw !== "number") continue;
274
+ const disp = toDisplay(tag, raw);
275
+ if (next[tag.tagName] !== disp) next[tag.tagName] = disp;
276
+ }
277
+ return next;
278
+ });
279
+ }, [tags, toDisplay]);
280
+
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
+ */
289
+ const handleTagUpdate = useCallback((tag: TagConfig, raw: unknown) => {
290
+ // Store the raw controller value (source of truth for scaling)
291
+ setRawValues(prev =>
292
+ prev[tag.tagName] === raw ? prev : { ...prev, [tag.tagName]: raw }
293
+ );
294
+ // Compute and store the display value (with scaling/codecs applied)
295
+ const display = toDisplay(tag, raw);
296
+ setValues(prev =>
297
+ prev[tag.tagName] === display ? prev : { ...prev, [tag.tagName]: display }
298
+ );
299
+ }, [toDisplay]);
300
+
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
+ */
305
+ const eagerPullNonADS = useCallback(
306
+ async (
307
+ domain: string,
308
+ tagList: readonly TagConfig[],
309
+ opts?: { concurrency?: number; minDelayMs?: number; jitterMs?: number }
310
+ ) => {
311
+ const concurrency = opts?.concurrency ?? 4;
312
+ const minDelayMs = opts?.minDelayMs ?? 20;
313
+ const jitterMs = opts?.jitterMs ?? 40;
314
+
315
+ let index = 0;
316
+
317
+ const workers = Array.from({ length: concurrency }, () => (async () => {
318
+ while (true) {
319
+ const i = index++;
320
+ if (i >= tagList.length) return;
321
+ const tag = tagList[i];
322
+
323
+ try {
324
+ // Try 'refresh' first (server broadcast), else 'read_value'
325
+ let usedPublishPath = false;
326
+ try {
327
+ await invoke(tag.fqdn, MessageType.Request, { action: "refresh" });
328
+ usedPublishPath = true;
329
+ } catch { /* fall through */ }
330
+
331
+ if (!usedPublishPath) {
332
+ const payload = domain.toUpperCase() === "GNV" ? { group: "ux" } : {};
333
+ try {
334
+ const resp: any = await read(tag.fqdn, payload);
335
+ if (resp?.success) {
336
+ handleTagUpdate(tag, resp.data);
337
+ }
338
+ } catch (err) { /* ignore missing keys on init */ }
339
+ }
340
+ } catch (outerErr) { /* ignore */ }
341
+ finally {
342
+ const extra = Math.floor(Math.random() * jitterMs);
343
+ await sleep(minDelayMs + extra);
344
+ }
345
+ }
346
+ })());
347
+
348
+ await Promise.all(workers);
349
+ },
350
+ [invoke, read, handleTagUpdate]
351
+ );
352
+
353
+ /**
354
+ * Reads scale configurations from the server (if configured).
355
+ * Ensures display units match the server's persisted configuration.
356
+ */
357
+ const pullServerScales = useCallback(async () => {
358
+ // Use actualScales to ensure we iterate over a stable object
359
+ for (const [scaleName, cfg] of Object.entries(actualScales)) {
360
+ if (!cfg.serverTag) continue;
361
+ const { domain, symbolName } = cfg.serverTag;
362
+
363
+ try {
364
+ const respUnknown = await read(`${domain}.${symbolName}`, { group: "ux" });
365
+ const env = respUnknown as HubEnvelope<string>;
366
+
367
+ if (env.data && (env.success ?? true) && (env.valid ?? true)) {
368
+ const env_data = JSON.parse(env.data) as ScaleConfig;
369
+ if (env_data && typeof env_data.scale === "number" ) {
370
+ const { scale, label } = env_data;
371
+ setScaleValues(prev => ({
372
+ ...prev,
373
+ [scaleName]: {
374
+ ...prev[scaleName],
375
+ scale,
376
+ label: label ?? prev[scaleName]?.label ?? "---",
377
+ },
378
+ }));
379
+ rescaleFromRaw(scaleName);
380
+ }
381
+ }
382
+ } catch { /* Scale not present, use default */ }
383
+ }
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
+ */
395
+ useEffect(() => {
396
+ let mounted = true;
397
+ const subscriptions: number[] = [];
398
+
399
+ const registerAndSubscribe = async () => {
400
+ try {
401
+ // 1. Load scales first so initial values are correct
402
+ await pullServerScales();
403
+
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);
420
+
421
+ // C. Eager Read: Get the initial value immediately.
422
+ if (eagerRead) {
423
+ read(tag.fqdn).then(resp => {
424
+ if (mounted && resp?.success) {
425
+ handleTagUpdate(tag, resp.data);
426
+ }
427
+ }).catch(e => {
428
+ console.warn(`Initial read failed for ${tag.tagName} (${tag.fqdn}):`, e);
429
+ });
430
+ }
431
+ }
432
+
433
+ // // 3. ADS Refresh: Ask ADS to broadcast current values immediately.
434
+ // if (eagerRead) {
435
+ // await invoke("ADS.refresh", MessageType.Request, {});
436
+ // }
437
+
438
+ } finally {
439
+ if (mounted) setTimeout(() => mounted && setIsLoading(false), 100);
440
+ }
441
+ };
442
+
443
+ // Prevents double-execution in React Strict Mode
444
+ const safeRegister = async () => {
445
+ if (!mounted || startedRef.current) return;
446
+ startedRef.current = true;
447
+ try { await registerAndSubscribe(); }
448
+ finally { if (mounted) setTimeout(() => mounted && setIsLoading(false), 100); }
449
+ };
450
+
451
+ // Wait for connection before registering
452
+ if (!isConnected()) {
453
+ const id = subscribe("HUB/connected", () => {
454
+ unsubscribe(id);
455
+ void safeRegister();
456
+ });
457
+ subscriptions.push(id);
458
+ } else {
459
+ void safeRegister();
460
+ }
461
+
462
+ return () => {
463
+ mounted = false;
464
+ subscriptions.forEach(unsubscribe);
465
+ startedRef.current = false;
466
+ };
467
+ }, [subscribe, unsubscribe, isConnected, invoke, serverSubscribe, eagerRead, tags, pullServerScales, eagerPullNonADS, handleTagUpdate]);
468
+
469
+ /**
470
+ * Memoized list of scales that need server subscriptions.
471
+ * Used for the separate effect that listens for live scale changes.
472
+ */
473
+ const scaleServerSubs = useMemo(
474
+ () =>
475
+ Object.entries(actualScales)
476
+ .filter(([, cfg]) => cfg.serverTag)
477
+ .map(([scaleName, cfg]) => ({
478
+ scaleName,
479
+ domain: cfg.serverTag!.domain,
480
+ symbolName: cfg.serverTag!.symbolName,
481
+ })),
482
+ [actualScales]
483
+ );
484
+
485
+ /**
486
+ * Effect to listen for live scale changes from the server.
487
+ * If another user changes units, this updates our local state immediately.
488
+ */
489
+ useEffect(() => {
490
+ let mounted = true;
491
+ const subs: number[] = [];
492
+
493
+ for (const { scaleName, domain, symbolName } of scaleServerSubs) {
494
+ const id = subscribe(`${domain}.${symbolName}`, (data) => {
495
+ if (!mounted) return;
496
+ const v = data?.value; // Scales come as { value: { scale, label } } usually
497
+ if (v && typeof v === "object" && typeof (v as any).scale === "number") {
498
+ const { scale, label } = v as { scale: number; label?: string };
499
+ setScaleValues(prev => ({
500
+ ...prev,
501
+ [scaleName]: {
502
+ ...prev[scaleName],
503
+ scale,
504
+ label: label ?? prev[scaleName]?.label ?? "---",
505
+ },
506
+ }));
507
+ rescaleFromRaw(scaleName);
508
+ }
509
+ });
510
+ subs.push(id);
511
+ }
512
+
513
+ return () => {
514
+ mounted = false;
515
+ subs.forEach(unsubscribe);
516
+ };
517
+ }, [subscribe, unsubscribe, scaleServerSubs, rescaleFromRaw]);
518
+
519
+ /**
520
+ * Writes a value to the server.
521
+ * 1. Finds tag config by `tagName`.
522
+ * 2. Inverse-scales the value (Display -> Raw).
523
+ * 3. Sends update to server using `fqdn`.
524
+ */
525
+ const write = useCallback(
526
+ async (tagName: string, displayValue: unknown) => {
527
+ const cfg = tags.find((t) => t.tagName === tagName);
528
+ if (!cfg) {
529
+ console.error(`write(): unknown tag '${tagName}'`);
530
+ return;
531
+ }
532
+
533
+ const serverValue = toServer(cfg, displayValue);
534
+ await hubWrite(cfg.fqdn, serverValue);
535
+ },
536
+ [tags, hubWrite, toServer]
537
+ );
538
+
539
+ /**
540
+ * Momentary pulse for boolean tags (True -> Wait 300ms -> False).
541
+ */
542
+ const tap = useCallback(
543
+ async (tagName: string) => {
544
+ const cfg = tags.find((t) => t.tagName === tagName);
545
+ if (!cfg) {
546
+ console.error(`tap(): unknown tag '${tagName}'`);
547
+ return;
548
+ }
549
+ if (cfg.valueType !== "boolean") {
550
+ console.warn(`tap(): tag '${tagName}' is not a boolean type`);
551
+ return;
552
+ }
553
+
554
+ await hubWrite(cfg.fqdn, true);
555
+ await sleep(300);
556
+ await hubWrite(cfg.fqdn, false);
557
+ },
558
+ [tags, hubWrite]
559
+ );
560
+
561
+ /**
562
+ * Updates a scale factor locally and optionally persists to server.
563
+ */
564
+ const updateScale = useCallback(
565
+ async (scaleName: string, newScale: number, newLabel: string) => {
566
+ const cfg = scaleValues[scaleName];
567
+ if (!cfg) {
568
+ console.error(`Scale '${scaleName}' not found`);
569
+ return;
570
+ }
571
+
572
+ if (cfg.serverTag) {
573
+ const topic = `${cfg.serverTag.domain}.${cfg.serverTag.symbolName}`;
574
+ await hubWrite(topic, { name: scaleName, scale: newScale, label: newLabel });
575
+ }
576
+
577
+ setScaleValues(prev => ({
578
+ ...prev,
579
+ [scaleName]: { ...prev[scaleName], scale: newScale, label: newLabel },
580
+ }));
581
+
582
+ rescaleFromRaw(scaleName);
583
+ },
584
+ [scaleValues, hubWrite, rescaleFromRaw]
585
+ );
586
+
587
+ /**
588
+ * Safety net: Recomputes all display values whenever scales or tags change.
589
+ * Ensures UI consistency even if individual updates were missed.
590
+ */
591
+ useEffect(() => {
592
+ setValues(prev => {
593
+ const next = { ...prev };
594
+ for (const tag of tags) {
595
+ const raw = rawRef.current[tag.tagName];
596
+ if (raw === undefined) continue;
597
+ const disp = toDisplay(tag, raw);
598
+ if (next[tag.tagName] !== disp) next[tag.tagName] = disp;
599
+ }
600
+ return next;
601
+ });
602
+ }, [tags, toDisplay, scaleValues]);
603
+
604
+ /**
605
+ * Construct context value. Memoized to prevent consumers from re-rendering
606
+ * unless actual data changes.
607
+ */
608
+ const ctxValue = useMemo<BaseContextValue<VMapRuntime>>(
609
+ () => ({
610
+ values, // Display values (scaled)
611
+ rawValues, // Raw values (unscaled)
612
+ isLoading,
613
+ write,
614
+ tap,
615
+ scales: scaleValues,
616
+ updateScale,
617
+ }),
618
+ [values, rawValues, isLoading, write, tap, scaleValues, updateScale]
619
+ );
620
+
621
+ return (
622
+ <AutoCoreTagContext.Provider value={ctxValue}>
623
+ {children}
624
+ </AutoCoreTagContext.Provider>
625
+ );
615
626
  };