@camstack/system 1.1.29 → 1.1.31
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/dist/builtins/device-manager/day-night-config-schema.d.ts +16 -0
- package/dist/builtins/device-manager/device-aggregation.d.ts +28 -1
- package/dist/builtins/device-manager/device-config-contribution.d.ts +16 -4
- package/dist/builtins/device-manager/device-manager.addon.js +398 -19
- package/dist/builtins/device-manager/device-manager.addon.mjs +398 -19
- package/dist/builtins/device-manager/image-settings-config-schema.d.ts +12 -0
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.d.ts +17 -4
- package/dist/builtins/native-metrics/native-metrics.addon.js +47 -8
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +47 -8
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ConfigUISchema, DayNightOptions, DayNightSettingsPatch, DayNightStatus } from '@camstack/types';
|
|
2
|
+
/**
|
|
3
|
+
* Build the `day-night` `ConfigUISchema` from the camera-probed options +
|
|
4
|
+
* current status. Returns `null` when the camera exposes no configurable
|
|
5
|
+
* property — the cap then reports "no schema" and the renderer shows the
|
|
6
|
+
* unsupported-camera message.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildDayNightConfigSchema(options: DayNightOptions, status: DayNightStatus | null): ConfigUISchema | null;
|
|
9
|
+
/**
|
|
10
|
+
* Re-parse a flat `ConfigFormBuilder` patch into a `DayNightSettingsPatch`.
|
|
11
|
+
* Returns an empty object when no `dayNight_*` field changed — the caller
|
|
12
|
+
* (`parseDerivedFormSettingsPatch`) forwards the result verbatim to the
|
|
13
|
+
* cap's `setSettings` mutation, which itself ignores fields it doesn't
|
|
14
|
+
* support.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseDayNightFormPatch(patch: Record<string, unknown>): DayNightSettingsPatch;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deviceManagerCapability, InferProvider, AddonContext, CapabilityDefinition, DeviceBindingEntry, DeviceConfigSpec, DeviceSettingsContribution, ICapabilityRegistry, IDevice } from '@camstack/types';
|
|
1
|
+
import { deviceManagerCapability, InferProvider, AddonContext, CapabilityDefinition, DeviceBindingEntry, DeviceConfigSpec, DeviceSettingsContribution, ICapabilityRegistry, IDevice, StreamProfile, StreamProfilePatch } from '@camstack/types';
|
|
2
2
|
import { ContributionShape, DriverConfigSchemaResult } from './device-meta-types.js';
|
|
3
3
|
import { BindingsDeps, RemoteNativeCaps } from './device-bindings-store.js';
|
|
4
4
|
/**
|
|
@@ -122,6 +122,33 @@ export declare function resolveDriverConfigSchema(deps: AggregationDeps, deviceI
|
|
|
122
122
|
addonId: string;
|
|
123
123
|
device: IDevice;
|
|
124
124
|
} | null): Promise<DriverConfigSchemaResult>;
|
|
125
|
+
/**
|
|
126
|
+
* Provider surface for a `derived-form` device-config cap's save path.
|
|
127
|
+
* Exactly one of `setProfile` / `setSettings` is present on the actual
|
|
128
|
+
* bound provider — which one is present tells us how to route the
|
|
129
|
+
* patch. `stream-params` exposes `setProfile` (profile axis); `day-night`
|
|
130
|
+
* / `image-settings` expose `setSettings` (no profile axis). Never branch
|
|
131
|
+
* on the cap name here — the method surface is the source of truth.
|
|
132
|
+
*/
|
|
133
|
+
export interface DerivedFormSaveSurface {
|
|
134
|
+
setProfile?: (input: {
|
|
135
|
+
deviceId: number;
|
|
136
|
+
profile: StreamProfile;
|
|
137
|
+
patch: StreamProfilePatch;
|
|
138
|
+
}) => Promise<void>;
|
|
139
|
+
setSettings?: (input: {
|
|
140
|
+
deviceId: number;
|
|
141
|
+
settings: Record<string, unknown>;
|
|
142
|
+
}) => Promise<void>;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Route a `derived-form` device-config cap's flat form patch to whichever
|
|
146
|
+
* mutation the bound provider actually exposes — inspecting the
|
|
147
|
+
* provider's method surface, not the cap name. `setSettings` fires once
|
|
148
|
+
* with the parsed flat patch; `setProfile` fans the patch out per-profile
|
|
149
|
+
* via the registered builderId reducer.
|
|
150
|
+
*/
|
|
151
|
+
export declare function dispatchDerivedFormSave(builderId: string, deviceId: number, patch: Record<string, unknown>, dcProvider: DerivedFormSaveSurface, capName: string): Promise<void>;
|
|
125
152
|
export declare function updateDeviceField(deps: AggregationDeps, input: {
|
|
126
153
|
deviceId: number;
|
|
127
154
|
writerCapName: string;
|
|
@@ -17,16 +17,28 @@ export interface DerivedContributionShape {
|
|
|
17
17
|
fields: readonly unknown[];
|
|
18
18
|
}>;
|
|
19
19
|
}
|
|
20
|
-
/** A per-profile mutation callback the cap's provider supplies. */
|
|
20
|
+
/** A per-profile mutation callback the cap's provider supplies (profile-based caps only). */
|
|
21
21
|
export type ProfileSetter = (profile: StreamProfile, patch: StreamProfilePatch) => Promise<void>;
|
|
22
|
-
/**
|
|
22
|
+
/** Registered builderIds today. New device-config caps add their own reducer below. */
|
|
23
23
|
export declare const STREAM_PARAMS_BUILDER_ID: "stream-params";
|
|
24
|
+
export declare const DAY_NIGHT_BUILDER_ID: "day-night";
|
|
25
|
+
export declare const IMAGE_SETTINGS_BUILDER_ID: "image-settings";
|
|
24
26
|
/**
|
|
25
27
|
* Build the device-detail form section for a `derived-form` device-config
|
|
26
28
|
* cap. Returns null when the camera exposes no configurable property.
|
|
27
29
|
*/
|
|
28
30
|
export declare function deriveFormContribution(builderId: string, options: unknown, status: unknown): DerivedContributionShape | null;
|
|
29
31
|
/**
|
|
30
|
-
* Route a flat form patch back through
|
|
32
|
+
* Route a flat form patch back through a profile-based cap's per-profile
|
|
33
|
+
* `setProfile` mutation (`stream-params`). Throws if `builderId` isn't
|
|
34
|
+
* registered as a profile-based reducer — callers dispatch here only
|
|
35
|
+
* after confirming the bound provider exposes `setProfile`.
|
|
31
36
|
*/
|
|
32
|
-
export declare function
|
|
37
|
+
export declare function applyDerivedFormProfilePatch(builderId: string, patch: Record<string, unknown>, setProfile: ProfileSetter): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Parse a flat form patch into the settings payload for a `setSettings`-
|
|
40
|
+
* based cap (`day-night`, `image-settings`). The caller forwards the
|
|
41
|
+
* result verbatim to `dcProvider.setSettings({ deviceId, settings })`.
|
|
42
|
+
* Throws if `builderId` isn't registered as a settings-based reducer.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseDerivedFormSettingsPatch(builderId: string, patch: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -7,13 +7,361 @@ let _camstack_types_node = require("@camstack/types/node");
|
|
|
7
7
|
let _camstack_types = require("@camstack/types");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
9
9
|
let zod = require("zod");
|
|
10
|
+
//#region src/builtins/device-manager/day-night-config-schema.ts
|
|
11
|
+
var MODE_LABELS = {
|
|
12
|
+
auto: "Auto",
|
|
13
|
+
day: "Day",
|
|
14
|
+
night: "Night",
|
|
15
|
+
schedule: "Schedule"
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Build the `day-night` `ConfigUISchema` from the camera-probed options +
|
|
19
|
+
* current status. Returns `null` when the camera exposes no configurable
|
|
20
|
+
* property — the cap then reports "no schema" and the renderer shows the
|
|
21
|
+
* unsupported-camera message.
|
|
22
|
+
*/
|
|
23
|
+
function buildDayNightConfigSchema(options, status) {
|
|
24
|
+
const fields = [];
|
|
25
|
+
if (options.modes.length > 0) fields.push({
|
|
26
|
+
type: "select",
|
|
27
|
+
key: "dayNight_mode",
|
|
28
|
+
label: "Mode",
|
|
29
|
+
options: options.modes.map((m) => ({
|
|
30
|
+
value: m,
|
|
31
|
+
label: MODE_LABELS[m]
|
|
32
|
+
})),
|
|
33
|
+
default: status?.mode ?? options.modes[0]
|
|
34
|
+
});
|
|
35
|
+
if (options.supportsSensitivity && options.sensitivity) fields.push({
|
|
36
|
+
type: "slider",
|
|
37
|
+
key: "dayNight_sensitivity",
|
|
38
|
+
label: "IR-cut sensitivity",
|
|
39
|
+
min: options.sensitivity.min,
|
|
40
|
+
max: options.sensitivity.max,
|
|
41
|
+
step: options.sensitivity.step,
|
|
42
|
+
showValue: true,
|
|
43
|
+
default: status?.sensitivity ?? options.sensitivity.min
|
|
44
|
+
});
|
|
45
|
+
if (options.supportsSwitchDelay && options.switchDelaySec) fields.push({
|
|
46
|
+
type: "number",
|
|
47
|
+
key: "dayNight_switchDelaySec",
|
|
48
|
+
label: "Switch delay",
|
|
49
|
+
unit: "s",
|
|
50
|
+
min: options.switchDelaySec.min,
|
|
51
|
+
max: options.switchDelaySec.max,
|
|
52
|
+
step: options.switchDelaySec.step,
|
|
53
|
+
default: status?.switchDelaySec ?? options.switchDelaySec.min
|
|
54
|
+
});
|
|
55
|
+
if (fields.length === 0) return null;
|
|
56
|
+
return { sections: [{
|
|
57
|
+
id: "day-night",
|
|
58
|
+
tab: "image",
|
|
59
|
+
title: "Day / Night",
|
|
60
|
+
description: "IR-cut switching mode and the photocell knobs that gate it.",
|
|
61
|
+
columns: 2,
|
|
62
|
+
fields
|
|
63
|
+
}] };
|
|
64
|
+
}
|
|
65
|
+
var DAY_NIGHT_MODES = new Set([
|
|
66
|
+
"auto",
|
|
67
|
+
"day",
|
|
68
|
+
"night",
|
|
69
|
+
"schedule"
|
|
70
|
+
]);
|
|
71
|
+
function isDayNightMode(value) {
|
|
72
|
+
return typeof value === "string" && DAY_NIGHT_MODES.has(value);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Re-parse a flat `ConfigFormBuilder` patch into a `DayNightSettingsPatch`.
|
|
76
|
+
* Returns an empty object when no `dayNight_*` field changed — the caller
|
|
77
|
+
* (`parseDerivedFormSettingsPatch`) forwards the result verbatim to the
|
|
78
|
+
* cap's `setSettings` mutation, which itself ignores fields it doesn't
|
|
79
|
+
* support.
|
|
80
|
+
*/
|
|
81
|
+
function parseDayNightFormPatch(patch) {
|
|
82
|
+
const out = {};
|
|
83
|
+
if ("dayNight_mode" in patch && isDayNightMode(patch.dayNight_mode)) out.mode = patch.dayNight_mode;
|
|
84
|
+
if ("dayNight_sensitivity" in patch) {
|
|
85
|
+
const value = Number(patch.dayNight_sensitivity);
|
|
86
|
+
if (Number.isFinite(value)) out.sensitivity = value;
|
|
87
|
+
}
|
|
88
|
+
if ("dayNight_switchDelaySec" in patch) {
|
|
89
|
+
const value = Number(patch.dayNight_switchDelaySec);
|
|
90
|
+
if (Number.isFinite(value)) out.switchDelaySec = value;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/builtins/device-manager/image-settings-config-schema.ts
|
|
96
|
+
var ROTATE_LABELS = {
|
|
97
|
+
"0": "0°",
|
|
98
|
+
"90": "90°",
|
|
99
|
+
"180": "180°",
|
|
100
|
+
"270": "270°"
|
|
101
|
+
};
|
|
102
|
+
var WHITE_BALANCE_LABELS = {
|
|
103
|
+
auto: "Auto",
|
|
104
|
+
manual: "Manual"
|
|
105
|
+
};
|
|
106
|
+
var EXPOSURE_LABELS = {
|
|
107
|
+
auto: "Auto",
|
|
108
|
+
manual: "Manual"
|
|
109
|
+
};
|
|
110
|
+
var BACKLIGHT_LABELS = {
|
|
111
|
+
off: "Off",
|
|
112
|
+
blc: "Backlight compensation",
|
|
113
|
+
wdr: "Wide dynamic range",
|
|
114
|
+
hlc: "Highlight compensation"
|
|
115
|
+
};
|
|
116
|
+
var ROTATE_VALUES = new Set([
|
|
117
|
+
"0",
|
|
118
|
+
"90",
|
|
119
|
+
"180",
|
|
120
|
+
"270"
|
|
121
|
+
]);
|
|
122
|
+
var WHITE_BALANCE_VALUES = new Set(["auto", "manual"]);
|
|
123
|
+
var EXPOSURE_VALUES = new Set(["auto", "manual"]);
|
|
124
|
+
var BACKLIGHT_VALUES = new Set([
|
|
125
|
+
"off",
|
|
126
|
+
"blc",
|
|
127
|
+
"wdr",
|
|
128
|
+
"hlc"
|
|
129
|
+
]);
|
|
130
|
+
function isImageRotate(value) {
|
|
131
|
+
return typeof value === "string" && ROTATE_VALUES.has(value);
|
|
132
|
+
}
|
|
133
|
+
function isWhiteBalanceMode(value) {
|
|
134
|
+
return typeof value === "string" && WHITE_BALANCE_VALUES.has(value);
|
|
135
|
+
}
|
|
136
|
+
function isExposureMode(value) {
|
|
137
|
+
return typeof value === "string" && EXPOSURE_VALUES.has(value);
|
|
138
|
+
}
|
|
139
|
+
function isBacklightMode(value) {
|
|
140
|
+
return typeof value === "string" && BACKLIGHT_VALUES.has(value);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Build the `image-settings` `ConfigUISchema` from the camera-probed
|
|
144
|
+
* options + current status. Returns `null` when the camera exposes no
|
|
145
|
+
* configurable property.
|
|
146
|
+
*/
|
|
147
|
+
function buildImageSettingsConfigSchema(options, status) {
|
|
148
|
+
const fields = [];
|
|
149
|
+
if (options.supportsBrightness && options.brightness) fields.push({
|
|
150
|
+
type: "slider",
|
|
151
|
+
key: "imageSettings_brightness",
|
|
152
|
+
label: "Brightness",
|
|
153
|
+
min: options.brightness.min,
|
|
154
|
+
max: options.brightness.max,
|
|
155
|
+
step: options.brightness.step,
|
|
156
|
+
showValue: true,
|
|
157
|
+
default: status?.brightness ?? options.brightness.min
|
|
158
|
+
});
|
|
159
|
+
if (options.supportsContrast && options.contrast) fields.push({
|
|
160
|
+
type: "slider",
|
|
161
|
+
key: "imageSettings_contrast",
|
|
162
|
+
label: "Contrast",
|
|
163
|
+
min: options.contrast.min,
|
|
164
|
+
max: options.contrast.max,
|
|
165
|
+
step: options.contrast.step,
|
|
166
|
+
showValue: true,
|
|
167
|
+
default: status?.contrast ?? options.contrast.min
|
|
168
|
+
});
|
|
169
|
+
if (options.supportsSaturation && options.saturation) fields.push({
|
|
170
|
+
type: "slider",
|
|
171
|
+
key: "imageSettings_saturation",
|
|
172
|
+
label: "Saturation",
|
|
173
|
+
min: options.saturation.min,
|
|
174
|
+
max: options.saturation.max,
|
|
175
|
+
step: options.saturation.step,
|
|
176
|
+
showValue: true,
|
|
177
|
+
default: status?.saturation ?? options.saturation.min
|
|
178
|
+
});
|
|
179
|
+
if (options.supportsSharpness && options.sharpness) fields.push({
|
|
180
|
+
type: "slider",
|
|
181
|
+
key: "imageSettings_sharpness",
|
|
182
|
+
label: "Sharpness",
|
|
183
|
+
min: options.sharpness.min,
|
|
184
|
+
max: options.sharpness.max,
|
|
185
|
+
step: options.sharpness.step,
|
|
186
|
+
showValue: true,
|
|
187
|
+
default: status?.sharpness ?? options.sharpness.min
|
|
188
|
+
});
|
|
189
|
+
if (options.supportsMirror) fields.push({
|
|
190
|
+
type: "boolean",
|
|
191
|
+
key: "imageSettings_mirror",
|
|
192
|
+
label: "Mirror",
|
|
193
|
+
style: "switch",
|
|
194
|
+
default: status?.mirror ?? false
|
|
195
|
+
});
|
|
196
|
+
if (options.supportsFlip) fields.push({
|
|
197
|
+
type: "boolean",
|
|
198
|
+
key: "imageSettings_flip",
|
|
199
|
+
label: "Flip",
|
|
200
|
+
style: "switch",
|
|
201
|
+
default: status?.flip ?? false
|
|
202
|
+
});
|
|
203
|
+
if (options.rotateOptions.length > 0) fields.push({
|
|
204
|
+
type: "select",
|
|
205
|
+
key: "imageSettings_rotate",
|
|
206
|
+
label: "Rotation",
|
|
207
|
+
options: options.rotateOptions.map((r) => ({
|
|
208
|
+
value: r,
|
|
209
|
+
label: ROTATE_LABELS[r]
|
|
210
|
+
})),
|
|
211
|
+
default: status?.rotate ?? options.rotateOptions[0]
|
|
212
|
+
});
|
|
213
|
+
if (options.whiteBalanceModes.length > 0) fields.push({
|
|
214
|
+
type: "select",
|
|
215
|
+
key: "imageSettings_whiteBalance",
|
|
216
|
+
label: "White balance",
|
|
217
|
+
options: options.whiteBalanceModes.map((m) => ({
|
|
218
|
+
value: m,
|
|
219
|
+
label: WHITE_BALANCE_LABELS[m]
|
|
220
|
+
})),
|
|
221
|
+
default: status?.whiteBalance ?? options.whiteBalanceModes[0]
|
|
222
|
+
});
|
|
223
|
+
if (options.supportsWarmth && options.warmth) fields.push({
|
|
224
|
+
type: "slider",
|
|
225
|
+
key: "imageSettings_warmth",
|
|
226
|
+
label: "Warmth",
|
|
227
|
+
min: options.warmth.min,
|
|
228
|
+
max: options.warmth.max,
|
|
229
|
+
step: options.warmth.step,
|
|
230
|
+
showValue: true,
|
|
231
|
+
default: status?.warmth ?? options.warmth.min,
|
|
232
|
+
showWhen: {
|
|
233
|
+
field: "imageSettings_whiteBalance",
|
|
234
|
+
equals: "manual"
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
if (options.exposureModes.length > 0) fields.push({
|
|
238
|
+
type: "select",
|
|
239
|
+
key: "imageSettings_exposureMode",
|
|
240
|
+
label: "Exposure mode",
|
|
241
|
+
options: options.exposureModes.map((m) => ({
|
|
242
|
+
value: m,
|
|
243
|
+
label: EXPOSURE_LABELS[m]
|
|
244
|
+
})),
|
|
245
|
+
default: status?.exposureMode ?? options.exposureModes[0]
|
|
246
|
+
});
|
|
247
|
+
if (options.backlightModes.length > 0) fields.push({
|
|
248
|
+
type: "select",
|
|
249
|
+
key: "imageSettings_backlightMode",
|
|
250
|
+
label: "Backlight compensation",
|
|
251
|
+
options: options.backlightModes.map((m) => ({
|
|
252
|
+
value: m,
|
|
253
|
+
label: BACKLIGHT_LABELS[m]
|
|
254
|
+
})),
|
|
255
|
+
default: status?.backlightMode ?? options.backlightModes[0]
|
|
256
|
+
});
|
|
257
|
+
if (fields.length === 0) return null;
|
|
258
|
+
return { sections: [{
|
|
259
|
+
id: "image-settings",
|
|
260
|
+
tab: "image",
|
|
261
|
+
title: "Image adjustment",
|
|
262
|
+
description: "Picture sliders, orientation and exposure. Option lists are read live from the camera — fields the firmware doesn't expose are hidden.",
|
|
263
|
+
columns: 2,
|
|
264
|
+
fields
|
|
265
|
+
}] };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Re-parse a flat `ConfigFormBuilder` patch into an `ImageSettingsPatch`.
|
|
269
|
+
* Returns an empty object when no `imageSettings_*` field changed.
|
|
270
|
+
*/
|
|
271
|
+
function parseImageSettingsFormPatch(patch) {
|
|
272
|
+
const out = {};
|
|
273
|
+
if ("imageSettings_brightness" in patch) {
|
|
274
|
+
const value = Number(patch.imageSettings_brightness);
|
|
275
|
+
if (Number.isFinite(value)) out.brightness = value;
|
|
276
|
+
}
|
|
277
|
+
if ("imageSettings_contrast" in patch) {
|
|
278
|
+
const value = Number(patch.imageSettings_contrast);
|
|
279
|
+
if (Number.isFinite(value)) out.contrast = value;
|
|
280
|
+
}
|
|
281
|
+
if ("imageSettings_saturation" in patch) {
|
|
282
|
+
const value = Number(patch.imageSettings_saturation);
|
|
283
|
+
if (Number.isFinite(value)) out.saturation = value;
|
|
284
|
+
}
|
|
285
|
+
if ("imageSettings_sharpness" in patch) {
|
|
286
|
+
const value = Number(patch.imageSettings_sharpness);
|
|
287
|
+
if (Number.isFinite(value)) out.sharpness = value;
|
|
288
|
+
}
|
|
289
|
+
if ("imageSettings_mirror" in patch && typeof patch.imageSettings_mirror === "boolean") out.mirror = patch.imageSettings_mirror;
|
|
290
|
+
if ("imageSettings_flip" in patch && typeof patch.imageSettings_flip === "boolean") out.flip = patch.imageSettings_flip;
|
|
291
|
+
if ("imageSettings_rotate" in patch && isImageRotate(patch.imageSettings_rotate)) out.rotate = patch.imageSettings_rotate;
|
|
292
|
+
if ("imageSettings_whiteBalance" in patch && isWhiteBalanceMode(patch.imageSettings_whiteBalance)) out.whiteBalance = patch.imageSettings_whiteBalance;
|
|
293
|
+
if ("imageSettings_warmth" in patch) {
|
|
294
|
+
const value = Number(patch.imageSettings_warmth);
|
|
295
|
+
if (Number.isFinite(value)) out.warmth = value;
|
|
296
|
+
}
|
|
297
|
+
if ("imageSettings_exposureMode" in patch && isExposureMode(patch.imageSettings_exposureMode)) out.exposureMode = patch.imageSettings_exposureMode;
|
|
298
|
+
if ("imageSettings_backlightMode" in patch && isBacklightMode(patch.imageSettings_backlightMode)) out.backlightMode = patch.imageSettings_backlightMode;
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/builtins/device-manager/device-config-contribution.ts
|
|
303
|
+
/**
|
|
304
|
+
* D14 device-config archetype — framework-side contribution derivation.
|
|
305
|
+
*
|
|
306
|
+
* A `deviceConfig` cap with `ui.kind: 'derived-form'` names a `builderId`.
|
|
307
|
+
* This module owns the registry of `builderId → reducer` pure functions
|
|
308
|
+
* that build the UI section from the cap's `getOptions` + `getStatus`
|
|
309
|
+
* output and route a flat form patch back through the cap's `set*`
|
|
310
|
+
* mutation. No per-vendor UI code: reolink and hikvision produce the same
|
|
311
|
+
* section from the same inputs.
|
|
312
|
+
*
|
|
313
|
+
* Two reducer shapes exist, one per save-path surface a `derived-form` cap
|
|
314
|
+
* can expose:
|
|
315
|
+
* - `kind: 'profile'` — the cap mutates one of several named profiles
|
|
316
|
+
* via `setProfile({ deviceId, profile, patch })` (`stream-params`).
|
|
317
|
+
* - `kind: 'settings'` — the cap has no profile axis and mutates via a
|
|
318
|
+
* single `setSettings({ deviceId, settings })` (`day-night`,
|
|
319
|
+
* `image-settings`).
|
|
320
|
+
*
|
|
321
|
+
* `device-aggregation.ts` picks which apply function to call by
|
|
322
|
+
* inspecting the bound provider's actual method surface (`setProfile` vs
|
|
323
|
+
* `setSettings`), never by branching on the cap name.
|
|
324
|
+
*/
|
|
325
|
+
/** Registered builderIds today. New device-config caps add their own reducer below. */
|
|
326
|
+
var STREAM_PARAMS_BUILDER_ID = "stream-params";
|
|
327
|
+
var DAY_NIGHT_BUILDER_ID = "day-night";
|
|
328
|
+
var IMAGE_SETTINGS_BUILDER_ID = "image-settings";
|
|
329
|
+
var STREAM_PARAMS_REDUCER = {
|
|
330
|
+
kind: "profile",
|
|
331
|
+
buildSchema: (options, status) => (0, _camstack_types.buildStreamParamsConfigSchema)(options, status ?? null),
|
|
332
|
+
applyPatch: async (patch, setProfile) => {
|
|
333
|
+
for (const meta of _camstack_types.STREAM_PROFILE_META) {
|
|
334
|
+
const profilePatch = (0, _camstack_types.parseStreamParamsFormPatch)(patch, meta.prefix);
|
|
335
|
+
if (profilePatch) await setProfile(meta.profile, profilePatch);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
var DAY_NIGHT_REDUCER = {
|
|
340
|
+
kind: "settings",
|
|
341
|
+
buildSchema: (options, status) => buildDayNightConfigSchema(options, status ?? null),
|
|
342
|
+
parsePatch: (patch) => parseDayNightFormPatch(patch)
|
|
343
|
+
};
|
|
344
|
+
var IMAGE_SETTINGS_REDUCER = {
|
|
345
|
+
kind: "settings",
|
|
346
|
+
buildSchema: (options, status) => buildImageSettingsConfigSchema(options, status ?? null),
|
|
347
|
+
parsePatch: (patch) => parseImageSettingsFormPatch(patch)
|
|
348
|
+
};
|
|
349
|
+
var BUILDER_REDUCERS = {
|
|
350
|
+
[STREAM_PARAMS_BUILDER_ID]: STREAM_PARAMS_REDUCER,
|
|
351
|
+
[DAY_NIGHT_BUILDER_ID]: DAY_NIGHT_REDUCER,
|
|
352
|
+
[IMAGE_SETTINGS_BUILDER_ID]: IMAGE_SETTINGS_REDUCER
|
|
353
|
+
};
|
|
354
|
+
function resolveReducer(builderId) {
|
|
355
|
+
const reducer = BUILDER_REDUCERS[builderId];
|
|
356
|
+
if (!reducer) throw new Error(`device-config: unknown derived-form builderId "${builderId}"`);
|
|
357
|
+
return reducer;
|
|
358
|
+
}
|
|
10
359
|
/**
|
|
11
360
|
* Build the device-detail form section for a `derived-form` device-config
|
|
12
361
|
* cap. Returns null when the camera exposes no configurable property.
|
|
13
362
|
*/
|
|
14
363
|
function deriveFormContribution(builderId, options, status) {
|
|
15
|
-
|
|
16
|
-
const schema = (0, _camstack_types.buildStreamParamsConfigSchema)(options, status ?? null);
|
|
364
|
+
const schema = resolveReducer(builderId).buildSchema(options, status);
|
|
17
365
|
if (!schema) return null;
|
|
18
366
|
return { sections: schema.sections.map((s) => ({
|
|
19
367
|
id: s.id,
|
|
@@ -26,14 +374,26 @@ function deriveFormContribution(builderId, options, status) {
|
|
|
26
374
|
})) };
|
|
27
375
|
}
|
|
28
376
|
/**
|
|
29
|
-
* Route a flat form patch back through
|
|
377
|
+
* Route a flat form patch back through a profile-based cap's per-profile
|
|
378
|
+
* `setProfile` mutation (`stream-params`). Throws if `builderId` isn't
|
|
379
|
+
* registered as a profile-based reducer — callers dispatch here only
|
|
380
|
+
* after confirming the bound provider exposes `setProfile`.
|
|
30
381
|
*/
|
|
31
|
-
async function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
382
|
+
async function applyDerivedFormProfilePatch(builderId, patch, setProfile) {
|
|
383
|
+
const reducer = resolveReducer(builderId);
|
|
384
|
+
if (reducer.kind !== "profile") throw new Error(`device-config: builderId "${builderId}" is not a profile-based derived-form`);
|
|
385
|
+
await reducer.applyPatch(patch, setProfile);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Parse a flat form patch into the settings payload for a `setSettings`-
|
|
389
|
+
* based cap (`day-night`, `image-settings`). The caller forwards the
|
|
390
|
+
* result verbatim to `dcProvider.setSettings({ deviceId, settings })`.
|
|
391
|
+
* Throws if `builderId` isn't registered as a settings-based reducer.
|
|
392
|
+
*/
|
|
393
|
+
function parseDerivedFormSettingsPatch(builderId, patch) {
|
|
394
|
+
const reducer = resolveReducer(builderId);
|
|
395
|
+
if (reducer.kind !== "settings") throw new Error(`device-config: builderId "${builderId}" is not a settings-based derived-form`);
|
|
396
|
+
return reducer.parsePatch(patch);
|
|
37
397
|
}
|
|
38
398
|
//#endregion
|
|
39
399
|
//#region src/builtins/device-manager/device-aggregation-merge.ts
|
|
@@ -985,6 +1345,33 @@ async function resolveDriverConfigSchema(deps, deviceId, hubLocal) {
|
|
|
985
1345
|
});
|
|
986
1346
|
return { status: "unavailable" };
|
|
987
1347
|
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Route a `derived-form` device-config cap's flat form patch to whichever
|
|
1350
|
+
* mutation the bound provider actually exposes — inspecting the
|
|
1351
|
+
* provider's method surface, not the cap name. `setSettings` fires once
|
|
1352
|
+
* with the parsed flat patch; `setProfile` fans the patch out per-profile
|
|
1353
|
+
* via the registered builderId reducer.
|
|
1354
|
+
*/
|
|
1355
|
+
async function dispatchDerivedFormSave(builderId, deviceId, patch, dcProvider, capName) {
|
|
1356
|
+
if (typeof dcProvider.setSettings === "function") {
|
|
1357
|
+
const settings = parseDerivedFormSettingsPatch(builderId, patch);
|
|
1358
|
+
await dcProvider.setSettings({
|
|
1359
|
+
deviceId,
|
|
1360
|
+
settings
|
|
1361
|
+
});
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (typeof dcProvider.setProfile === "function") {
|
|
1365
|
+
const setProfile = dcProvider.setProfile;
|
|
1366
|
+
await applyDerivedFormProfilePatch(builderId, patch, (profile, profilePatch) => setProfile({
|
|
1367
|
+
deviceId,
|
|
1368
|
+
profile,
|
|
1369
|
+
patch: profilePatch
|
|
1370
|
+
}));
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
throw new Error(`[device-manager] device-config cap "${capName}" provider exposes neither setSettings nor setProfile`);
|
|
1374
|
+
}
|
|
988
1375
|
async function updateDeviceField(deps, input) {
|
|
989
1376
|
if (input.writerCapName === "device-manager") {
|
|
990
1377
|
const hubRegistry = deps.ctx.kernel?.deviceRegistry;
|
|
@@ -1008,11 +1395,7 @@ async function updateDeviceField(deps, input) {
|
|
|
1008
1395
|
if (def.deviceConfig.ui.kind === "widget") return { success: true };
|
|
1009
1396
|
const dcProvider = registry.getProviderForDevice(input.writerCapName, input.deviceId);
|
|
1010
1397
|
if (!dcProvider) throw new Error(`[device-manager] no provider for device-config cap "${input.writerCapName}" on device ${input.deviceId}`);
|
|
1011
|
-
await
|
|
1012
|
-
deviceId: input.deviceId,
|
|
1013
|
-
profile,
|
|
1014
|
-
patch
|
|
1015
|
-
}));
|
|
1398
|
+
await dispatchDerivedFormSave(def.deviceConfig.ui.builderId, input.deviceId, { [input.key]: input.value }, dcProvider, input.writerCapName);
|
|
1016
1399
|
return { success: true };
|
|
1017
1400
|
}
|
|
1018
1401
|
if (!def?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${input.writerCapName}" does not expose device settings`);
|
|
@@ -1131,11 +1514,7 @@ async function applyGroupPatch(deps, deviceId, group) {
|
|
|
1131
1514
|
if (def.deviceConfig.ui.kind === "widget") return;
|
|
1132
1515
|
const dcProvider = registry.getProviderForDevice(group.writerCapName, deviceId);
|
|
1133
1516
|
if (!dcProvider) throw new Error(`[device-manager] no provider for device-config cap "${group.writerCapName}" on device ${deviceId}`);
|
|
1134
|
-
await
|
|
1135
|
-
deviceId,
|
|
1136
|
-
profile,
|
|
1137
|
-
patch
|
|
1138
|
-
}));
|
|
1517
|
+
await dispatchDerivedFormSave(def.deviceConfig.ui.builderId, deviceId, group.patch, dcProvider, group.writerCapName);
|
|
1139
1518
|
return;
|
|
1140
1519
|
}
|
|
1141
1520
|
if (!def?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${group.writerCapName}" does not expose device settings`);
|
|
@@ -2,13 +2,361 @@ import { canonicalDeviceFingerprint } from "@camstack/types/node";
|
|
|
2
2
|
import { ALL_CAPABILITY_DEFINITIONS, BaseAddon, CAP_NAMES_WITH_STATUS, DeviceFeature, DeviceRole, DeviceStatusSchema, DeviceType, EventCategory, STREAM_PROFILE_META, WELL_KNOWN_TAB_MAP, applyTransform, buildStreamParamsConfigSchema, deviceManagerCapability, deviceStateCapability, deviceStatusCapability, enumerateItemArrayFields, enumerateSchemaFields, errMsg, evaluateLinkExpression, getByPath, isDeviceConfigCap, normalizeUnit, parseStreamParamsFormPatch, setByPath, sleep, toExpressionValue, validateExpressionSource } from "@camstack/types";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
//#region src/builtins/device-manager/day-night-config-schema.ts
|
|
6
|
+
var MODE_LABELS = {
|
|
7
|
+
auto: "Auto",
|
|
8
|
+
day: "Day",
|
|
9
|
+
night: "Night",
|
|
10
|
+
schedule: "Schedule"
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Build the `day-night` `ConfigUISchema` from the camera-probed options +
|
|
14
|
+
* current status. Returns `null` when the camera exposes no configurable
|
|
15
|
+
* property — the cap then reports "no schema" and the renderer shows the
|
|
16
|
+
* unsupported-camera message.
|
|
17
|
+
*/
|
|
18
|
+
function buildDayNightConfigSchema(options, status) {
|
|
19
|
+
const fields = [];
|
|
20
|
+
if (options.modes.length > 0) fields.push({
|
|
21
|
+
type: "select",
|
|
22
|
+
key: "dayNight_mode",
|
|
23
|
+
label: "Mode",
|
|
24
|
+
options: options.modes.map((m) => ({
|
|
25
|
+
value: m,
|
|
26
|
+
label: MODE_LABELS[m]
|
|
27
|
+
})),
|
|
28
|
+
default: status?.mode ?? options.modes[0]
|
|
29
|
+
});
|
|
30
|
+
if (options.supportsSensitivity && options.sensitivity) fields.push({
|
|
31
|
+
type: "slider",
|
|
32
|
+
key: "dayNight_sensitivity",
|
|
33
|
+
label: "IR-cut sensitivity",
|
|
34
|
+
min: options.sensitivity.min,
|
|
35
|
+
max: options.sensitivity.max,
|
|
36
|
+
step: options.sensitivity.step,
|
|
37
|
+
showValue: true,
|
|
38
|
+
default: status?.sensitivity ?? options.sensitivity.min
|
|
39
|
+
});
|
|
40
|
+
if (options.supportsSwitchDelay && options.switchDelaySec) fields.push({
|
|
41
|
+
type: "number",
|
|
42
|
+
key: "dayNight_switchDelaySec",
|
|
43
|
+
label: "Switch delay",
|
|
44
|
+
unit: "s",
|
|
45
|
+
min: options.switchDelaySec.min,
|
|
46
|
+
max: options.switchDelaySec.max,
|
|
47
|
+
step: options.switchDelaySec.step,
|
|
48
|
+
default: status?.switchDelaySec ?? options.switchDelaySec.min
|
|
49
|
+
});
|
|
50
|
+
if (fields.length === 0) return null;
|
|
51
|
+
return { sections: [{
|
|
52
|
+
id: "day-night",
|
|
53
|
+
tab: "image",
|
|
54
|
+
title: "Day / Night",
|
|
55
|
+
description: "IR-cut switching mode and the photocell knobs that gate it.",
|
|
56
|
+
columns: 2,
|
|
57
|
+
fields
|
|
58
|
+
}] };
|
|
59
|
+
}
|
|
60
|
+
var DAY_NIGHT_MODES = new Set([
|
|
61
|
+
"auto",
|
|
62
|
+
"day",
|
|
63
|
+
"night",
|
|
64
|
+
"schedule"
|
|
65
|
+
]);
|
|
66
|
+
function isDayNightMode(value) {
|
|
67
|
+
return typeof value === "string" && DAY_NIGHT_MODES.has(value);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Re-parse a flat `ConfigFormBuilder` patch into a `DayNightSettingsPatch`.
|
|
71
|
+
* Returns an empty object when no `dayNight_*` field changed — the caller
|
|
72
|
+
* (`parseDerivedFormSettingsPatch`) forwards the result verbatim to the
|
|
73
|
+
* cap's `setSettings` mutation, which itself ignores fields it doesn't
|
|
74
|
+
* support.
|
|
75
|
+
*/
|
|
76
|
+
function parseDayNightFormPatch(patch) {
|
|
77
|
+
const out = {};
|
|
78
|
+
if ("dayNight_mode" in patch && isDayNightMode(patch.dayNight_mode)) out.mode = patch.dayNight_mode;
|
|
79
|
+
if ("dayNight_sensitivity" in patch) {
|
|
80
|
+
const value = Number(patch.dayNight_sensitivity);
|
|
81
|
+
if (Number.isFinite(value)) out.sensitivity = value;
|
|
82
|
+
}
|
|
83
|
+
if ("dayNight_switchDelaySec" in patch) {
|
|
84
|
+
const value = Number(patch.dayNight_switchDelaySec);
|
|
85
|
+
if (Number.isFinite(value)) out.switchDelaySec = value;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/builtins/device-manager/image-settings-config-schema.ts
|
|
91
|
+
var ROTATE_LABELS = {
|
|
92
|
+
"0": "0°",
|
|
93
|
+
"90": "90°",
|
|
94
|
+
"180": "180°",
|
|
95
|
+
"270": "270°"
|
|
96
|
+
};
|
|
97
|
+
var WHITE_BALANCE_LABELS = {
|
|
98
|
+
auto: "Auto",
|
|
99
|
+
manual: "Manual"
|
|
100
|
+
};
|
|
101
|
+
var EXPOSURE_LABELS = {
|
|
102
|
+
auto: "Auto",
|
|
103
|
+
manual: "Manual"
|
|
104
|
+
};
|
|
105
|
+
var BACKLIGHT_LABELS = {
|
|
106
|
+
off: "Off",
|
|
107
|
+
blc: "Backlight compensation",
|
|
108
|
+
wdr: "Wide dynamic range",
|
|
109
|
+
hlc: "Highlight compensation"
|
|
110
|
+
};
|
|
111
|
+
var ROTATE_VALUES = new Set([
|
|
112
|
+
"0",
|
|
113
|
+
"90",
|
|
114
|
+
"180",
|
|
115
|
+
"270"
|
|
116
|
+
]);
|
|
117
|
+
var WHITE_BALANCE_VALUES = new Set(["auto", "manual"]);
|
|
118
|
+
var EXPOSURE_VALUES = new Set(["auto", "manual"]);
|
|
119
|
+
var BACKLIGHT_VALUES = new Set([
|
|
120
|
+
"off",
|
|
121
|
+
"blc",
|
|
122
|
+
"wdr",
|
|
123
|
+
"hlc"
|
|
124
|
+
]);
|
|
125
|
+
function isImageRotate(value) {
|
|
126
|
+
return typeof value === "string" && ROTATE_VALUES.has(value);
|
|
127
|
+
}
|
|
128
|
+
function isWhiteBalanceMode(value) {
|
|
129
|
+
return typeof value === "string" && WHITE_BALANCE_VALUES.has(value);
|
|
130
|
+
}
|
|
131
|
+
function isExposureMode(value) {
|
|
132
|
+
return typeof value === "string" && EXPOSURE_VALUES.has(value);
|
|
133
|
+
}
|
|
134
|
+
function isBacklightMode(value) {
|
|
135
|
+
return typeof value === "string" && BACKLIGHT_VALUES.has(value);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Build the `image-settings` `ConfigUISchema` from the camera-probed
|
|
139
|
+
* options + current status. Returns `null` when the camera exposes no
|
|
140
|
+
* configurable property.
|
|
141
|
+
*/
|
|
142
|
+
function buildImageSettingsConfigSchema(options, status) {
|
|
143
|
+
const fields = [];
|
|
144
|
+
if (options.supportsBrightness && options.brightness) fields.push({
|
|
145
|
+
type: "slider",
|
|
146
|
+
key: "imageSettings_brightness",
|
|
147
|
+
label: "Brightness",
|
|
148
|
+
min: options.brightness.min,
|
|
149
|
+
max: options.brightness.max,
|
|
150
|
+
step: options.brightness.step,
|
|
151
|
+
showValue: true,
|
|
152
|
+
default: status?.brightness ?? options.brightness.min
|
|
153
|
+
});
|
|
154
|
+
if (options.supportsContrast && options.contrast) fields.push({
|
|
155
|
+
type: "slider",
|
|
156
|
+
key: "imageSettings_contrast",
|
|
157
|
+
label: "Contrast",
|
|
158
|
+
min: options.contrast.min,
|
|
159
|
+
max: options.contrast.max,
|
|
160
|
+
step: options.contrast.step,
|
|
161
|
+
showValue: true,
|
|
162
|
+
default: status?.contrast ?? options.contrast.min
|
|
163
|
+
});
|
|
164
|
+
if (options.supportsSaturation && options.saturation) fields.push({
|
|
165
|
+
type: "slider",
|
|
166
|
+
key: "imageSettings_saturation",
|
|
167
|
+
label: "Saturation",
|
|
168
|
+
min: options.saturation.min,
|
|
169
|
+
max: options.saturation.max,
|
|
170
|
+
step: options.saturation.step,
|
|
171
|
+
showValue: true,
|
|
172
|
+
default: status?.saturation ?? options.saturation.min
|
|
173
|
+
});
|
|
174
|
+
if (options.supportsSharpness && options.sharpness) fields.push({
|
|
175
|
+
type: "slider",
|
|
176
|
+
key: "imageSettings_sharpness",
|
|
177
|
+
label: "Sharpness",
|
|
178
|
+
min: options.sharpness.min,
|
|
179
|
+
max: options.sharpness.max,
|
|
180
|
+
step: options.sharpness.step,
|
|
181
|
+
showValue: true,
|
|
182
|
+
default: status?.sharpness ?? options.sharpness.min
|
|
183
|
+
});
|
|
184
|
+
if (options.supportsMirror) fields.push({
|
|
185
|
+
type: "boolean",
|
|
186
|
+
key: "imageSettings_mirror",
|
|
187
|
+
label: "Mirror",
|
|
188
|
+
style: "switch",
|
|
189
|
+
default: status?.mirror ?? false
|
|
190
|
+
});
|
|
191
|
+
if (options.supportsFlip) fields.push({
|
|
192
|
+
type: "boolean",
|
|
193
|
+
key: "imageSettings_flip",
|
|
194
|
+
label: "Flip",
|
|
195
|
+
style: "switch",
|
|
196
|
+
default: status?.flip ?? false
|
|
197
|
+
});
|
|
198
|
+
if (options.rotateOptions.length > 0) fields.push({
|
|
199
|
+
type: "select",
|
|
200
|
+
key: "imageSettings_rotate",
|
|
201
|
+
label: "Rotation",
|
|
202
|
+
options: options.rotateOptions.map((r) => ({
|
|
203
|
+
value: r,
|
|
204
|
+
label: ROTATE_LABELS[r]
|
|
205
|
+
})),
|
|
206
|
+
default: status?.rotate ?? options.rotateOptions[0]
|
|
207
|
+
});
|
|
208
|
+
if (options.whiteBalanceModes.length > 0) fields.push({
|
|
209
|
+
type: "select",
|
|
210
|
+
key: "imageSettings_whiteBalance",
|
|
211
|
+
label: "White balance",
|
|
212
|
+
options: options.whiteBalanceModes.map((m) => ({
|
|
213
|
+
value: m,
|
|
214
|
+
label: WHITE_BALANCE_LABELS[m]
|
|
215
|
+
})),
|
|
216
|
+
default: status?.whiteBalance ?? options.whiteBalanceModes[0]
|
|
217
|
+
});
|
|
218
|
+
if (options.supportsWarmth && options.warmth) fields.push({
|
|
219
|
+
type: "slider",
|
|
220
|
+
key: "imageSettings_warmth",
|
|
221
|
+
label: "Warmth",
|
|
222
|
+
min: options.warmth.min,
|
|
223
|
+
max: options.warmth.max,
|
|
224
|
+
step: options.warmth.step,
|
|
225
|
+
showValue: true,
|
|
226
|
+
default: status?.warmth ?? options.warmth.min,
|
|
227
|
+
showWhen: {
|
|
228
|
+
field: "imageSettings_whiteBalance",
|
|
229
|
+
equals: "manual"
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
if (options.exposureModes.length > 0) fields.push({
|
|
233
|
+
type: "select",
|
|
234
|
+
key: "imageSettings_exposureMode",
|
|
235
|
+
label: "Exposure mode",
|
|
236
|
+
options: options.exposureModes.map((m) => ({
|
|
237
|
+
value: m,
|
|
238
|
+
label: EXPOSURE_LABELS[m]
|
|
239
|
+
})),
|
|
240
|
+
default: status?.exposureMode ?? options.exposureModes[0]
|
|
241
|
+
});
|
|
242
|
+
if (options.backlightModes.length > 0) fields.push({
|
|
243
|
+
type: "select",
|
|
244
|
+
key: "imageSettings_backlightMode",
|
|
245
|
+
label: "Backlight compensation",
|
|
246
|
+
options: options.backlightModes.map((m) => ({
|
|
247
|
+
value: m,
|
|
248
|
+
label: BACKLIGHT_LABELS[m]
|
|
249
|
+
})),
|
|
250
|
+
default: status?.backlightMode ?? options.backlightModes[0]
|
|
251
|
+
});
|
|
252
|
+
if (fields.length === 0) return null;
|
|
253
|
+
return { sections: [{
|
|
254
|
+
id: "image-settings",
|
|
255
|
+
tab: "image",
|
|
256
|
+
title: "Image adjustment",
|
|
257
|
+
description: "Picture sliders, orientation and exposure. Option lists are read live from the camera — fields the firmware doesn't expose are hidden.",
|
|
258
|
+
columns: 2,
|
|
259
|
+
fields
|
|
260
|
+
}] };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Re-parse a flat `ConfigFormBuilder` patch into an `ImageSettingsPatch`.
|
|
264
|
+
* Returns an empty object when no `imageSettings_*` field changed.
|
|
265
|
+
*/
|
|
266
|
+
function parseImageSettingsFormPatch(patch) {
|
|
267
|
+
const out = {};
|
|
268
|
+
if ("imageSettings_brightness" in patch) {
|
|
269
|
+
const value = Number(patch.imageSettings_brightness);
|
|
270
|
+
if (Number.isFinite(value)) out.brightness = value;
|
|
271
|
+
}
|
|
272
|
+
if ("imageSettings_contrast" in patch) {
|
|
273
|
+
const value = Number(patch.imageSettings_contrast);
|
|
274
|
+
if (Number.isFinite(value)) out.contrast = value;
|
|
275
|
+
}
|
|
276
|
+
if ("imageSettings_saturation" in patch) {
|
|
277
|
+
const value = Number(patch.imageSettings_saturation);
|
|
278
|
+
if (Number.isFinite(value)) out.saturation = value;
|
|
279
|
+
}
|
|
280
|
+
if ("imageSettings_sharpness" in patch) {
|
|
281
|
+
const value = Number(patch.imageSettings_sharpness);
|
|
282
|
+
if (Number.isFinite(value)) out.sharpness = value;
|
|
283
|
+
}
|
|
284
|
+
if ("imageSettings_mirror" in patch && typeof patch.imageSettings_mirror === "boolean") out.mirror = patch.imageSettings_mirror;
|
|
285
|
+
if ("imageSettings_flip" in patch && typeof patch.imageSettings_flip === "boolean") out.flip = patch.imageSettings_flip;
|
|
286
|
+
if ("imageSettings_rotate" in patch && isImageRotate(patch.imageSettings_rotate)) out.rotate = patch.imageSettings_rotate;
|
|
287
|
+
if ("imageSettings_whiteBalance" in patch && isWhiteBalanceMode(patch.imageSettings_whiteBalance)) out.whiteBalance = patch.imageSettings_whiteBalance;
|
|
288
|
+
if ("imageSettings_warmth" in patch) {
|
|
289
|
+
const value = Number(patch.imageSettings_warmth);
|
|
290
|
+
if (Number.isFinite(value)) out.warmth = value;
|
|
291
|
+
}
|
|
292
|
+
if ("imageSettings_exposureMode" in patch && isExposureMode(patch.imageSettings_exposureMode)) out.exposureMode = patch.imageSettings_exposureMode;
|
|
293
|
+
if ("imageSettings_backlightMode" in patch && isBacklightMode(patch.imageSettings_backlightMode)) out.backlightMode = patch.imageSettings_backlightMode;
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/builtins/device-manager/device-config-contribution.ts
|
|
298
|
+
/**
|
|
299
|
+
* D14 device-config archetype — framework-side contribution derivation.
|
|
300
|
+
*
|
|
301
|
+
* A `deviceConfig` cap with `ui.kind: 'derived-form'` names a `builderId`.
|
|
302
|
+
* This module owns the registry of `builderId → reducer` pure functions
|
|
303
|
+
* that build the UI section from the cap's `getOptions` + `getStatus`
|
|
304
|
+
* output and route a flat form patch back through the cap's `set*`
|
|
305
|
+
* mutation. No per-vendor UI code: reolink and hikvision produce the same
|
|
306
|
+
* section from the same inputs.
|
|
307
|
+
*
|
|
308
|
+
* Two reducer shapes exist, one per save-path surface a `derived-form` cap
|
|
309
|
+
* can expose:
|
|
310
|
+
* - `kind: 'profile'` — the cap mutates one of several named profiles
|
|
311
|
+
* via `setProfile({ deviceId, profile, patch })` (`stream-params`).
|
|
312
|
+
* - `kind: 'settings'` — the cap has no profile axis and mutates via a
|
|
313
|
+
* single `setSettings({ deviceId, settings })` (`day-night`,
|
|
314
|
+
* `image-settings`).
|
|
315
|
+
*
|
|
316
|
+
* `device-aggregation.ts` picks which apply function to call by
|
|
317
|
+
* inspecting the bound provider's actual method surface (`setProfile` vs
|
|
318
|
+
* `setSettings`), never by branching on the cap name.
|
|
319
|
+
*/
|
|
320
|
+
/** Registered builderIds today. New device-config caps add their own reducer below. */
|
|
321
|
+
var STREAM_PARAMS_BUILDER_ID = "stream-params";
|
|
322
|
+
var DAY_NIGHT_BUILDER_ID = "day-night";
|
|
323
|
+
var IMAGE_SETTINGS_BUILDER_ID = "image-settings";
|
|
324
|
+
var STREAM_PARAMS_REDUCER = {
|
|
325
|
+
kind: "profile",
|
|
326
|
+
buildSchema: (options, status) => buildStreamParamsConfigSchema(options, status ?? null),
|
|
327
|
+
applyPatch: async (patch, setProfile) => {
|
|
328
|
+
for (const meta of STREAM_PROFILE_META) {
|
|
329
|
+
const profilePatch = parseStreamParamsFormPatch(patch, meta.prefix);
|
|
330
|
+
if (profilePatch) await setProfile(meta.profile, profilePatch);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
var DAY_NIGHT_REDUCER = {
|
|
335
|
+
kind: "settings",
|
|
336
|
+
buildSchema: (options, status) => buildDayNightConfigSchema(options, status ?? null),
|
|
337
|
+
parsePatch: (patch) => parseDayNightFormPatch(patch)
|
|
338
|
+
};
|
|
339
|
+
var IMAGE_SETTINGS_REDUCER = {
|
|
340
|
+
kind: "settings",
|
|
341
|
+
buildSchema: (options, status) => buildImageSettingsConfigSchema(options, status ?? null),
|
|
342
|
+
parsePatch: (patch) => parseImageSettingsFormPatch(patch)
|
|
343
|
+
};
|
|
344
|
+
var BUILDER_REDUCERS = {
|
|
345
|
+
[STREAM_PARAMS_BUILDER_ID]: STREAM_PARAMS_REDUCER,
|
|
346
|
+
[DAY_NIGHT_BUILDER_ID]: DAY_NIGHT_REDUCER,
|
|
347
|
+
[IMAGE_SETTINGS_BUILDER_ID]: IMAGE_SETTINGS_REDUCER
|
|
348
|
+
};
|
|
349
|
+
function resolveReducer(builderId) {
|
|
350
|
+
const reducer = BUILDER_REDUCERS[builderId];
|
|
351
|
+
if (!reducer) throw new Error(`device-config: unknown derived-form builderId "${builderId}"`);
|
|
352
|
+
return reducer;
|
|
353
|
+
}
|
|
5
354
|
/**
|
|
6
355
|
* Build the device-detail form section for a `derived-form` device-config
|
|
7
356
|
* cap. Returns null when the camera exposes no configurable property.
|
|
8
357
|
*/
|
|
9
358
|
function deriveFormContribution(builderId, options, status) {
|
|
10
|
-
|
|
11
|
-
const schema = buildStreamParamsConfigSchema(options, status ?? null);
|
|
359
|
+
const schema = resolveReducer(builderId).buildSchema(options, status);
|
|
12
360
|
if (!schema) return null;
|
|
13
361
|
return { sections: schema.sections.map((s) => ({
|
|
14
362
|
id: s.id,
|
|
@@ -21,14 +369,26 @@ function deriveFormContribution(builderId, options, status) {
|
|
|
21
369
|
})) };
|
|
22
370
|
}
|
|
23
371
|
/**
|
|
24
|
-
* Route a flat form patch back through
|
|
372
|
+
* Route a flat form patch back through a profile-based cap's per-profile
|
|
373
|
+
* `setProfile` mutation (`stream-params`). Throws if `builderId` isn't
|
|
374
|
+
* registered as a profile-based reducer — callers dispatch here only
|
|
375
|
+
* after confirming the bound provider exposes `setProfile`.
|
|
25
376
|
*/
|
|
26
|
-
async function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
377
|
+
async function applyDerivedFormProfilePatch(builderId, patch, setProfile) {
|
|
378
|
+
const reducer = resolveReducer(builderId);
|
|
379
|
+
if (reducer.kind !== "profile") throw new Error(`device-config: builderId "${builderId}" is not a profile-based derived-form`);
|
|
380
|
+
await reducer.applyPatch(patch, setProfile);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Parse a flat form patch into the settings payload for a `setSettings`-
|
|
384
|
+
* based cap (`day-night`, `image-settings`). The caller forwards the
|
|
385
|
+
* result verbatim to `dcProvider.setSettings({ deviceId, settings })`.
|
|
386
|
+
* Throws if `builderId` isn't registered as a settings-based reducer.
|
|
387
|
+
*/
|
|
388
|
+
function parseDerivedFormSettingsPatch(builderId, patch) {
|
|
389
|
+
const reducer = resolveReducer(builderId);
|
|
390
|
+
if (reducer.kind !== "settings") throw new Error(`device-config: builderId "${builderId}" is not a settings-based derived-form`);
|
|
391
|
+
return reducer.parsePatch(patch);
|
|
32
392
|
}
|
|
33
393
|
//#endregion
|
|
34
394
|
//#region src/builtins/device-manager/device-aggregation-merge.ts
|
|
@@ -980,6 +1340,33 @@ async function resolveDriverConfigSchema(deps, deviceId, hubLocal) {
|
|
|
980
1340
|
});
|
|
981
1341
|
return { status: "unavailable" };
|
|
982
1342
|
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Route a `derived-form` device-config cap's flat form patch to whichever
|
|
1345
|
+
* mutation the bound provider actually exposes — inspecting the
|
|
1346
|
+
* provider's method surface, not the cap name. `setSettings` fires once
|
|
1347
|
+
* with the parsed flat patch; `setProfile` fans the patch out per-profile
|
|
1348
|
+
* via the registered builderId reducer.
|
|
1349
|
+
*/
|
|
1350
|
+
async function dispatchDerivedFormSave(builderId, deviceId, patch, dcProvider, capName) {
|
|
1351
|
+
if (typeof dcProvider.setSettings === "function") {
|
|
1352
|
+
const settings = parseDerivedFormSettingsPatch(builderId, patch);
|
|
1353
|
+
await dcProvider.setSettings({
|
|
1354
|
+
deviceId,
|
|
1355
|
+
settings
|
|
1356
|
+
});
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
if (typeof dcProvider.setProfile === "function") {
|
|
1360
|
+
const setProfile = dcProvider.setProfile;
|
|
1361
|
+
await applyDerivedFormProfilePatch(builderId, patch, (profile, profilePatch) => setProfile({
|
|
1362
|
+
deviceId,
|
|
1363
|
+
profile,
|
|
1364
|
+
patch: profilePatch
|
|
1365
|
+
}));
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
throw new Error(`[device-manager] device-config cap "${capName}" provider exposes neither setSettings nor setProfile`);
|
|
1369
|
+
}
|
|
983
1370
|
async function updateDeviceField(deps, input) {
|
|
984
1371
|
if (input.writerCapName === "device-manager") {
|
|
985
1372
|
const hubRegistry = deps.ctx.kernel?.deviceRegistry;
|
|
@@ -1003,11 +1390,7 @@ async function updateDeviceField(deps, input) {
|
|
|
1003
1390
|
if (def.deviceConfig.ui.kind === "widget") return { success: true };
|
|
1004
1391
|
const dcProvider = registry.getProviderForDevice(input.writerCapName, input.deviceId);
|
|
1005
1392
|
if (!dcProvider) throw new Error(`[device-manager] no provider for device-config cap "${input.writerCapName}" on device ${input.deviceId}`);
|
|
1006
|
-
await
|
|
1007
|
-
deviceId: input.deviceId,
|
|
1008
|
-
profile,
|
|
1009
|
-
patch
|
|
1010
|
-
}));
|
|
1393
|
+
await dispatchDerivedFormSave(def.deviceConfig.ui.builderId, input.deviceId, { [input.key]: input.value }, dcProvider, input.writerCapName);
|
|
1011
1394
|
return { success: true };
|
|
1012
1395
|
}
|
|
1013
1396
|
if (!def?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${input.writerCapName}" does not expose device settings`);
|
|
@@ -1126,11 +1509,7 @@ async function applyGroupPatch(deps, deviceId, group) {
|
|
|
1126
1509
|
if (def.deviceConfig.ui.kind === "widget") return;
|
|
1127
1510
|
const dcProvider = registry.getProviderForDevice(group.writerCapName, deviceId);
|
|
1128
1511
|
if (!dcProvider) throw new Error(`[device-manager] no provider for device-config cap "${group.writerCapName}" on device ${deviceId}`);
|
|
1129
|
-
await
|
|
1130
|
-
deviceId,
|
|
1131
|
-
profile,
|
|
1132
|
-
patch
|
|
1133
|
-
}));
|
|
1512
|
+
await dispatchDerivedFormSave(def.deviceConfig.ui.builderId, deviceId, group.patch, dcProvider, group.writerCapName);
|
|
1134
1513
|
return;
|
|
1135
1514
|
}
|
|
1136
1515
|
if (!def?.exposesDeviceSettings) throw new Error(`[device-manager] cap "${group.writerCapName}" does not expose device settings`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ConfigUISchema, ImageSettingsOptions, ImageSettingsPatch, ImageSettingsStatus } from '@camstack/types';
|
|
2
|
+
/**
|
|
3
|
+
* Build the `image-settings` `ConfigUISchema` from the camera-probed
|
|
4
|
+
* options + current status. Returns `null` when the camera exposes no
|
|
5
|
+
* configurable property.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildImageSettingsConfigSchema(options: ImageSettingsOptions, status: ImageSettingsStatus | null): ConfigUISchema | null;
|
|
8
|
+
/**
|
|
9
|
+
* Re-parse a flat `ConfigFormBuilder` patch into an `ImageSettingsPatch`.
|
|
10
|
+
* Returns an empty object when no `imageSettings_*` field changed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseImageSettingsFormPatch(patch: Record<string, unknown>): ImageSettingsPatch;
|
|
@@ -5957,7 +5957,7 @@ var AuthManager = class {
|
|
|
5957
5957
|
}
|
|
5958
5958
|
signToken(payload) {
|
|
5959
5959
|
const configured = this.config.get("auth.tokenExpiry");
|
|
5960
|
-
const expiresIn = typeof configured === "string" && configured.trim() !== "" ? configured.trim() : "
|
|
5960
|
+
const expiresIn = typeof configured === "string" && configured.trim() !== "" ? configured.trim() : "7d";
|
|
5961
5961
|
return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn });
|
|
5962
5962
|
}
|
|
5963
5963
|
verifyToken(token) {
|
|
@@ -5984,7 +5984,7 @@ var AuthManager = class {
|
|
|
5984
5984
|
}
|
|
5985
5985
|
signToken(payload) {
|
|
5986
5986
|
const configured = this.config.get("auth.tokenExpiry");
|
|
5987
|
-
const expiresIn = typeof configured === "string" && configured.trim() !== "" ? configured.trim() : "
|
|
5987
|
+
const expiresIn = typeof configured === "string" && configured.trim() !== "" ? configured.trim() : "7d";
|
|
5988
5988
|
return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn });
|
|
5989
5989
|
}
|
|
5990
5990
|
verifyToken(token) {
|
|
@@ -10,6 +10,7 @@ export default class NativeMetricsAddon extends BaseAddon<NativeMetricsConfig> {
|
|
|
10
10
|
private provider;
|
|
11
11
|
private startedAtMs;
|
|
12
12
|
private snapshotTimer;
|
|
13
|
+
private processSnapshotTimer;
|
|
13
14
|
/**
|
|
14
15
|
* Snapshot-equality cache for the resources + processes emit.
|
|
15
16
|
* Stores the coarsened JSON and timestamp; a tick where the
|
|
@@ -22,11 +23,23 @@ export default class NativeMetricsAddon extends BaseAddon<NativeMetricsConfig> {
|
|
|
22
23
|
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
23
24
|
protected onShutdown(): Promise<void>;
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* subscribe and read state directly from the payload.
|
|
26
|
+
* Resolve this node's short id (strips any `nodeId/addonId` suffix).
|
|
27
|
+
* Returns null when the provider or event bus isn't available yet.
|
|
28
28
|
*/
|
|
29
|
-
private
|
|
29
|
+
private resolveSnapshotNodeId;
|
|
30
|
+
/**
|
|
31
|
+
* Emit one `metrics.node-resources-snapshot` for this node. Cheap —
|
|
32
|
+
* reads the cached sample (the provider's background sampler already
|
|
33
|
+
* runs at samplingIntervalMs). Fires on METRICS_SNAPSHOT_INTERVAL_MS.
|
|
34
|
+
*/
|
|
35
|
+
private emitResourcesSnapshot;
|
|
36
|
+
/**
|
|
37
|
+
* Emit one `metrics.node-processes-snapshot` for this node. Heavy —
|
|
38
|
+
* runs a full OS `ps -eo` scan (`runPs`) plus a `$process.list` broker
|
|
39
|
+
* call. Fires on the coarser PROCESS_SNAPSHOT_INTERVAL_MS so an idle
|
|
40
|
+
* node isn't paying a process-table walk every 5s. Skip on failure.
|
|
41
|
+
*/
|
|
42
|
+
private emitProcessesSnapshot;
|
|
30
43
|
protected onConfigChanged(): Promise<void>;
|
|
31
44
|
private listWorkerInstances;
|
|
32
45
|
private listAddonInstances;
|
|
@@ -504,6 +504,19 @@ var SUPERVISOR_BOUNDARY_RE = /(tsx\s+watch\s.*launcher\.ts|packages\/agent\/dist
|
|
|
504
504
|
*/
|
|
505
505
|
var METRICS_SNAPSHOT_INTERVAL_MS = 5e3;
|
|
506
506
|
/**
|
|
507
|
+
* Cadence for the per-node PROCESS-TREE snapshot bus events. Kept
|
|
508
|
+
* deliberately coarser than the resources cadence because each tick
|
|
509
|
+
* runs a full OS `ps -eo` scan (`runPs`) plus a `$process.list` broker
|
|
510
|
+
* call — heavy work that was previously paid every 5s regardless of
|
|
511
|
+
* whether any UI consumer was subscribed to
|
|
512
|
+
* `MetricsNodeProcessesSnapshot`. The event bus exposes no per-category
|
|
513
|
+
* subscriber count, so instead of demand-gating we split the cadence:
|
|
514
|
+
* 20s cuts the `ps` frequency 4x while the ProcessesTab still gets a
|
|
515
|
+
* fresh-on-open payload from the on-demand `listNodeProcesses` cap call.
|
|
516
|
+
* The dedup + 60s heartbeat below still apply on top of this.
|
|
517
|
+
*/
|
|
518
|
+
var PROCESS_SNAPSHOT_INTERVAL_MS = 2e4;
|
|
519
|
+
/**
|
|
507
520
|
* Force a metrics-snapshot emit at least every 60s even when the
|
|
508
521
|
* coarsened payload looks unchanged. Without this an idle node
|
|
509
522
|
* (steady CPU within ±5%, no process churn) goes silent on the
|
|
@@ -569,6 +582,7 @@ var NativeMetricsAddon = class extends _camstack_types.BaseAddon {
|
|
|
569
582
|
provider = null;
|
|
570
583
|
startedAtMs = Date.now();
|
|
571
584
|
snapshotTimer = null;
|
|
585
|
+
processSnapshotTimer = null;
|
|
572
586
|
/**
|
|
573
587
|
* Snapshot-equality cache for the resources + processes emit.
|
|
574
588
|
* Stores the coarsened JSON and timestamp; a tick where the
|
|
@@ -601,7 +615,8 @@ var NativeMetricsAddon = class extends _camstack_types.BaseAddon {
|
|
|
601
615
|
killProcess: (params) => this.killProcess(params),
|
|
602
616
|
dumpHeapSnapshot: (params) => this.dumpHeapSnapshot(params)
|
|
603
617
|
};
|
|
604
|
-
this.snapshotTimer = setInterval(() => this.
|
|
618
|
+
this.snapshotTimer = setInterval(() => this.emitResourcesSnapshot(), METRICS_SNAPSHOT_INTERVAL_MS);
|
|
619
|
+
this.processSnapshotTimer = setInterval(() => this.emitProcessesSnapshot(), PROCESS_SNAPSHOT_INTERVAL_MS);
|
|
605
620
|
return [{
|
|
606
621
|
capability: _camstack_types.metricsProviderCapability,
|
|
607
622
|
provider: composed
|
|
@@ -612,20 +627,32 @@ var NativeMetricsAddon = class extends _camstack_types.BaseAddon {
|
|
|
612
627
|
clearInterval(this.snapshotTimer);
|
|
613
628
|
this.snapshotTimer = null;
|
|
614
629
|
}
|
|
630
|
+
if (this.processSnapshotTimer) {
|
|
631
|
+
clearInterval(this.processSnapshotTimer);
|
|
632
|
+
this.processSnapshotTimer = null;
|
|
633
|
+
}
|
|
615
634
|
this.provider?.stopSampling();
|
|
616
635
|
this.provider = null;
|
|
617
636
|
}
|
|
618
637
|
/**
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
|
|
638
|
+
* Resolve this node's short id (strips any `nodeId/addonId` suffix).
|
|
639
|
+
* Returns null when the provider or event bus isn't available yet.
|
|
640
|
+
*/
|
|
641
|
+
resolveSnapshotNodeId() {
|
|
642
|
+
if (!this.provider || !this.ctx.eventBus) return null;
|
|
643
|
+
const rawNodeId = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
644
|
+
return rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Emit one `metrics.node-resources-snapshot` for this node. Cheap —
|
|
648
|
+
* reads the cached sample (the provider's background sampler already
|
|
649
|
+
* runs at samplingIntervalMs). Fires on METRICS_SNAPSHOT_INTERVAL_MS.
|
|
622
650
|
*/
|
|
623
|
-
async
|
|
651
|
+
async emitResourcesSnapshot() {
|
|
624
652
|
const provider = this.provider;
|
|
625
653
|
const eventBus = this.ctx.eventBus;
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const nodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
|
|
654
|
+
const nodeId = this.resolveSnapshotNodeId();
|
|
655
|
+
if (!provider || !eventBus || !nodeId) return;
|
|
629
656
|
const timestamp = Date.now();
|
|
630
657
|
try {
|
|
631
658
|
const snapshot = await provider.getCached() ?? await provider.collectSnapshot();
|
|
@@ -648,6 +675,18 @@ var NativeMetricsAddon = class extends _camstack_types.BaseAddon {
|
|
|
648
675
|
}));
|
|
649
676
|
}
|
|
650
677
|
} catch {}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Emit one `metrics.node-processes-snapshot` for this node. Heavy —
|
|
681
|
+
* runs a full OS `ps -eo` scan (`runPs`) plus a `$process.list` broker
|
|
682
|
+
* call. Fires on the coarser PROCESS_SNAPSHOT_INTERVAL_MS so an idle
|
|
683
|
+
* node isn't paying a process-table walk every 5s. Skip on failure.
|
|
684
|
+
*/
|
|
685
|
+
async emitProcessesSnapshot() {
|
|
686
|
+
const eventBus = this.ctx.eventBus;
|
|
687
|
+
const nodeId = this.resolveSnapshotNodeId();
|
|
688
|
+
if (!this.provider || !eventBus || !nodeId) return;
|
|
689
|
+
const timestamp = Date.now();
|
|
651
690
|
try {
|
|
652
691
|
const processes = await this.listNodeProcesses();
|
|
653
692
|
const coarse = coarsenProcessList(processes);
|
|
@@ -497,6 +497,19 @@ var SUPERVISOR_BOUNDARY_RE = /(tsx\s+watch\s.*launcher\.ts|packages\/agent\/dist
|
|
|
497
497
|
*/
|
|
498
498
|
var METRICS_SNAPSHOT_INTERVAL_MS = 5e3;
|
|
499
499
|
/**
|
|
500
|
+
* Cadence for the per-node PROCESS-TREE snapshot bus events. Kept
|
|
501
|
+
* deliberately coarser than the resources cadence because each tick
|
|
502
|
+
* runs a full OS `ps -eo` scan (`runPs`) plus a `$process.list` broker
|
|
503
|
+
* call — heavy work that was previously paid every 5s regardless of
|
|
504
|
+
* whether any UI consumer was subscribed to
|
|
505
|
+
* `MetricsNodeProcessesSnapshot`. The event bus exposes no per-category
|
|
506
|
+
* subscriber count, so instead of demand-gating we split the cadence:
|
|
507
|
+
* 20s cuts the `ps` frequency 4x while the ProcessesTab still gets a
|
|
508
|
+
* fresh-on-open payload from the on-demand `listNodeProcesses` cap call.
|
|
509
|
+
* The dedup + 60s heartbeat below still apply on top of this.
|
|
510
|
+
*/
|
|
511
|
+
var PROCESS_SNAPSHOT_INTERVAL_MS = 2e4;
|
|
512
|
+
/**
|
|
500
513
|
* Force a metrics-snapshot emit at least every 60s even when the
|
|
501
514
|
* coarsened payload looks unchanged. Without this an idle node
|
|
502
515
|
* (steady CPU within ±5%, no process churn) goes silent on the
|
|
@@ -562,6 +575,7 @@ var NativeMetricsAddon = class extends BaseAddon {
|
|
|
562
575
|
provider = null;
|
|
563
576
|
startedAtMs = Date.now();
|
|
564
577
|
snapshotTimer = null;
|
|
578
|
+
processSnapshotTimer = null;
|
|
565
579
|
/**
|
|
566
580
|
* Snapshot-equality cache for the resources + processes emit.
|
|
567
581
|
* Stores the coarsened JSON and timestamp; a tick where the
|
|
@@ -594,7 +608,8 @@ var NativeMetricsAddon = class extends BaseAddon {
|
|
|
594
608
|
killProcess: (params) => this.killProcess(params),
|
|
595
609
|
dumpHeapSnapshot: (params) => this.dumpHeapSnapshot(params)
|
|
596
610
|
};
|
|
597
|
-
this.snapshotTimer = setInterval(() => this.
|
|
611
|
+
this.snapshotTimer = setInterval(() => this.emitResourcesSnapshot(), METRICS_SNAPSHOT_INTERVAL_MS);
|
|
612
|
+
this.processSnapshotTimer = setInterval(() => this.emitProcessesSnapshot(), PROCESS_SNAPSHOT_INTERVAL_MS);
|
|
598
613
|
return [{
|
|
599
614
|
capability: metricsProviderCapability,
|
|
600
615
|
provider: composed
|
|
@@ -605,20 +620,32 @@ var NativeMetricsAddon = class extends BaseAddon {
|
|
|
605
620
|
clearInterval(this.snapshotTimer);
|
|
606
621
|
this.snapshotTimer = null;
|
|
607
622
|
}
|
|
623
|
+
if (this.processSnapshotTimer) {
|
|
624
|
+
clearInterval(this.processSnapshotTimer);
|
|
625
|
+
this.processSnapshotTimer = null;
|
|
626
|
+
}
|
|
608
627
|
this.provider?.stopSampling();
|
|
609
628
|
this.provider = null;
|
|
610
629
|
}
|
|
611
630
|
/**
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
|
|
631
|
+
* Resolve this node's short id (strips any `nodeId/addonId` suffix).
|
|
632
|
+
* Returns null when the provider or event bus isn't available yet.
|
|
633
|
+
*/
|
|
634
|
+
resolveSnapshotNodeId() {
|
|
635
|
+
if (!this.provider || !this.ctx.eventBus) return null;
|
|
636
|
+
const rawNodeId = this.ctx.kernel.localNodeId ?? this.ctx.id;
|
|
637
|
+
return rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Emit one `metrics.node-resources-snapshot` for this node. Cheap —
|
|
641
|
+
* reads the cached sample (the provider's background sampler already
|
|
642
|
+
* runs at samplingIntervalMs). Fires on METRICS_SNAPSHOT_INTERVAL_MS.
|
|
615
643
|
*/
|
|
616
|
-
async
|
|
644
|
+
async emitResourcesSnapshot() {
|
|
617
645
|
const provider = this.provider;
|
|
618
646
|
const eventBus = this.ctx.eventBus;
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const nodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
|
|
647
|
+
const nodeId = this.resolveSnapshotNodeId();
|
|
648
|
+
if (!provider || !eventBus || !nodeId) return;
|
|
622
649
|
const timestamp = Date.now();
|
|
623
650
|
try {
|
|
624
651
|
const snapshot = await provider.getCached() ?? await provider.collectSnapshot();
|
|
@@ -641,6 +668,18 @@ var NativeMetricsAddon = class extends BaseAddon {
|
|
|
641
668
|
}));
|
|
642
669
|
}
|
|
643
670
|
} catch {}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Emit one `metrics.node-processes-snapshot` for this node. Heavy —
|
|
674
|
+
* runs a full OS `ps -eo` scan (`runPs`) plus a `$process.list` broker
|
|
675
|
+
* call. Fires on the coarser PROCESS_SNAPSHOT_INTERVAL_MS so an idle
|
|
676
|
+
* node isn't paying a process-table walk every 5s. Skip on failure.
|
|
677
|
+
*/
|
|
678
|
+
async emitProcessesSnapshot() {
|
|
679
|
+
const eventBus = this.ctx.eventBus;
|
|
680
|
+
const nodeId = this.resolveSnapshotNodeId();
|
|
681
|
+
if (!this.provider || !eventBus || !nodeId) return;
|
|
682
|
+
const timestamp = Date.now();
|
|
644
683
|
try {
|
|
645
684
|
const processes = await this.listNodeProcesses();
|
|
646
685
|
const coarse = coarsenProcessList(processes);
|