@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.
- package/additional-docs/react_performance_notes.md +94 -0
- package/dist/components/AutoCoreDevPanel.d.ts.map +1 -1
- package/dist/components/AutoCoreDevPanel.js +1 -1
- package/dist/components/FileList.d.ts.map +1 -1
- package/dist/components/FileList.js +1 -1
- package/dist/components/FileSelect.d.ts.map +1 -1
- package/dist/components/FileSelect.js +1 -1
- package/dist/core/AutoCoreTagContext.d.ts +59 -185
- package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
- package/dist/core/AutoCoreTagContext.js +1 -1
- package/dist/core/AutoCoreTagTypes.d.ts +127 -6
- package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
- package/dist/core/CoreStreamTypes.d.ts +345 -0
- package/dist/core/CoreStreamTypes.d.ts.map +1 -0
- package/dist/core/CoreStreamTypes.js +1 -0
- package/dist/core/EventEmitterContext.d.ts +91 -473
- package/dist/core/EventEmitterContext.d.ts.map +1 -1
- package/dist/core/EventEmitterContext.js +1 -1
- package/dist/hooks/adsHooks.d.ts.map +1 -1
- package/dist/hooks/adsHooks.js +1 -1
- package/dist/hooks/commandHooks.d.ts +3 -3
- package/dist/hooks/commandHooks.d.ts.map +1 -1
- package/dist/hooks/commandHooks.js +1 -1
- package/dist/hooks/useAutoCoreTag.js +1 -1
- package/dist/hub/CommandMessage.d.ts +18 -9
- package/dist/hub/CommandMessage.d.ts.map +1 -1
- package/dist/hub/CommandMessage.js +1 -1
- package/dist/hub/DebugPanel.d.ts +31 -0
- package/dist/hub/DebugPanel.d.ts.map +1 -0
- package/dist/hub/DebugPanel.js +1 -0
- package/dist/hub/HubBase.d.ts +83 -129
- package/dist/hub/HubBase.d.ts.map +1 -1
- package/dist/hub/HubBase.js +1 -1
- package/dist/hub/HubSimulate.d.ts +41 -8
- package/dist/hub/HubSimulate.d.ts.map +1 -1
- package/dist/hub/HubSimulate.js +1 -1
- package/dist/hub/HubTauri.d.ts +24 -60
- package/dist/hub/HubTauri.d.ts.map +1 -1
- package/dist/hub/HubTauri.js +1 -1
- package/dist/hub/HubWebSocket.d.ts +33 -17
- package/dist/hub/HubWebSocket.d.ts.map +1 -1
- package/dist/hub/HubWebSocket.js +1 -1
- package/dist/hub/debug.d.ts +23 -0
- package/dist/hub/debug.d.ts.map +1 -0
- package/dist/hub/debug.js +1 -0
- package/dist/hub/index.d.ts +19 -4
- package/dist/hub/index.d.ts.map +1 -1
- package/dist/hub/index.js +1 -1
- package/package.json +8 -4
- package/src/components/AutoCoreDevPanel.tsx +14 -11
- package/src/components/FileList.tsx +5 -4
- package/src/components/FileSelect.tsx +2 -1
- package/src/core/ActionMode.ts +1 -1
- package/src/core/AutoCoreTagContext.tsx +247 -330
- package/src/core/AutoCoreTagTypes.ts +236 -104
- package/src/core/CoreStreamTypes.ts +512 -0
- package/src/core/EventEmitterContext.tsx +182 -520
- package/src/core/IndicatorButtonState.ts +1 -1
- package/src/core/hoc.tsx +1 -1
- package/src/hooks/adsHooks.tsx +21 -22
- package/src/hooks/commandHooks.tsx +23 -19
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useAutoCoreTag.ts +2 -2
- package/src/hooks/useScaledValue.tsx +1 -1
- package/src/hub/CommandMessage.ts +71 -19
- package/src/hub/DebugPanel.ts +280 -0
- package/src/hub/HubBase.ts +147 -223
- package/src/hub/HubSimulate.ts +93 -24
- package/src/hub/HubTauri.ts +87 -96
- package/src/hub/HubWebSocket.ts +118 -140
- package/src/hub/debug.ts +211 -0
- package/src/hub/index.ts +49 -39
- package/docs/.nojekyll +0 -1
- package/docs/assets/hierarchy.js +0 -1
- package/docs/assets/highlight.css +0 -134
- package/docs/assets/icons.js +0 -18
- package/docs/assets/icons.svg +0 -1
- package/docs/assets/main.js +0 -60
- package/docs/assets/navigation.js +0 -1
- package/docs/assets/search.js +0 -1
- package/docs/assets/style.css +0 -1633
- package/docs/classes/components_CodeEditor.CodeEditor.html +0 -135
- package/docs/classes/components_Indicator.Indicator.html +0 -122
- package/docs/classes/components_IndicatorRect.IndicatorRect.html +0 -121
- package/docs/classes/components_JogPanel.JogPanel.html +0 -136
- package/docs/classes/components_Lamp.Lamp.html +0 -122
- package/docs/classes/components_OskDialog.OskDialog.html +0 -125
- package/docs/classes/components_TextInput.TextInput.html +0 -125
- package/docs/classes/components_ValueDisplay.ValueDisplay.html +0 -148
- package/docs/classes/components_ValueIndicator.ValueIndicator.html +0 -126
- package/docs/classes/core_ValueSimulator.ValueSimulator.html +0 -51
- package/docs/classes/hub_HubBase.HubBase.html +0 -106
- package/docs/classes/hub_HubSimulate.HubSimulate.html +0 -75
- package/docs/classes/hub_HubTauri.HubTauri.html +0 -93
- package/docs/classes/hub_HubWebSocket.HubWebSocket.html +0 -112
- package/docs/documents/core_AutoCoreTagContext.AutoCoreTagContext.html +0 -148
- package/docs/enums/components_JogPanel.JogDistanceAction.html +0 -5
- package/docs/enums/components_JogPanel.JogPanelAction.html +0 -18
- package/docs/enums/components_JogPanel.JogSpeedAction.html +0 -5
- package/docs/enums/core_ActionMode.ActionMode.html +0 -6
- package/docs/enums/core_IndicatorColor.IndicatorColor.html +0 -23
- package/docs/functions/assets.BlocklyLogo.html +0 -1
- package/docs/functions/assets.Distance.html +0 -1
- package/docs/functions/assets.JogLong.html +0 -1
- package/docs/functions/assets.JogMedium.html +0 -1
- package/docs/functions/assets.JogShort.html +0 -1
- package/docs/functions/assets.PythonLogo.html +0 -1
- package/docs/functions/assets.Rotation3D.html +0 -1
- package/docs/functions/assets.RotationCcw.html +0 -1
- package/docs/functions/assets.RotationCcwA.html +0 -1
- package/docs/functions/assets.RotationCcwB.html +0 -1
- package/docs/functions/assets.RotationCcwC.html +0 -1
- package/docs/functions/assets.RotationCw.html +0 -1
- package/docs/functions/assets.RotationCwA.html +0 -1
- package/docs/functions/assets.RotationCwB.html +0 -1
- package/docs/functions/assets.RotationCwC.html +0 -1
- package/docs/functions/assets.Run.html +0 -1
- package/docs/functions/assets.Speed.html +0 -1
- package/docs/functions/assets.SpeedFast.html +0 -1
- package/docs/functions/assets.SpeedMedium.html +0 -1
- package/docs/functions/assets.SpeedNone.html +0 -1
- package/docs/functions/assets.SpeedSlow.html +0 -1
- package/docs/functions/assets.Walk.html +0 -1
- package/docs/functions/components_BlocklyEditor.createCustomToolbox.html +0 -6
- package/docs/functions/core_UniqueId.UniqueId.html +0 -9
- package/docs/functions/core_hoc.hocAddSubscription.html +0 -6
- package/docs/functions/hooks_adsHooks.useAdsRegisterSymbols.html +0 -16
- package/docs/functions/hooks_adsHooks.useAdsTapValue.html +0 -8
- package/docs/functions/hooks_adsHooks.useAdsWriteScaledValue.html +0 -18
- package/docs/functions/hooks_adsHooks.useAdsWriteValue.html +0 -9
- package/docs/functions/hooks_commandHooks.useRegisterSymbols.html +0 -16
- package/docs/functions/hooks_commandHooks.useTapValue.html +0 -10
- package/docs/functions/hooks_commandHooks.useWriteScaledValue.html +0 -18
- package/docs/functions/hooks_commandHooks.useWriteValue.html +0 -11
- package/docs/functions/hooks_useAutoCoreTag.ts.makeAutoCoreTagHooks.html +0 -12
- package/docs/functions/hooks_useScaledValue.useScaledValue.html +0 -18
- package/docs/functions/hub.createHub.html +0 -3
- package/docs/hierarchy.html +0 -1
- package/docs/index.html +0 -148
- package/docs/interfaces/components_IndicatorButton.IndicatorButtonProps.html +0 -654
- package/docs/interfaces/components_IndicatorRect.IndicatorRectProps.html +0 -37
- package/docs/interfaces/components_JogPanel.JogPanelButtonDefinition.html +0 -5
- package/docs/interfaces/components_ToggleGroup.ToggleGroupProps.html +0 -644
- package/docs/interfaces/core_AutoCoreTagTypes.BaseContextValue.html +0 -12
- package/docs/interfaces/core_AutoCoreTagTypes.ScaleConfig.html +0 -13
- package/docs/interfaces/core_EventEmitterContext.Action.html +0 -8
- package/docs/interfaces/core_EventEmitterContext.EventEmitterContextType.html +0 -33
- package/docs/interfaces/core_EventEmitterContext.State.html +0 -8
- package/docs/interfaces/core_EventEmitterContext.Subscription.html +0 -6
- package/docs/interfaces/core_IndicatorButtonState.IndicatorButtonState.html +0 -10
- package/docs/interfaces/core_PositionContext.IPositionContext.html +0 -17
- package/docs/interfaces/hub_CommandMessage.CommandMessage.html +0 -6
- package/docs/interfaces/hub_CommandMessage.CommandMessageResult.html +0 -4
- package/docs/modules/assets.html +0 -1
- package/docs/modules/assets_BlocklyLogo.html +0 -1
- package/docs/modules/assets_Distance.html +0 -1
- package/docs/modules/assets_JogLong.html +0 -1
- package/docs/modules/assets_JogMedium.html +0 -1
- package/docs/modules/assets_JogShort.html +0 -1
- package/docs/modules/assets_PythonLogo.html +0 -1
- package/docs/modules/assets_Rotation3D.html +0 -1
- package/docs/modules/assets_RotationCcw.html +0 -1
- package/docs/modules/assets_RotationCcwA.html +0 -1
- package/docs/modules/assets_RotationCcwB.html +0 -1
- package/docs/modules/assets_RotationCcwC.html +0 -1
- package/docs/modules/assets_RotationCw.html +0 -1
- package/docs/modules/assets_RotationCwA.html +0 -1
- package/docs/modules/assets_RotationCwB.html +0 -1
- package/docs/modules/assets_RotationCwC.html +0 -1
- package/docs/modules/assets_Run.html +0 -1
- package/docs/modules/assets_Speed.html +0 -1
- package/docs/modules/assets_SpeedFast.html +0 -1
- package/docs/modules/assets_SpeedMedium.html +0 -1
- package/docs/modules/assets_SpeedNone.html +0 -1
- package/docs/modules/assets_SpeedSlow.html +0 -1
- package/docs/modules/assets_Walk.html +0 -1
- package/docs/modules/components_AutoCoreDevPanel.html +0 -20
- package/docs/modules/components_BlocklyEditor.html +0 -1
- package/docs/modules/components_CodeEditor.html +0 -1
- package/docs/modules/components_FileList.html +0 -1
- package/docs/modules/components_FileSelect.html +0 -1
- package/docs/modules/components_FitText.html +0 -1
- package/docs/modules/components_Indicator.html +0 -1
- package/docs/modules/components_IndicatorButton.html +0 -1
- package/docs/modules/components_IndicatorRect.html +0 -1
- package/docs/modules/components_JogPanel.html +0 -1
- package/docs/modules/components_Lamp.html +0 -1
- package/docs/modules/components_Osk.html +0 -1
- package/docs/modules/components_OskDialog.html +0 -1
- package/docs/modules/components_ProgressBarWithValue.html +0 -1
- package/docs/modules/components_TextInput.html +0 -1
- package/docs/modules/components_ToggleGroup.html +0 -1
- package/docs/modules/components_ValueDisplay.html +0 -1
- package/docs/modules/components_ValueIndicator.html +0 -1
- package/docs/modules/components_ValueInput.html +0 -1
- package/docs/modules/core_ActionMode.html +0 -1
- package/docs/modules/core_AutoCoreTagContext.html +0 -11
- package/docs/modules/core_AutoCoreTagTypes.html +0 -1
- package/docs/modules/core_EventEmitterContext.html +0 -53
- package/docs/modules/core_IndicatorButtonState.html +0 -1
- package/docs/modules/core_IndicatorColor.html +0 -1
- package/docs/modules/core_MaskPatterns.html +0 -1
- package/docs/modules/core_NumerableTypes.html +0 -1
- package/docs/modules/core_PositionContext.html +0 -1
- package/docs/modules/core_UniqueId.html +0 -1
- package/docs/modules/core_ValueSimulator.html +0 -1
- package/docs/modules/core_hoc.html +0 -1
- package/docs/modules/hooks.html +0 -1
- package/docs/modules/hooks_adsHooks.html +0 -1
- package/docs/modules/hooks_commandHooks.html +0 -1
- package/docs/modules/hooks_useAutoCoreTag.ts.html +0 -52
- package/docs/modules/hooks_useScaledValue.html +0 -1
- package/docs/modules/hub.html +0 -1
- package/docs/modules/hub_CommandMessage.html +0 -1
- package/docs/modules/hub_HubBase.html +0 -1
- package/docs/modules/hub_HubSimulate.html +0 -1
- package/docs/modules/hub_HubTauri.html +0 -1
- package/docs/modules/hub_HubWebSocket.html +0 -1
- package/docs/modules.html +0 -23
- package/docs/types/components_IndicatorButton.IndicatorButtonOptionsType.html +0 -1
- package/docs/types/core_AutoCoreTagTypes.ExtractByTag.html +0 -2
- package/docs/types/core_AutoCoreTagTypes.PrimitiveKind.html +0 -1
- package/docs/types/core_AutoCoreTagTypes.TagConfig.html +0 -16
- package/docs/types/core_AutoCoreTagTypes.TagValueMap.html +0 -1
- package/docs/types/core_AutoCoreTagTypes.TagValueOf.html +0 -1
- package/docs/types/core_EventEmitterContext.EmitterDispatchFunction.html +0 -3
- package/docs/types/core_EventEmitterContext.EmitterSubscribeFunction.html +0 -3
- package/docs/types/core_EventEmitterContext.EmitterUnsubscribeFunction.html +0 -3
- package/docs/types/core_NumerableTypes.NumerableFormatOptions.html +0 -4
- package/docs/types/core_hoc.HocAddSubscriptionProps.html +0 -6
- package/docs/variables/components_AutoCoreDevPanel.AutoCoreDevPanel.html +0 -43
- package/docs/variables/components_BlocklyEditor.BlocklyEditor.html +0 -13
- package/docs/variables/components_BlocklyEditor.StandardToolbox.html +0 -1
- package/docs/variables/components_FileList.FileList.html +0 -23
- package/docs/variables/components_FileSelect.FileSelect.html +0 -1
- package/docs/variables/components_FitText.FitText.html +0 -4
- package/docs/variables/components_IndicatorButton.IndicatorButton.html +0 -1
- package/docs/variables/components_JogPanel.DefaultLinearJogButtons.html +0 -2
- package/docs/variables/components_JogPanel.DefaultRotationJogButtons.html +0 -2
- package/docs/variables/components_Osk.Osk.html +0 -1
- package/docs/variables/components_ProgressBarWithValue.ProgressBarWithValue.html +0 -1
- package/docs/variables/components_ToggleGroup.ToggleGroup.html +0 -1
- package/docs/variables/components_ValueInput.ValueInput.html +0 -4
- package/docs/variables/core_AutoCoreTagContext.AutoCoreTagContext.html +0 -1
- package/docs/variables/core_AutoCoreTagContext.AutoCoreTagProvider.html +0 -7
- package/docs/variables/core_EventEmitterContext.EventEmitterContext.html +0 -64
- package/docs/variables/core_EventEmitterContext.EventEmitterProvider.html +0 -10
- package/docs/variables/core_MaskPatterns.PrimeReactMaskPatterns.html +0 -14
- package/docs/variables/core_MaskPatterns.RegExMaskPatterns.html +0 -15
- package/docs/variables/core_PositionContext.DimensionsContext.html +0 -6
- package/docs/variables/hooks_useScaledValue.kMillimeters2Inches.html +0 -2
- package/docs/variables/hooks_useScaledValue.kNewtons2Pounds.html +0 -2
|
@@ -2,7 +2,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:
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
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
|
-
* ❖ **
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
* ❖ **
|
|
47
|
-
* -
|
|
48
|
-
* -
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
* -
|
|
155
|
-
*
|
|
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
|
|
157
|
+
}> = ({ children, tags, scales, eagerRead = true }) => {
|
|
265
158
|
const startedRef = useRef(false);
|
|
266
159
|
|
|
267
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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>>(
|
|
198
|
+
useState<Record<string, ScaleConfig>>(actualScales);
|
|
288
199
|
|
|
289
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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(
|
|
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
|
|
401
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
}, [
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
//
|
|
401
|
+
// 1. Load scales first so initial values are correct
|
|
475
402
|
await pullServerScales();
|
|
476
403
|
|
|
477
|
-
//
|
|
478
|
-
for (const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
//
|
|
508
|
-
if (eagerRead) {
|
|
509
|
-
|
|
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
|
-
/**
|
|
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(
|
|
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
|
-
[
|
|
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}
|
|
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
|
-
/**
|
|
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,
|
|
525
|
+
[tags, hubWrite, toServer]
|
|
607
526
|
);
|
|
608
527
|
|
|
609
|
-
/**
|
|
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
|
-
|
|
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
|
|
545
|
+
await hubWrite(cfg.fqdn, false);
|
|
633
546
|
},
|
|
634
|
-
[tags,
|
|
547
|
+
[tags, hubWrite]
|
|
635
548
|
);
|
|
636
549
|
|
|
637
|
-
/**
|
|
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
|
-
|
|
649
|
-
|
|
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,
|
|
573
|
+
[scaleValues, hubWrite, rescaleFromRaw]
|
|
663
574
|
);
|
|
664
575
|
|
|
665
|
-
/**
|
|
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
|
+
};
|