@camstack/system 1.2.253 → 1.2.255

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.
Files changed (88) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/autotrack/autotrack-cameras.d.ts +76 -0
  8. package/dist/builtins/autotrack/autotrack-config.d.ts +89 -0
  9. package/dist/builtins/autotrack/autotrack-decision.d.ts +99 -0
  10. package/dist/builtins/autotrack/autotrack-loop.d.ts +75 -0
  11. package/dist/builtins/autotrack/autotrack.addon.d.ts +71 -0
  12. package/dist/builtins/autotrack/frame-error.d.ts +50 -0
  13. package/dist/builtins/autotrack/frame-measurements.d.ts +97 -0
  14. package/dist/builtins/autotrack/index.d.ts +2 -0
  15. package/dist/builtins/autotrack/index.js +981 -0
  16. package/dist/builtins/autotrack/index.mjs +975 -0
  17. package/dist/builtins/autotrack/ptz-mirror.d.ts +25 -0
  18. package/dist/builtins/autotrack/target-selection.d.ts +60 -0
  19. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  20. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  21. package/dist/builtins/camera-grid/addon.d.ts +118 -0
  22. package/dist/builtins/camera-grid/flv-join.d.ts +66 -0
  23. package/dist/builtins/camera-grid/grid-camera-declarations.d.ts +59 -0
  24. package/dist/builtins/camera-grid/grid-camera-device.d.ts +32 -0
  25. package/dist/builtins/camera-grid/grid-child.d.ts +9 -0
  26. package/dist/builtins/camera-grid/grid-device-settings.d.ts +11 -0
  27. package/dist/builtins/camera-grid/grid-filter-graph.d.ts +53 -0
  28. package/dist/builtins/camera-grid/grid-frame-sample.d.ts +27 -0
  29. package/dist/builtins/camera-grid/grid-instances.d.ts +49 -0
  30. package/dist/builtins/camera-grid/grid-last-frame-store.d.ts +19 -0
  31. package/dist/builtins/camera-grid/grid-layout-native-provider.d.ts +8 -0
  32. package/dist/builtins/camera-grid/grid-plan.d.ts +22 -0
  33. package/dist/builtins/camera-grid/grid-profiles.d.ts +47 -0
  34. package/dist/builtins/camera-grid/grid-row-normalization.d.ts +10 -0
  35. package/dist/builtins/camera-grid/grid-sentry-sources.d.ts +52 -0
  36. package/dist/builtins/camera-grid/grid-snapshot.d.ts +62 -0
  37. package/dist/builtins/camera-grid/grid-stream-descriptor.d.ts +24 -0
  38. package/dist/builtins/camera-grid/grid-stream-invocation.d.ts +29 -0
  39. package/dist/builtins/camera-grid/grid-stream-relay.d.ts +36 -0
  40. package/dist/builtins/camera-grid/grid-stream-session.d.ts +113 -0
  41. package/dist/builtins/camera-grid/grid-wire-format.d.ts +14 -0
  42. package/dist/builtins/camera-grid/index.d.ts +44 -0
  43. package/dist/builtins/camera-grid/index.js +2309 -0
  44. package/dist/builtins/camera-grid/index.mjs +2276 -0
  45. package/dist/builtins/camera-grid/silence-analysis.d.ts +13 -0
  46. package/dist/builtins/console-logging/index.js +1 -1
  47. package/dist/builtins/console-logging/index.mjs +1 -1
  48. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  49. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  50. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  51. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  52. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  53. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  54. package/dist/builtins/hub-forwarder/index.js +1 -1
  55. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  56. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  57. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  58. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  59. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  60. package/dist/builtins/local-network/local-network.addon.js +1 -1
  61. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  62. package/dist/builtins/loki-logging/index.js +1 -1
  63. package/dist/builtins/loki-logging/index.mjs +1 -1
  64. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  65. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  66. package/dist/builtins/platform-probe/index.js +1 -1
  67. package/dist/builtins/platform-probe/index.mjs +1 -1
  68. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  69. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  70. package/dist/builtins/snapshot/index.js +1 -1
  71. package/dist/builtins/snapshot/index.mjs +1 -1
  72. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  73. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  74. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  75. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  76. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  77. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  78. package/dist/builtins/system-config/system-config.addon.js +1 -1
  79. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  80. package/dist/builtins/winston-logging/index.js +1 -1
  81. package/dist/builtins/winston-logging/index.mjs +1 -1
  82. package/dist/{dist-B9y-3rme.js → dist-BJj6Akye.js} +1347 -13
  83. package/dist/{dist-DH6ThyTd.mjs → dist-Ch93tyHB.mjs} +1288 -14
  84. package/dist/index.js +1 -1
  85. package/dist/index.mjs +1 -1
  86. package/dist/{retired-settings-keys-CnMg7wvS.js → retired-settings-keys-BRCn6-mt.js} +1 -1
  87. package/dist/{retired-settings-keys-DNl1lrr_.mjs → retired-settings-keys-uCaSKuwd.mjs} +1 -1
  88. package/package.json +32 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-B9y-3rme.js");
6
+ const require_dist = require("../../dist-BJj6Akye.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { A as addonPagesCapability, Cn as EventCategory, Xt as errMsg, Zt as BaseAddon } from "../../dist-DH6ThyTd.mjs";
1
+ import { Nn as EventCategory, P as addonPagesCapability, in as errMsg, on as BaseAddon } from "../../dist-Ch93tyHB.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-B9y-3rme.js");
6
+ const require_dist = require("../../dist-BJj6Akye.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Cn as EventCategory, M as addonWidgetsCapability, Xt as errMsg, Zt as BaseAddon } from "../../dist-DH6ThyTd.mjs";
1
+ import { I as addonWidgetsCapability, Nn as EventCategory, in as errMsg, on as BaseAddon } from "../../dist-Ch93tyHB.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-B9y-3rme.js");
6
+ const require_dist = require("../../dist-BJj6Akye.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -1,4 +1,4 @@
1
- import { Cn as EventCategory, N as alertsCapability, Xt as errMsg, Zt as BaseAddon, n as AlertSchema, un as createEvent } from "../../dist-DH6ThyTd.mjs";
1
+ import { L as alertsCapability, Nn as EventCategory, in as errMsg, on as BaseAddon, r as AlertSchema, yn as createEvent } from "../../dist-Ch93tyHB.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -0,0 +1,76 @@
1
+ import { FirstLevelMacroClass, PtzAutotrackSettings, PtzAutotrackStatus } from '@camstack/types';
2
+ import { AutotrackLoopConfig } from './autotrack-config.js';
3
+ /** One camera's record. */
4
+ export interface AutotrackCameraRecord {
5
+ readonly enabled: boolean;
6
+ readonly settings: PtzAutotrackSettings;
7
+ }
8
+ /** deviceId → record. */
9
+ export type AutotrackCameraRecords = Readonly<Record<number, AutotrackCameraRecord>>;
10
+ /**
11
+ * The defaults a camera starts from.
12
+ *
13
+ * `disappearDelaySeconds` matches the loop's own default so the two cannot
14
+ * disagree on the day nobody has opened the form. `stopDelaySeconds` is
15
+ * persisted and NOT honoured — see {@link loopConfigFor}.
16
+ */
17
+ export declare const DEFAULT_CAMERA_RECORD: AutotrackCameraRecord;
18
+ /**
19
+ * What this implementation can follow, as the cap's target options.
20
+ *
21
+ * A vendor list would be a lie here: the emulated loop follows what the
22
+ * DETECTOR emits, and the detector's first-level macro classes are these
23
+ * three. Reolink's `people` and Hikvision's `human` are vendor words for the
24
+ * same thing and are normalised in {@link targetClassFor} rather than offered.
25
+ */
26
+ export declare const EMULATED_TARGET_OPTIONS: PtzAutotrackStatus['supportedTargetTypes'];
27
+ /**
28
+ * Read the persisted blob.
29
+ *
30
+ * A malformed ENTRY falls back to the defaults and keeps its `enabled` flag —
31
+ * it is never dropped. Dropping it would disarm a function the operator armed,
32
+ * which is the one direction this must not fail in; a camera that is not in
33
+ * the blob at all is simply a camera nobody has switched on.
34
+ */
35
+ export declare function parseCameraRecords(raw: unknown): AutotrackCameraRecords;
36
+ /** Immutable per-camera patch — a NEW map, the previous one untouched. */
37
+ export declare function withCameraPatch(records: AutotrackCameraRecords, deviceId: number, patch: {
38
+ enabled?: boolean;
39
+ settings?: Partial<PtzAutotrackSettings>;
40
+ }): AutotrackCameraRecords;
41
+ /**
42
+ * The detection macro class this camera follows.
43
+ *
44
+ * `''` is the cap's "camera default" and means PERSON here — the class an
45
+ * autotrack is asked for in almost every case, and the one the measurement
46
+ * slice used. A vendor string (`people`, `human`) normalises to the macro it
47
+ * names; anything else falls back to person rather than following nothing,
48
+ * because a loop that silently follows nothing is indistinguishable from one
49
+ * that is broken.
50
+ */
51
+ export declare function targetClassFor(settings: PtzAutotrackSettings): FirstLevelMacroClass;
52
+ /**
53
+ * The loop's config for this camera.
54
+ *
55
+ * Only ONE of the cap's three settings reaches the loop:
56
+ *
57
+ * - `disappearDelaySeconds` → {@link AutotrackLoopConfig.disappearDelayMs}. Same
58
+ * meaning on both sides: how long a target that stopped appearing is held
59
+ * before it is released and the head goes home.
60
+ * - `targetType` → the detection class, through {@link targetClassFor}.
61
+ * - `stopDelaySeconds` is persisted and NOT honoured. It means "keep tracking a
62
+ * STATIONARY target for this long before stopping", and a pulse-based loop
63
+ * already does nothing while the subject is inside the dead zone — there is
64
+ * no motion to stop. Storing it and ignoring it is deliberate: the cap is
65
+ * cross-vendor, a native provider honours it, and dropping it from the
66
+ * payload would make a camera that switches from native to emulated lose a
67
+ * setting it had. It is reported back by `getStatus` verbatim, never as a
68
+ * number this loop acted on.
69
+ *
70
+ * Everything else in the loop config is a TUNING CONSTANT that D520 kept out of
71
+ * the operator surface on purpose (dead zone, settle zone, command ceiling,
72
+ * pulse dead time). They stay constants.
73
+ */
74
+ export declare function loopConfigFor(settings: PtzAutotrackSettings): AutotrackLoopConfig;
75
+ /** The cap's status for one camera. */
76
+ export declare function statusFor(record: AutotrackCameraRecord, lastChangedAt: number): PtzAutotrackStatus;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Tuning constants for the emulated PTZ autotrack.
3
+ *
4
+ * Everything here is a number the OPERATOR does not set: the dead zone, the
5
+ * settle zone, the command ceiling, the pulse dead time, the refresh cadences.
6
+ * D520 kept them out of the settings surface on purpose — they are the
7
+ * behaviour of the control loop, not a preference, and a camera whose dead zone
8
+ * somebody widened by hand is a camera nobody can reason about.
9
+ *
10
+ * What IS per camera lives with the record (`autotrack-cameras.ts`): the switch,
11
+ * the target class and the disappear delay, all read through the cap's own
12
+ * settings so there is one place an operator changes them.
13
+ */
14
+ /**
15
+ * ## The constants that used to live here
16
+ *
17
+ * `AUTOTRACK_ARMED` and `AUTOTRACK_TARGET_DEVICE_ID` were the whole protection
18
+ * of the measurement slice: arming was a deploy-shaped act because there was no
19
+ * switch to do it with. There is one now — the emulation registers
20
+ * `ptz-autotrack` as a wrapper the operator BINDS per camera and switches on
21
+ * through the cap's own `setEnabled` (D62: one function, one switch). The
22
+ * protection is no longer a constant; it is that a camera must be both bound
23
+ * and switched on, and that the loop still refuses to move without
24
+ * `moveImpulseMs` or while the camera's native autotrack is armed (D520).
25
+ *
26
+ * `AUTOTRACK_TARGET_CLASS` went the same way: the class is per camera now,
27
+ * from the cap's `targetType` setting (`autotrack-cameras.ts`).
28
+ */
29
+ /** Signed single-axis step handed to `ptz.move` — a DIRECTION, not a delta.
30
+ * Every provider reads the sign and ignores the magnitude (reolink:
31
+ * `pan > 0 ? 'Right' : 'Left'`), so only -1 / 0 / +1 are meaningful. */
32
+ export type PtzAxisStep = -1 | 0 | 1;
33
+ /** Tuning of the control loop. Every field is a time or a fraction; nothing
34
+ * here is derived from `detectionFps` — see {@link maxCommandsPerSecond}. */
35
+ export interface AutotrackLoopConfig {
36
+ /**
37
+ * Width/height of the central "do nothing" box, as a fraction of the frame.
38
+ * `0.5` = the central 50%: a correction starts only once the subject's
39
+ * centre leaves it (|error| > 0.25 on that axis).
40
+ */
41
+ readonly deadZoneFraction: number;
42
+ /**
43
+ * Width/height of the TIGHTER box that ENDS a correction, as a fraction of
44
+ * the frame. Must be smaller than {@link deadZoneFraction}.
45
+ *
46
+ * This is the anti-overshoot rule: the loop stops correcting BEFORE the
47
+ * subject reaches the centre. Aiming at the centre with an estimated gain
48
+ * overshoots, and an overshoot on the next frame reverses the sign — that is
49
+ * the oscillation this box exists to prevent.
50
+ */
51
+ readonly settleZoneFraction: number;
52
+ /**
53
+ * Hard ceiling on commands issued to the camera, in commands per second,
54
+ * owned by the LOOP.
55
+ *
56
+ * Explicitly NOT derived from the analysis rate. `frame-tracked` arrives at
57
+ * 1–10 Hz; at 10 Hz a per-frame command would be ~10 commands/s, an order of
58
+ * magnitude above what the hardware takes. The ceiling is measured, in-repo:
59
+ * a Reolink E1 Outdoor PoE rejects a command issued while the previous one
60
+ * is still travelling with `response_code 400` — presses 1.2 s apart were
61
+ * REJECTED, 3 s apart were ACCEPTED (`reolink-camera.ts`, `runZoom`). `1/3`
62
+ * is that 3 s, and it is the gate that binds in practice.
63
+ */
64
+ readonly maxCommandsPerSecond: number;
65
+ /**
66
+ * Extra silence after a pulse is expected to have FINISHED, ms.
67
+ *
68
+ * The dead time after a command is `moveImpulseMs + pulseSettleMs`: the head
69
+ * is still travelling for the first part and the detector has not yet seen
70
+ * the new framing for the second. Commanding inside it measures an error
71
+ * that the in-flight pulse has already corrected.
72
+ */
73
+ readonly pulseSettleMs: number;
74
+ /**
75
+ * How long the chosen target may be absent before it is released and the
76
+ * head is sent home, ms. Also the silence watchdog's budget: if
77
+ * `frame-tracked` stops arriving entirely, the same delay elapses and the
78
+ * same release happens — off the event path, by timer.
79
+ */
80
+ readonly disappearDelayMs: number;
81
+ /** Normalized speed (0..1) handed to `ptz.move`. */
82
+ readonly speed: number;
83
+ /** How often the off-event-path mirror re-reads `ptz.getOptions` and the
84
+ * native-autotrack state, ms. Never on the event path (D49). */
85
+ readonly mirrorRefreshMs: number;
86
+ /** How often the measurement aggregates are summarized to the log, ms. */
87
+ readonly measurementSummaryMs: number;
88
+ }
89
+ export declare const DEFAULT_AUTOTRACK_LOOP_CONFIG: AutotrackLoopConfig;
@@ -0,0 +1,99 @@
1
+ import { AutotrackLoopConfig, PtzAxisStep } from './autotrack-config.js';
2
+ import { NormalizedError } from './frame-error.js';
3
+ /**
4
+ * What the loop knows about the camera's NATIVE autotrack, from the
5
+ * off-event-path mirror.
6
+ *
7
+ * Native and emulated are two implementations of ONE function and must never
8
+ * be two switches an operator can set to disagree (D62). Until that single
9
+ * switch exists, this slice refuses to move a head whose firmware may also be
10
+ * moving it — and `unknown` refuses too, because not-yet-read must not look
11
+ * like not-enabled (D315, D393).
12
+ */
13
+ export type NativeAutotrackState =
14
+ /** The camera has no `ptz-autotrack` binding at all. No conflict possible. */
15
+ 'absent'
16
+ /** Bound and reported OFF. */
17
+ | 'disabled'
18
+ /** Bound and reported ON — the firmware is driving this head. */
19
+ | 'enabled'
20
+ /** Never successfully read, or the last read failed. */
21
+ | 'unknown';
22
+ /** Why the loop did NOT move. Every one of these is logged, per frame, with
23
+ * the measured error: a loop that decides not to move must not be
24
+ * indistinguishable from a dead one (D391). */
25
+ export type AutotrackHoldReason =
26
+ /** `AUTOTRACK_ARMED` is false, or no target camera is configured. */
27
+ 'disarmed'
28
+ /** No target has ever been held. */
29
+ | 'no-target'
30
+ /** The held target was absent from this frame, inside its disappear delay. */
31
+ | 'target-absent'
32
+ /** The target is gone and the head has already been sent home. */
33
+ | 'home-already-issued'
34
+ /** The frame did not say how big it is — the error is unknowable (D393). */
35
+ | 'frame-geometry-unknown'
36
+ /** The subject's centre is inside the central dead zone. THE NORMAL CASE. */
37
+ | 'inside-dead-zone'
38
+ /** A correction was running and the subject reached the tighter inner box.
39
+ * The correction ENDS here, deliberately short of the centre. */
40
+ | 'settled'
41
+ /** The driver does not report `moveImpulseMs`, so nothing proves the pulse
42
+ * self-terminates. */
43
+ | 'impulse-duration-unknown'
44
+ /** The camera's own autotrack is armed — two sources, one mechanism. */
45
+ | 'native-autotrack-enabled'
46
+ /** The camera's own autotrack state has never been read. */
47
+ | 'native-autotrack-unknown'
48
+ /** The previous pulse is still travelling, or the detector has not yet seen
49
+ * the framing it produced. */
50
+ | 'pulse-dead-time'
51
+ /** The loop's own commands-per-second ceiling. */
52
+ | 'rate-limited';
53
+ export type AutotrackDecision = {
54
+ readonly action: 'hold';
55
+ readonly reason: AutotrackHoldReason;
56
+ } | {
57
+ readonly action: 'pulse';
58
+ readonly pan: PtzAxisStep;
59
+ readonly tilt: PtzAxisStep;
60
+ readonly speed: number;
61
+ } | {
62
+ readonly action: 'home';
63
+ readonly reason: 'target-gone';
64
+ };
65
+ export interface AutotrackLoopState {
66
+ /** A correction is in progress on this axis: it ends at the tighter inner
67
+ * box, not at the dead zone it started outside of. */
68
+ readonly correctingX: boolean;
69
+ readonly correctingY: boolean;
70
+ /** When the last command (pulse or home) was issued. `null` = never. */
71
+ readonly lastCommandAtMs: number | null;
72
+ /** Whether the head has already been sent home for the current absence. */
73
+ readonly homeIssued: boolean;
74
+ }
75
+ export declare const INITIAL_AUTOTRACK_LOOP_STATE: AutotrackLoopState;
76
+ /** The held target as this frame saw it. `error` is `null` when the frame's
77
+ * geometry was unreadable — present-but-unmeasurable, which is NOT absent. */
78
+ export interface AutotrackTargetFrame {
79
+ readonly trackId: string;
80
+ readonly error: NormalizedError | null;
81
+ }
82
+ export interface AutotrackDecisionInput {
83
+ readonly nowMs: number;
84
+ /** Armed AND pointed at a camera. False ⇒ the loop only measures. */
85
+ readonly armed: boolean;
86
+ /** The held target if it was OBSERVED on this frame, else `null`. */
87
+ readonly target: AutotrackTargetFrame | null;
88
+ /** When the held target was last observed. `null` = nothing held. */
89
+ readonly targetLastSeenAtMs: number | null;
90
+ /** `ptz.getOptions().moveImpulseMs`, from the mirror. `null` = unknown. */
91
+ readonly moveImpulseMs: number | null;
92
+ readonly nativeAutotrack: NativeAutotrackState;
93
+ readonly config: AutotrackLoopConfig;
94
+ }
95
+ export interface AutotrackDecisionResult {
96
+ readonly state: AutotrackLoopState;
97
+ readonly decision: AutotrackDecision;
98
+ }
99
+ export declare function decideAutotrack(state: AutotrackLoopState, input: AutotrackDecisionInput): AutotrackDecisionResult;
@@ -0,0 +1,75 @@
1
+ import { IScopedLogger, ObjectDetection } from '@camstack/types';
2
+ import { AutotrackLoopConfig, PtzAxisStep } from './autotrack-config.js';
3
+ import { MirrorRefreshOutcome, PtzMirror } from './ptz-mirror.js';
4
+ import { TargetCandidate } from './target-selection.js';
5
+ export interface PulseCommand {
6
+ readonly deviceId: number;
7
+ readonly pan: PtzAxisStep;
8
+ readonly tilt: PtzAxisStep;
9
+ readonly speed: number;
10
+ }
11
+ /** The only way this loop touches the camera. One self-terminating pulse, or
12
+ * home. There is deliberately no `continuousMove` and no `stop`. */
13
+ export interface AutotrackCommands {
14
+ pulse(command: PulseCommand): Promise<void>;
15
+ goHome(deviceId: number): Promise<void>;
16
+ }
17
+ export interface AutotrackLoopDeps {
18
+ readonly deviceId: number;
19
+ readonly targetClass: string;
20
+ readonly armed: boolean;
21
+ readonly config: AutotrackLoopConfig;
22
+ readonly logger: IScopedLogger;
23
+ readonly commands: AutotrackCommands;
24
+ readonly now: () => number;
25
+ }
26
+ /** The subset of the `frame-tracked` payload this loop reads. Named so the
27
+ * tests can build one without the event envelope. */
28
+ export interface TrackedFrame {
29
+ readonly deviceId: number;
30
+ readonly timestamp: number;
31
+ readonly frameWidth: number;
32
+ readonly frameHeight: number;
33
+ readonly detections: readonly ObjectDetection[];
34
+ }
35
+ /**
36
+ * Candidates of the configured class on this frame.
37
+ *
38
+ * A detection counts only when it carries a `track` — `buildOverlayDetections`
39
+ * attaches one exactly to the boxes a tracked entry was ASSOCIATED with on this
40
+ * frame, so this set is the observed-and-tracked set and nothing else.
41
+ */
42
+ export declare function candidatesFromFrame(detections: readonly ObjectDetection[], targetClass: string): readonly TargetCandidate[];
43
+ export declare class AutotrackLoop {
44
+ private readonly deps;
45
+ private selection;
46
+ private loopState;
47
+ private mirror;
48
+ private latency;
49
+ private presence;
50
+ private lastFrameAtMs;
51
+ private lastSummaryAtMs;
52
+ constructor(deps: AutotrackLoopDeps);
53
+ /** Read-only view of the mirror, for the addon's startup log. */
54
+ get ptzMirror(): PtzMirror;
55
+ /** Fold one refresh outcome into the mirror. Called by the TIMER, never from
56
+ * {@link onFrame}. */
57
+ applyMirror(outcome: MirrorRefreshOutcome): void;
58
+ /** One analysed frame. Measures first, then decides. */
59
+ onFrame(frame: TrackedFrame): Promise<void>;
60
+ /**
61
+ * Off-event-path tick: the silence watchdog.
62
+ *
63
+ * Evaluates the SAME disappear delay against the time since the last frame.
64
+ * A stream that died is a target that is gone, and the head goes home —
65
+ * which is also the only thing that would otherwise never happen, because
66
+ * every other release is driven by a frame arriving.
67
+ */
68
+ tick(): Promise<void>;
69
+ /** Emit the aggregates now, whatever the schedule says. Used at shutdown so
70
+ * a short run still reports what it measured. */
71
+ logSummary(): void;
72
+ private maybeSummarize;
73
+ private dispatch;
74
+ private logDecision;
75
+ }
@@ -0,0 +1,71 @@
1
+ import { AddonInitResult, BaseAddon } from '@camstack/types';
2
+ /** Persisted shape: one entry per camera the operator has touched. */
3
+ export interface AutotrackConfig {
4
+ readonly cameras: Record<string, unknown>;
5
+ }
6
+ export declare class AutotrackAddon extends BaseAddon<AutotrackConfig> {
7
+ private records;
8
+ private readonly sessions;
9
+ /** Cameras whose `ptz-autotrack` binding names THIS addon, last time we read. */
10
+ private bound;
11
+ /**
12
+ * Cameras that ALSO have a `ptz-autotrack` provider that is not us.
13
+ *
14
+ * This is the only way to ask about a firmware autotrack without asking
15
+ * ourselves: on a camera where our wrapper is the binding, `ptzAutotrack.
16
+ * getStatus` resolves to THIS addon and returns our own switch — so a loop
17
+ * that read it would see `enabled: true` the moment the operator turned it
18
+ * on, conclude the firmware was driving the head, and refuse to move. A
19
+ * self-deadlock that would have looked exactly like the safety gate working.
20
+ */
21
+ private foreignProvider;
22
+ private readonly lastChangedAt;
23
+ private unsubscribeFrames;
24
+ private unsubscribeBindings;
25
+ private mirrorTimer;
26
+ private watchdogTimer;
27
+ constructor();
28
+ protected onInitialize(): Promise<AddonInitResult>;
29
+ protected onShutdown(): Promise<void>;
30
+ protected onConfigChanged(): Promise<void>;
31
+ private recordOf;
32
+ private statusOf;
33
+ private patchCamera;
34
+ private serialiseRecords;
35
+ /** Bindings first, then sessions: a session must never outlive its binding. */
36
+ private refreshEverything;
37
+ /**
38
+ * Which cameras have this addon bound as their `ptz-autotrack` provider.
39
+ *
40
+ * Only cameras the operator has a record for are asked about: a wrapper that
41
+ * enumerated every camera on every refresh would walk the whole fleet to
42
+ * learn nothing about the ones nobody switched on.
43
+ *
44
+ * A read that FAILS leaves the previous answer standing (D49) — a transient
45
+ * store blip must not stop a running loop. A read that SUCCEEDS and omits us
46
+ * is believed immediately: unlike a doorbell press, the cost of erring open
47
+ * here is a head that keeps moving after the operator unbound it.
48
+ */
49
+ private refreshBindings;
50
+ /** Start a loop for every camera that is bound AND switched on; stop the rest. */
51
+ private reconcileSessions;
52
+ private startSession;
53
+ private stopSession;
54
+ /**
55
+ * One off-event-path refresh of a camera's mirror.
56
+ *
57
+ * Two reads, and either one failing fails the whole refresh: a mirror half
58
+ * from now and half from twenty minutes ago is worse than one that says it
59
+ * is stale. The previous answers stand (D49).
60
+ */
61
+ private refreshMirror;
62
+ /**
63
+ * Is the camera's OWN autotrack on?
64
+ *
65
+ * `null` when the camera has no native autotrack at all — which is the case
66
+ * this addon exists for, and is NOT an error. A camera that HAS one and has
67
+ * it armed makes this loop refuse: two implementations driving one head is
68
+ * the thing the single-switch rule exists to prevent (D62).
69
+ */
70
+ private readNativeAutotrack;
71
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The frame-space error term: where the subject's centre sits relative to the
3
+ * centre of the frame, normalized to ±0.5 on each axis.
4
+ *
5
+ * ## There is no letterbox to discount here, and that was checked
6
+ *
7
+ * The detector runs on a 640×640 `fit: 'contain'` view
8
+ * (`SPLIT_DETECTION_VIEW`, `packages/addon-pipeline/src/pipeline-runner/
9
+ * index.ts`), so the raster the model sees IS letterboxed. The coordinates that
10
+ * reach this module are not: `ObjectDetection.bbox` is documented — and
11
+ * produced — as "absolute pixel coordinates in the SOURCE frame"
12
+ * (`packages/types/src/types/detection.ts`), and the same payload carries the
13
+ * `frameWidth` / `frameHeight` those pixels are expressed in
14
+ * (`PipelineAnalyticsFrameTrackedPayload`). The un-letterboxing has already
15
+ * happened upstream; doing it again here would double-count the bars.
16
+ *
17
+ * ## A geometry we cannot read is `null`, never centred (D393)
18
+ *
19
+ * A frame that arrives with a zero or non-finite width has not told us the
20
+ * subject is centred — it has told us nothing. Folding that into `{x:0, y:0}`
21
+ * would read as "perfectly on target" and silence the loop exactly when it
22
+ * should complain, the same shape as the unreadable `statfs` folded into "0
23
+ * bytes of headroom". The return type says so, and every caller must branch.
24
+ */
25
+ /** Signed offset of the subject's centre from the frame centre, in fractions
26
+ * of the frame: `-0.5` = hard left/top edge, `0` = centred, `+0.5` = hard
27
+ * right/bottom edge. `y` grows DOWNWARD, as image coordinates do. */
28
+ export interface NormalizedError {
29
+ readonly x: number;
30
+ readonly y: number;
31
+ }
32
+ /** The frame the bbox's pixels are expressed in. */
33
+ export interface FrameGeometry {
34
+ readonly frameWidth: number;
35
+ readonly frameHeight: number;
36
+ }
37
+ /** Absolute-pixel box in the source frame (`ObjectDetection.bbox`). */
38
+ export interface FrameBox {
39
+ readonly x: number;
40
+ readonly y: number;
41
+ readonly width: number;
42
+ readonly height: number;
43
+ }
44
+ /**
45
+ * Error of one subject box against the frame centre.
46
+ *
47
+ * `null` when the frame geometry or the box is unreadable — never a centred
48
+ * zero (D393).
49
+ */
50
+ export declare function computeNormalizedError(box: FrameBox, geometry: FrameGeometry): NormalizedError | null;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The two measurements this slice exists to produce. Pure reducers: a tally in,
3
+ * a NEW tally out, nothing mutated, no clock read inside.
4
+ *
5
+ * ## 1. Bus latency — how old a `frame-tracked` payload is when a hub-only
6
+ * subscriber gets it
7
+ *
8
+ * `Date.now() - payload.timestamp`, per frame. Nobody has measured this hop.
9
+ * The only latency number in the docs (p90 381 ms, max 22 s) is a DIFFERENT hop
10
+ * and must not be reused for it. It decides whether a visual control loop is
11
+ * viable at all: the error the loop acts on is this many milliseconds stale, so
12
+ * a subject walking at 1 m/s has already moved `latency × 1 m/s` by the time a
13
+ * pulse is issued.
14
+ *
15
+ * ## 2. Track absence — the empirical test of the loop's SAFETY PREMISE
16
+ *
17
+ * The premise is that every bbox on `frame-tracked` is an OBSERVED detection,
18
+ * never a coasted `SortTracker` prediction. It is INFERRED, not measured:
19
+ * `buildOverlayDetections` (`addon-post-analysis/src/pipeline-analytics/
20
+ * overlay-frame-builder.ts`) iterates the REAL frame detections and attaches a
21
+ * track only to the ones a tracked entry was associated with, so a coasted
22
+ * track contributes no box. `matchedThisFrame` is not on the wire, so the
23
+ * premise cannot be read off the payload — only inferred from the SHAPE of the
24
+ * gaps.
25
+ *
26
+ * If the premise HOLDS, an absence is a genuine miss and gaps are ragged: runs
27
+ * of 1, the odd 2, no preferred length. If it is FALSE — if a coasted
28
+ * prediction is being published as a box — the loop would be chasing a
29
+ * fabricated position, and the tell is the opposite: long stretches with NO
30
+ * gaps at all (the tracker filling every miss), then a gap exactly as long as
31
+ * the tracker's max age when it finally gives up. That is what
32
+ * {@link PresenceTally.gapRunHistogram} is for; a run-length distribution with
33
+ * a spike at one value is coasting, a decaying distribution is honest misses.
34
+ */
35
+ /** Rolling aggregate of the bus-latency measurement. */
36
+ export interface LatencyTally {
37
+ readonly frames: number;
38
+ readonly sumMs: number;
39
+ readonly minMs: number | null;
40
+ readonly maxMs: number | null;
41
+ /** Upper bounds, ms, of the histogram buckets — the last bucket is the
42
+ * overflow above the final bound. */
43
+ readonly bucketBoundsMs: readonly number[];
44
+ /** One count per bucket bound, plus one trailing overflow count. */
45
+ readonly bucketCounts: readonly number[];
46
+ /** Frames whose `timestamp` produced a NEGATIVE age. A clock skew between
47
+ * the analytics node and the hub shows up here and nowhere else; counted
48
+ * separately rather than folded into the mean, which would hide it. */
49
+ readonly negativeAgeFrames: number;
50
+ }
51
+ export declare const LATENCY_BUCKET_BOUNDS_MS: readonly number[];
52
+ export declare const INITIAL_LATENCY_TALLY: LatencyTally;
53
+ export declare function recordLatency(tally: LatencyTally, latencyMs: number): LatencyTally;
54
+ /** Rolling aggregate of the track-absence measurement. */
55
+ export interface PresenceTally {
56
+ /** Frames counted while a target was HELD — the denominator. A frame with
57
+ * no target held at all is not evidence about absence and is excluded. */
58
+ readonly framesWithTarget: number;
59
+ /** Of those, how many did not carry a box for the held track. */
60
+ readonly absentFrames: number;
61
+ /** Length of the gap currently open, in frames. */
62
+ readonly currentGapFrames: number;
63
+ /** Longest gap seen. */
64
+ readonly maxGapFrames: number;
65
+ /** How many CLOSED gaps had length 1, 2, 3, … The index is `length - 1`;
66
+ * lengths beyond {@link GAP_HISTOGRAM_LENGTHS} land in the final overflow
67
+ * slot. The SHAPE of this is the premise test — see the module docblock. */
68
+ readonly gapRunHistogram: readonly number[];
69
+ }
70
+ export declare const GAP_HISTOGRAM_LENGTHS = 8;
71
+ export declare const INITIAL_PRESENCE_TALLY: PresenceTally;
72
+ /**
73
+ * Count one frame on which a target was held.
74
+ *
75
+ * `present` is "the held track carried a box on THIS frame". A closing gap is
76
+ * binned at the moment it closes, which is why the open gap is reported
77
+ * separately (`currentGapFrames`) instead of being counted twice.
78
+ */
79
+ export declare function recordPresence(tally: PresenceTally, present: boolean): PresenceTally;
80
+ /** Flat, log-friendly view of both tallies. Every field is a number so the log
81
+ * line can be grouped and diffed per camera. */
82
+ export interface MeasurementSummary {
83
+ readonly frames: number;
84
+ readonly latencyMeanMs: number | null;
85
+ readonly latencyMinMs: number | null;
86
+ readonly latencyMaxMs: number | null;
87
+ readonly latencyBucketsMs: readonly number[];
88
+ readonly latencyBucketCounts: readonly number[];
89
+ readonly negativeAgeFrames: number;
90
+ readonly framesWithTarget: number;
91
+ readonly absentFrames: number;
92
+ readonly absentFraction: number | null;
93
+ readonly maxGapFrames: number;
94
+ readonly openGapFrames: number;
95
+ readonly gapRunHistogram: readonly number[];
96
+ }
97
+ export declare function summarize(latency: LatencyTally, presence: PresenceTally): MeasurementSummary;
@@ -0,0 +1,2 @@
1
+ export { AutotrackAddon } from './autotrack.addon.js';
2
+ export { AutotrackAddon as default } from './autotrack.addon.js';