@camstack/addon-pipeline-orchestrator 1.2.264 → 1.2.265

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/index.js CHANGED
@@ -8036,7 +8036,59 @@ function errMsg$1(err) {
8036
8036
  return String(err);
8037
8037
  }
8038
8038
  //#endregion
8039
- //#region ../types/dist/composition-BK-GTFYb.mjs
8039
+ //#region ../types/dist/composition-Dq42XWp0.mjs
8040
+ /**
8041
+ * The ffmpeg build every CamStack node downloads, per platform and arch.
8042
+ *
8043
+ * ## Why a download, on every node, including the container
8044
+ *
8045
+ * The version of ffmpeg used to be CODE: baked into the image, changeable only
8046
+ * by rebuilding and rolling it. Now it is DATA — this file states the release,
8047
+ * and a node that boots with a different one downloads it. Changing ffmpeg is a
8048
+ * framework bump, not an image rebuild.
8049
+ *
8050
+ * It also settles the licence question by removing it. FFmpeg's licence is
8051
+ * chosen at compile time: the core is LGPL 2.1+, `--enable-gpl` makes it GPL
8052
+ * (for x264/x265), `--enable-version3` makes that v3, and `--enable-nonfree`
8053
+ * makes the result undistributable. These are GPL v3 builds, and we do not
8054
+ * distribute them: the user's own node fetches the archive from the project
8055
+ * that publishes it, so we convey no GPL work and inherit none of the
8056
+ * obligations that come with conveying one. That is the whole reason the image
8057
+ * no longer installs ffmpeg at all — not a technical preference.
8058
+ *
8059
+ * ## Why jellyfin-ffmpeg
8060
+ *
8061
+ * One vendor covering linux-amd64, linux-arm64, darwin-arm64 and darwin-x64
8062
+ * from one release, with the widest hardware surface of anything maintained:
8063
+ * VAAPI, QSV, NVENC/NVDEC, AMF, Vulkan, OpenCL and DRM on amd64, plus Rockchip
8064
+ * MPP on arm64. It reaches the vendor libraries through dlopen trampolines,
8065
+ * so the binary is not linked against a driver stack it may not find.
8066
+ *
8067
+ * **What it still needs from the host, measured 2026-09-19.** On the Unraid
8068
+ * HOST, which has no libva, `-vaapi_device` aborts the process outright —
8069
+ * `implib-gen: libva-drm.so.2: failed to load library ... Assertion '0 &&
8070
+ * "Assertion in generated code"' failed`. Inside our container, which installs
8071
+ * libva plus the iHD driver, the same command encodes: verified end to end with
8072
+ * `testsrc → format=nv12,hwupload → h264_vaapi`. So the image drops ffmpeg and
8073
+ * KEEPS the Intel media stack; the drivers are the part a binary cannot bring.
8074
+ *
8075
+ * ## Why each artifact carries a checksum and a claim
8076
+ *
8077
+ * The download is now the only source, so `sha256` is verified before anything
8078
+ * is extracted — jellyfin publishes no checksums on its release assets, so
8079
+ * these are ours, taken at the version bump. And `expectedHwaccels` is what the
8080
+ * build is supposed to be able to do: a binary that probes short of its own
8081
+ * claim is a source that changed under us, which is precisely what went
8082
+ * unnoticed for months when a static build turned out to carry no VAAPI at all
8083
+ * (D536).
8084
+ */
8085
+ /** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
8086
+ var FFMPEG_RELEASE = "8.1.2-5";
8087
+ var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
8088
+ function portable(target) {
8089
+ return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
8090
+ }
8091
+ portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64");
8040
8092
  /**
8041
8093
  * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
8042
8094
  * valutatore puro del suo FRONTE.
@@ -14076,6 +14128,70 @@ var CompositionBlockStateSchema = object({
14076
14128
  reason: string().optional(),
14077
14129
  fields: array(CompositionFieldStateSchema)
14078
14130
  });
14131
+ var NUMBER = {
14132
+ kind: "fixed",
14133
+ result: ["number"]
14134
+ };
14135
+ var BOOLEAN = {
14136
+ kind: "fixed",
14137
+ result: ["boolean"]
14138
+ };
14139
+ Object.freeze({
14140
+ min: NUMBER,
14141
+ max: NUMBER,
14142
+ abs: NUMBER,
14143
+ floor: NUMBER,
14144
+ ceil: NUMBER,
14145
+ sqrt: NUMBER,
14146
+ round: NUMBER,
14147
+ pow: NUMBER,
14148
+ clamp: NUMBER,
14149
+ avg: NUMBER,
14150
+ sum: NUMBER,
14151
+ age: NUMBER,
14152
+ convert: NUMBER,
14153
+ coalesce: { kind: "coalesce" },
14154
+ any: BOOLEAN,
14155
+ all: BOOLEAN,
14156
+ within: BOOLEAN,
14157
+ latest: {
14158
+ kind: "fixed",
14159
+ result: ["number", "null"]
14160
+ },
14161
+ number: {
14162
+ kind: "fixed",
14163
+ result: ["number", "null"]
14164
+ },
14165
+ rose: {
14166
+ kind: "fixed",
14167
+ result: ["number", "null"]
14168
+ },
14169
+ count: NUMBER
14170
+ });
14171
+ `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
14172
+ /**
14173
+ * What a composition may BUILD on a device type (G2, D676): the caps a new
14174
+ * device of that type, or an existing one that lacks them, can be given. The
14175
+ * blockers are the validator's own refusal codes, and a spec compares this
14176
+ * listing with `planComposition` for every capability — the editor can never
14177
+ * offer a feature the save would refuse for the same reason.
14178
+ */
14179
+ var ComposableFieldSchema = object({
14180
+ path: string(),
14181
+ kind: CompositionFieldKindSchema,
14182
+ nullable: boolean(),
14183
+ required: boolean(),
14184
+ enumValues: array(string()).optional()
14185
+ });
14186
+ var ComposableCapabilitiesSchema = object({ caps: array(object({
14187
+ cap: string(),
14188
+ fields: array(ComposableFieldSchema),
14189
+ /** Present for an item-array cap (`consumables.items`). */
14190
+ itemArray: object({
14191
+ path: string(),
14192
+ keyField: string()
14193
+ }).optional()
14194
+ })) });
14079
14195
  /** Where a block runs. The operator chooses — a block driving a device on an
14080
14196
  * agent is the reason placement is not fixed to the hub. */
14081
14197
  var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
@@ -14240,7 +14356,7 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
14240
14356
  }), method(object({ blockId: string() }), CompositionGetReadSchema, { auth: "admin" }), method(object({}), CompositionListReadSchema, { auth: "admin" }), method(object({
14241
14357
  addonId: string().min(1),
14242
14358
  stableId: string().min(1)
14243
- }), CompositionCustomizationReadSchema, { auth: "admin" }), method(object({
14359
+ }), CompositionCustomizationReadSchema, { auth: "admin" }), method(object({ deviceType: _enum(DeviceType) }), ComposableCapabilitiesSchema, { auth: "admin" }), method(object({
14244
14360
  composition: CompositionSchema,
14245
14361
  blockId: string().optional()
14246
14362
  }), CompositionValidationSchema, {
@@ -25698,6 +25814,16 @@ var NodeInferenceDevicesSchema = object({
25698
25814
  * Strictly the control plane: never sees a frame, never calls inference,
25699
25815
  * never drains queues. The runner does that work locally on each agent.
25700
25816
  */
25817
+ /**
25818
+ * The input of `setCameraSensors` (G5, D676): the ONE typed write path of a
25819
+ * camera's attached sensors (D672). `config: null` removes the attachment —
25820
+ * the camera reads `not-configured` again, never `unreadable`.
25821
+ */
25822
+ var SetCameraSensorsInputSchema = object({
25823
+ deviceId: number().int().nonnegative(),
25824
+ /** `null` removes the attachment: the camera goes back to `not-configured`. */
25825
+ config: CameraSensorsConfigSchema.nullable()
25826
+ });
25701
25827
  var pipelineOrchestratorCapability = {
25702
25828
  name: "pipeline-orchestrator",
25703
25829
  scope: "system",
@@ -26163,6 +26289,18 @@ var pipelineOrchestratorCapability = {
26163
26289
  */
26164
26290
  getCameraStatuses: method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()),
26165
26291
  /**
26292
+ * Attach sensors to a camera, or detach them with `config: null` (D672
26293
+ * configuration, G5/D676). Written to the orchestrator's per-device store
26294
+ * under the same key `applyDeviceSettingsPatch` writes, and announced as a
26295
+ * settings change so the camera-sensors bridge refreshes. Refused by name
26296
+ * — nothing written — for a device that cannot be read, does not exist,
26297
+ * is not a camera, or names itself as its own sensor.
26298
+ */
26299
+ setCameraSensors: method(SetCameraSensorsInputSchema, object({ success: literal(true) }), {
26300
+ kind: "mutation",
26301
+ auth: "admin"
26302
+ }),
26303
+ /**
26166
26304
  * Disk-wins fleet reconcile after a recordings wipe. Starts the walk in
26167
26305
  * the addon process and returns immediately with the job snapshot — the
26168
26306
  * work outlives the tRPC/UDS 60s timeout. Poll `getReconcileFromDiskStatus`.
@@ -37192,47 +37330,6 @@ var zoneRulesCapability = {
37192
37330
  */
37193
37331
  durability: "restored"
37194
37332
  };
37195
- var NUMBER = {
37196
- kind: "fixed",
37197
- result: ["number"]
37198
- };
37199
- var BOOLEAN = {
37200
- kind: "fixed",
37201
- result: ["boolean"]
37202
- };
37203
- Object.freeze({
37204
- min: NUMBER,
37205
- max: NUMBER,
37206
- abs: NUMBER,
37207
- floor: NUMBER,
37208
- ceil: NUMBER,
37209
- sqrt: NUMBER,
37210
- round: NUMBER,
37211
- pow: NUMBER,
37212
- clamp: NUMBER,
37213
- avg: NUMBER,
37214
- sum: NUMBER,
37215
- age: NUMBER,
37216
- convert: NUMBER,
37217
- coalesce: { kind: "coalesce" },
37218
- any: BOOLEAN,
37219
- all: BOOLEAN,
37220
- within: BOOLEAN,
37221
- latest: {
37222
- kind: "fixed",
37223
- result: ["number", "null"]
37224
- },
37225
- number: {
37226
- kind: "fixed",
37227
- result: ["number", "null"]
37228
- },
37229
- rose: {
37230
- kind: "fixed",
37231
- result: ["number", "null"]
37232
- },
37233
- count: NUMBER
37234
- });
37235
- `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
37236
37333
  /**
37237
37334
  * Accessory device helpers — shared across drivers.
37238
37335
  *
@@ -39683,6 +39780,12 @@ Object.freeze({
39683
39780
  addonId: null,
39684
39781
  access: "view"
39685
39782
  },
39783
+ "coreBlocks.listComposableCaps": {
39784
+ capName: "core-blocks",
39785
+ capScope: "system",
39786
+ addonId: null,
39787
+ access: "view"
39788
+ },
39686
39789
  "coreBlocks.listCompositions": {
39687
39790
  capName: "core-blocks",
39688
39791
  capScope: "system",
@@ -43367,6 +43470,12 @@ Object.freeze({
43367
43470
  addonId: null,
43368
43471
  access: "create"
43369
43472
  },
43473
+ "pipelineOrchestrator.setCameraSensors": {
43474
+ capName: "pipeline-orchestrator",
43475
+ capScope: "system",
43476
+ addonId: null,
43477
+ access: "create"
43478
+ },
43370
43479
  "pipelineOrchestrator.setCameraStepOverride": {
43371
43480
  capName: "pipeline-orchestrator",
43372
43481
  capScope: "system",
@@ -47077,6 +47186,11 @@ Object.freeze({
47077
47186
  form: "single",
47078
47187
  optional: false
47079
47188
  }],
47189
+ "pipelineOrchestrator.setCameraSensors": [{
47190
+ name: "deviceId",
47191
+ form: "single",
47192
+ optional: false
47193
+ }],
47080
47194
  "pipelineOrchestrator.setCameraStepOverride": [{
47081
47195
  name: "deviceId",
47082
47196
  form: "single",
@@ -49698,6 +49812,52 @@ var AudioSubscriptionController = class {
49698
49812
  }
49699
49813
  };
49700
49814
  //#endregion
49815
+ //#region src/camera-sensors/write.ts
49816
+ /**
49817
+ * The ONE typed write path of a camera's attached sensors (D672 config,
49818
+ * G5/D676) — `pipelineOrchestrator.setCameraSensors`.
49819
+ *
49820
+ * The input is already validated by the cap schema at the router. Here it is
49821
+ * refused BY NAME, and logged with the device tag, before anything is written
49822
+ * (D391): a device that could not be read (a failed read changes nothing,
49823
+ * D49), an unknown device, a non-camera, a camera named as its own sensor.
49824
+ * The value is then handed to `writeDeviceStore` under the CS-1 key, which the
49825
+ * addon binds to `applyDeviceSettingsPatch` — so the D672 patch gate, the
49826
+ * PARSED value, the store and the settings-updated event that refreshes the
49827
+ * bridge's resolver are ONE path, never a copy of it. `null` clears: the
49828
+ * device store keeps a null key, and `parseCameraSensorsSetting` reads it as
49829
+ * not-configured.
49830
+ */
49831
+ function refuse(deps, deviceId, reason) {
49832
+ deps.logger.warn("camera sensors write refused", {
49833
+ tags: { deviceId },
49834
+ meta: { reason }
49835
+ });
49836
+ throw new Error(`pipeline-orchestrator: camera sensors of ${deviceId} not saved — ${reason}`);
49837
+ }
49838
+ async function readOrRefuse(deps, deviceId) {
49839
+ let device;
49840
+ try {
49841
+ device = await deps.readDevice(deviceId);
49842
+ } catch (err) {
49843
+ return refuse(deps, deviceId, `could not read device ${deviceId}: ${errMsg$1(err)}`);
49844
+ }
49845
+ if (device === null) return refuse(deps, deviceId, `no device ${deviceId}`);
49846
+ return device;
49847
+ }
49848
+ async function writeCameraSensors(deps, input) {
49849
+ const { deviceId, config } = input;
49850
+ const device = await readOrRefuse(deps, deviceId);
49851
+ if (device.type !== DeviceType.Camera) return refuse(deps, deviceId, `device ${deviceId} is not a camera`);
49852
+ if (config !== null && config.sensors.some((s) => s.addonId === device.addonId && s.stableId === device.stableId)) return refuse(deps, deviceId, "a camera cannot be its own sensor");
49853
+ await deps.writeDeviceStore(deviceId, { [CAMERA_SENSORS_SETTINGS_KEY]: config });
49854
+ deps.logger.info("camera sensors saved", {
49855
+ tags: { deviceId },
49856
+ meta: { sensors: config === null ? 0 : config.sensors.length }
49857
+ });
49858
+ return { success: true };
49859
+ }
49860
+ //#endregion
49701
49861
  //#region src/disk-reconcile-fleet.ts
49702
49862
  async function reconcileFleetFromDisk(deps) {
49703
49863
  const deviceIds = await deps.listDeviceIds();
@@ -66215,6 +66375,37 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
66215
66375
  return this.deviceConfig.applyDeviceSettingsPatch(input);
66216
66376
  }
66217
66377
  /**
66378
+ * The one typed write path of a camera's attached sensors (G5, D676) — see
66379
+ * `camera-sensors/write.ts`. Its refusals run first; the store write then
66380
+ * goes THROUGH `applyDeviceSettingsPatch`, so the D672 patch gate, the
66381
+ * parsed value, the key and the `OrchestrationSettingsUpdated` event are the
66382
+ * one path a camera configured through the device page already takes.
66383
+ */
66384
+ async setCameraSensors(input) {
66385
+ const ctx = this.ctx;
66386
+ const deviceConfig = this.deviceConfig;
66387
+ if (deviceConfig === null) throw new Error("pipeline-orchestrator: not initialised — camera sensors not saved");
66388
+ const api = ctx.api;
66389
+ return writeCameraSensors({
66390
+ readDevice: async (deviceId) => {
66391
+ if (!api) throw new Error("the device registry is not reachable");
66392
+ const dev = await api.deviceManager.getDevice.query({ deviceId });
66393
+ return dev === null || dev === void 0 ? null : {
66394
+ type: dev.type,
66395
+ addonId: dev.addonId,
66396
+ stableId: dev.stableId
66397
+ };
66398
+ },
66399
+ writeDeviceStore: async (deviceId, patch) => {
66400
+ await deviceConfig.applyDeviceSettingsPatch({
66401
+ deviceId,
66402
+ patch
66403
+ });
66404
+ },
66405
+ logger: ctx.logger.child("camera-sensors")
66406
+ }, input);
66407
+ }
66408
+ /**
66218
66409
  * Apply the subset of global settings that affect runtime behaviour
66219
66410
  * (balancer thresholds, broker timeout, failover policy) into the
66220
66411
  * in-memory state so subsequent `balance()` / `handleNodeDisconnect`
package/dist/index.mjs CHANGED
@@ -8008,7 +8008,59 @@ function errMsg$1(err) {
8008
8008
  return String(err);
8009
8009
  }
8010
8010
  //#endregion
8011
- //#region ../types/dist/composition-BK-GTFYb.mjs
8011
+ //#region ../types/dist/composition-Dq42XWp0.mjs
8012
+ /**
8013
+ * The ffmpeg build every CamStack node downloads, per platform and arch.
8014
+ *
8015
+ * ## Why a download, on every node, including the container
8016
+ *
8017
+ * The version of ffmpeg used to be CODE: baked into the image, changeable only
8018
+ * by rebuilding and rolling it. Now it is DATA — this file states the release,
8019
+ * and a node that boots with a different one downloads it. Changing ffmpeg is a
8020
+ * framework bump, not an image rebuild.
8021
+ *
8022
+ * It also settles the licence question by removing it. FFmpeg's licence is
8023
+ * chosen at compile time: the core is LGPL 2.1+, `--enable-gpl` makes it GPL
8024
+ * (for x264/x265), `--enable-version3` makes that v3, and `--enable-nonfree`
8025
+ * makes the result undistributable. These are GPL v3 builds, and we do not
8026
+ * distribute them: the user's own node fetches the archive from the project
8027
+ * that publishes it, so we convey no GPL work and inherit none of the
8028
+ * obligations that come with conveying one. That is the whole reason the image
8029
+ * no longer installs ffmpeg at all — not a technical preference.
8030
+ *
8031
+ * ## Why jellyfin-ffmpeg
8032
+ *
8033
+ * One vendor covering linux-amd64, linux-arm64, darwin-arm64 and darwin-x64
8034
+ * from one release, with the widest hardware surface of anything maintained:
8035
+ * VAAPI, QSV, NVENC/NVDEC, AMF, Vulkan, OpenCL and DRM on amd64, plus Rockchip
8036
+ * MPP on arm64. It reaches the vendor libraries through dlopen trampolines,
8037
+ * so the binary is not linked against a driver stack it may not find.
8038
+ *
8039
+ * **What it still needs from the host, measured 2026-09-19.** On the Unraid
8040
+ * HOST, which has no libva, `-vaapi_device` aborts the process outright —
8041
+ * `implib-gen: libva-drm.so.2: failed to load library ... Assertion '0 &&
8042
+ * "Assertion in generated code"' failed`. Inside our container, which installs
8043
+ * libva plus the iHD driver, the same command encodes: verified end to end with
8044
+ * `testsrc → format=nv12,hwupload → h264_vaapi`. So the image drops ffmpeg and
8045
+ * KEEPS the Intel media stack; the drivers are the part a binary cannot bring.
8046
+ *
8047
+ * ## Why each artifact carries a checksum and a claim
8048
+ *
8049
+ * The download is now the only source, so `sha256` is verified before anything
8050
+ * is extracted — jellyfin publishes no checksums on its release assets, so
8051
+ * these are ours, taken at the version bump. And `expectedHwaccels` is what the
8052
+ * build is supposed to be able to do: a binary that probes short of its own
8053
+ * claim is a source that changed under us, which is precisely what went
8054
+ * unnoticed for months when a static build turned out to carry no VAAPI at all
8055
+ * (D536).
8056
+ */
8057
+ /** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
8058
+ var FFMPEG_RELEASE = "8.1.2-5";
8059
+ var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
8060
+ function portable(target) {
8061
+ return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
8062
+ }
8063
+ portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64");
8012
8064
  /**
8013
8065
  * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
8014
8066
  * valutatore puro del suo FRONTE.
@@ -14048,6 +14100,70 @@ var CompositionBlockStateSchema = object({
14048
14100
  reason: string().optional(),
14049
14101
  fields: array(CompositionFieldStateSchema)
14050
14102
  });
14103
+ var NUMBER = {
14104
+ kind: "fixed",
14105
+ result: ["number"]
14106
+ };
14107
+ var BOOLEAN = {
14108
+ kind: "fixed",
14109
+ result: ["boolean"]
14110
+ };
14111
+ Object.freeze({
14112
+ min: NUMBER,
14113
+ max: NUMBER,
14114
+ abs: NUMBER,
14115
+ floor: NUMBER,
14116
+ ceil: NUMBER,
14117
+ sqrt: NUMBER,
14118
+ round: NUMBER,
14119
+ pow: NUMBER,
14120
+ clamp: NUMBER,
14121
+ avg: NUMBER,
14122
+ sum: NUMBER,
14123
+ age: NUMBER,
14124
+ convert: NUMBER,
14125
+ coalesce: { kind: "coalesce" },
14126
+ any: BOOLEAN,
14127
+ all: BOOLEAN,
14128
+ within: BOOLEAN,
14129
+ latest: {
14130
+ kind: "fixed",
14131
+ result: ["number", "null"]
14132
+ },
14133
+ number: {
14134
+ kind: "fixed",
14135
+ result: ["number", "null"]
14136
+ },
14137
+ rose: {
14138
+ kind: "fixed",
14139
+ result: ["number", "null"]
14140
+ },
14141
+ count: NUMBER
14142
+ });
14143
+ `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
14144
+ /**
14145
+ * What a composition may BUILD on a device type (G2, D676): the caps a new
14146
+ * device of that type, or an existing one that lacks them, can be given. The
14147
+ * blockers are the validator's own refusal codes, and a spec compares this
14148
+ * listing with `planComposition` for every capability — the editor can never
14149
+ * offer a feature the save would refuse for the same reason.
14150
+ */
14151
+ var ComposableFieldSchema = object({
14152
+ path: string(),
14153
+ kind: CompositionFieldKindSchema,
14154
+ nullable: boolean(),
14155
+ required: boolean(),
14156
+ enumValues: array(string()).optional()
14157
+ });
14158
+ var ComposableCapabilitiesSchema = object({ caps: array(object({
14159
+ cap: string(),
14160
+ fields: array(ComposableFieldSchema),
14161
+ /** Present for an item-array cap (`consumables.items`). */
14162
+ itemArray: object({
14163
+ path: string(),
14164
+ keyField: string()
14165
+ }).optional()
14166
+ })) });
14051
14167
  /** Where a block runs. The operator chooses — a block driving a device on an
14052
14168
  * agent is the reason placement is not fixed to the hub. */
14053
14169
  var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
@@ -14212,7 +14328,7 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
14212
14328
  }), method(object({ blockId: string() }), CompositionGetReadSchema, { auth: "admin" }), method(object({}), CompositionListReadSchema, { auth: "admin" }), method(object({
14213
14329
  addonId: string().min(1),
14214
14330
  stableId: string().min(1)
14215
- }), CompositionCustomizationReadSchema, { auth: "admin" }), method(object({
14331
+ }), CompositionCustomizationReadSchema, { auth: "admin" }), method(object({ deviceType: _enum(DeviceType) }), ComposableCapabilitiesSchema, { auth: "admin" }), method(object({
14216
14332
  composition: CompositionSchema,
14217
14333
  blockId: string().optional()
14218
14334
  }), CompositionValidationSchema, {
@@ -25670,6 +25786,16 @@ var NodeInferenceDevicesSchema = object({
25670
25786
  * Strictly the control plane: never sees a frame, never calls inference,
25671
25787
  * never drains queues. The runner does that work locally on each agent.
25672
25788
  */
25789
+ /**
25790
+ * The input of `setCameraSensors` (G5, D676): the ONE typed write path of a
25791
+ * camera's attached sensors (D672). `config: null` removes the attachment —
25792
+ * the camera reads `not-configured` again, never `unreadable`.
25793
+ */
25794
+ var SetCameraSensorsInputSchema = object({
25795
+ deviceId: number().int().nonnegative(),
25796
+ /** `null` removes the attachment: the camera goes back to `not-configured`. */
25797
+ config: CameraSensorsConfigSchema.nullable()
25798
+ });
25673
25799
  var pipelineOrchestratorCapability = {
25674
25800
  name: "pipeline-orchestrator",
25675
25801
  scope: "system",
@@ -26135,6 +26261,18 @@ var pipelineOrchestratorCapability = {
26135
26261
  */
26136
26262
  getCameraStatuses: method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()),
26137
26263
  /**
26264
+ * Attach sensors to a camera, or detach them with `config: null` (D672
26265
+ * configuration, G5/D676). Written to the orchestrator's per-device store
26266
+ * under the same key `applyDeviceSettingsPatch` writes, and announced as a
26267
+ * settings change so the camera-sensors bridge refreshes. Refused by name
26268
+ * — nothing written — for a device that cannot be read, does not exist,
26269
+ * is not a camera, or names itself as its own sensor.
26270
+ */
26271
+ setCameraSensors: method(SetCameraSensorsInputSchema, object({ success: literal(true) }), {
26272
+ kind: "mutation",
26273
+ auth: "admin"
26274
+ }),
26275
+ /**
26138
26276
  * Disk-wins fleet reconcile after a recordings wipe. Starts the walk in
26139
26277
  * the addon process and returns immediately with the job snapshot — the
26140
26278
  * work outlives the tRPC/UDS 60s timeout. Poll `getReconcileFromDiskStatus`.
@@ -37164,47 +37302,6 @@ var zoneRulesCapability = {
37164
37302
  */
37165
37303
  durability: "restored"
37166
37304
  };
37167
- var NUMBER = {
37168
- kind: "fixed",
37169
- result: ["number"]
37170
- };
37171
- var BOOLEAN = {
37172
- kind: "fixed",
37173
- result: ["boolean"]
37174
- };
37175
- Object.freeze({
37176
- min: NUMBER,
37177
- max: NUMBER,
37178
- abs: NUMBER,
37179
- floor: NUMBER,
37180
- ceil: NUMBER,
37181
- sqrt: NUMBER,
37182
- round: NUMBER,
37183
- pow: NUMBER,
37184
- clamp: NUMBER,
37185
- avg: NUMBER,
37186
- sum: NUMBER,
37187
- age: NUMBER,
37188
- convert: NUMBER,
37189
- coalesce: { kind: "coalesce" },
37190
- any: BOOLEAN,
37191
- all: BOOLEAN,
37192
- within: BOOLEAN,
37193
- latest: {
37194
- kind: "fixed",
37195
- result: ["number", "null"]
37196
- },
37197
- number: {
37198
- kind: "fixed",
37199
- result: ["number", "null"]
37200
- },
37201
- rose: {
37202
- kind: "fixed",
37203
- result: ["number", "null"]
37204
- },
37205
- count: NUMBER
37206
- });
37207
- `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
37208
37305
  /**
37209
37306
  * Accessory device helpers — shared across drivers.
37210
37307
  *
@@ -39655,6 +39752,12 @@ Object.freeze({
39655
39752
  addonId: null,
39656
39753
  access: "view"
39657
39754
  },
39755
+ "coreBlocks.listComposableCaps": {
39756
+ capName: "core-blocks",
39757
+ capScope: "system",
39758
+ addonId: null,
39759
+ access: "view"
39760
+ },
39658
39761
  "coreBlocks.listCompositions": {
39659
39762
  capName: "core-blocks",
39660
39763
  capScope: "system",
@@ -43339,6 +43442,12 @@ Object.freeze({
43339
43442
  addonId: null,
43340
43443
  access: "create"
43341
43444
  },
43445
+ "pipelineOrchestrator.setCameraSensors": {
43446
+ capName: "pipeline-orchestrator",
43447
+ capScope: "system",
43448
+ addonId: null,
43449
+ access: "create"
43450
+ },
43342
43451
  "pipelineOrchestrator.setCameraStepOverride": {
43343
43452
  capName: "pipeline-orchestrator",
43344
43453
  capScope: "system",
@@ -47049,6 +47158,11 @@ Object.freeze({
47049
47158
  form: "single",
47050
47159
  optional: false
47051
47160
  }],
47161
+ "pipelineOrchestrator.setCameraSensors": [{
47162
+ name: "deviceId",
47163
+ form: "single",
47164
+ optional: false
47165
+ }],
47052
47166
  "pipelineOrchestrator.setCameraStepOverride": [{
47053
47167
  name: "deviceId",
47054
47168
  form: "single",
@@ -49670,6 +49784,52 @@ var AudioSubscriptionController = class {
49670
49784
  }
49671
49785
  };
49672
49786
  //#endregion
49787
+ //#region src/camera-sensors/write.ts
49788
+ /**
49789
+ * The ONE typed write path of a camera's attached sensors (D672 config,
49790
+ * G5/D676) — `pipelineOrchestrator.setCameraSensors`.
49791
+ *
49792
+ * The input is already validated by the cap schema at the router. Here it is
49793
+ * refused BY NAME, and logged with the device tag, before anything is written
49794
+ * (D391): a device that could not be read (a failed read changes nothing,
49795
+ * D49), an unknown device, a non-camera, a camera named as its own sensor.
49796
+ * The value is then handed to `writeDeviceStore` under the CS-1 key, which the
49797
+ * addon binds to `applyDeviceSettingsPatch` — so the D672 patch gate, the
49798
+ * PARSED value, the store and the settings-updated event that refreshes the
49799
+ * bridge's resolver are ONE path, never a copy of it. `null` clears: the
49800
+ * device store keeps a null key, and `parseCameraSensorsSetting` reads it as
49801
+ * not-configured.
49802
+ */
49803
+ function refuse(deps, deviceId, reason) {
49804
+ deps.logger.warn("camera sensors write refused", {
49805
+ tags: { deviceId },
49806
+ meta: { reason }
49807
+ });
49808
+ throw new Error(`pipeline-orchestrator: camera sensors of ${deviceId} not saved — ${reason}`);
49809
+ }
49810
+ async function readOrRefuse(deps, deviceId) {
49811
+ let device;
49812
+ try {
49813
+ device = await deps.readDevice(deviceId);
49814
+ } catch (err) {
49815
+ return refuse(deps, deviceId, `could not read device ${deviceId}: ${errMsg$1(err)}`);
49816
+ }
49817
+ if (device === null) return refuse(deps, deviceId, `no device ${deviceId}`);
49818
+ return device;
49819
+ }
49820
+ async function writeCameraSensors(deps, input) {
49821
+ const { deviceId, config } = input;
49822
+ const device = await readOrRefuse(deps, deviceId);
49823
+ if (device.type !== DeviceType.Camera) return refuse(deps, deviceId, `device ${deviceId} is not a camera`);
49824
+ if (config !== null && config.sensors.some((s) => s.addonId === device.addonId && s.stableId === device.stableId)) return refuse(deps, deviceId, "a camera cannot be its own sensor");
49825
+ await deps.writeDeviceStore(deviceId, { [CAMERA_SENSORS_SETTINGS_KEY]: config });
49826
+ deps.logger.info("camera sensors saved", {
49827
+ tags: { deviceId },
49828
+ meta: { sensors: config === null ? 0 : config.sensors.length }
49829
+ });
49830
+ return { success: true };
49831
+ }
49832
+ //#endregion
49673
49833
  //#region src/disk-reconcile-fleet.ts
49674
49834
  async function reconcileFleetFromDisk(deps) {
49675
49835
  const deviceIds = await deps.listDeviceIds();
@@ -66187,6 +66347,37 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
66187
66347
  return this.deviceConfig.applyDeviceSettingsPatch(input);
66188
66348
  }
66189
66349
  /**
66350
+ * The one typed write path of a camera's attached sensors (G5, D676) — see
66351
+ * `camera-sensors/write.ts`. Its refusals run first; the store write then
66352
+ * goes THROUGH `applyDeviceSettingsPatch`, so the D672 patch gate, the
66353
+ * parsed value, the key and the `OrchestrationSettingsUpdated` event are the
66354
+ * one path a camera configured through the device page already takes.
66355
+ */
66356
+ async setCameraSensors(input) {
66357
+ const ctx = this.ctx;
66358
+ const deviceConfig = this.deviceConfig;
66359
+ if (deviceConfig === null) throw new Error("pipeline-orchestrator: not initialised — camera sensors not saved");
66360
+ const api = ctx.api;
66361
+ return writeCameraSensors({
66362
+ readDevice: async (deviceId) => {
66363
+ if (!api) throw new Error("the device registry is not reachable");
66364
+ const dev = await api.deviceManager.getDevice.query({ deviceId });
66365
+ return dev === null || dev === void 0 ? null : {
66366
+ type: dev.type,
66367
+ addonId: dev.addonId,
66368
+ stableId: dev.stableId
66369
+ };
66370
+ },
66371
+ writeDeviceStore: async (deviceId, patch) => {
66372
+ await deviceConfig.applyDeviceSettingsPatch({
66373
+ deviceId,
66374
+ patch
66375
+ });
66376
+ },
66377
+ logger: ctx.logger.child("camera-sensors")
66378
+ }, input);
66379
+ }
66380
+ /**
66190
66381
  * Apply the subset of global settings that affect runtime behaviour
66191
66382
  * (balancer thresholds, broker timeout, failover policy) into the
66192
66383
  * in-memory state so subsequent `balance()` / `handleNodeDisconnect`