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