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