@camstack/addon-post-analysis 1.2.48 → 1.2.49
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/{dist-DKhwOB5L.js → dist-BaqsyWbd.js} +328 -23
- package/dist/{dist-4NhN1aPx.mjs → dist-DYOxxL_e.mjs} +328 -23
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-XPzbKGXi.js → node-CdY4IvEy.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CD55gKkr.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-B-HELo_d.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BfiNBOyJ.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CiIJt_s8.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-DnhLbxcA.mjs → hostInit-wlxZFMRT.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +408 -74
- package/dist/pipeline-analytics/index.mjs +407 -73
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-BaqsyWbd.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_dist.__toESM(node_path);
|
|
@@ -6812,6 +6812,34 @@ function isEdgeAwareNewBest(current, candidate, hysteresis) {
|
|
|
6812
6812
|
//#endregion
|
|
6813
6813
|
//#region src/pipeline-analytics/pipeline/label-utils.ts
|
|
6814
6814
|
/**
|
|
6815
|
+
* Every macro class the pipeline can carry on a detection. A `label` exists to
|
|
6816
|
+
* say something FINER than the class — a species, a vehicle type, a plate read,
|
|
6817
|
+
* an identity — so a value drawn from this set is by definition not a label,
|
|
6818
|
+
* whatever produced it.
|
|
6819
|
+
*
|
|
6820
|
+
* Typed against the macro unions so a new macro class is a compile error here
|
|
6821
|
+
* rather than a silent hole. `package` is a first-level macro of the default
|
|
6822
|
+
* detector that predates `FirstLevelMacro`, hence the explicit member.
|
|
6823
|
+
*/
|
|
6824
|
+
var MACRO_CLASSES = new Set([
|
|
6825
|
+
"person",
|
|
6826
|
+
"vehicle",
|
|
6827
|
+
"animal",
|
|
6828
|
+
"package",
|
|
6829
|
+
"face",
|
|
6830
|
+
"plate",
|
|
6831
|
+
"audio"
|
|
6832
|
+
]);
|
|
6833
|
+
/**
|
|
6834
|
+
* Whether a class token is a macro class. EXACT match, no case folding: plate
|
|
6835
|
+
* OCR text is upper-case and identities are capitalised, so `PLATE` (a real
|
|
6836
|
+
* read) and `Person` (a real name) must survive while `plate` / `person` (the
|
|
6837
|
+
* taxonomy tokens) must not.
|
|
6838
|
+
*/
|
|
6839
|
+
function isMacroClass(value) {
|
|
6840
|
+
return MACRO_CLASSES.has(value);
|
|
6841
|
+
}
|
|
6842
|
+
/**
|
|
6815
6843
|
* Opaque internal label markers that carry no human-readable meaning and must
|
|
6816
6844
|
* never surface on an event/track. The face-embedding (arcface) step pushes a
|
|
6817
6845
|
* `embedding:<dim>d` marker (score 1.0) onto the parent's labels — it is a
|
|
@@ -6842,12 +6870,26 @@ function pickTopLabel(labels, excludeClass) {
|
|
|
6842
6870
|
/**
|
|
6843
6871
|
* Resolve the label to stamp on an ObjectEvent / track: an enrichment label
|
|
6844
6872
|
* (identity, plate text) takes precedence; otherwise the raw detector class
|
|
6845
|
-
* (`originalClass`) when it
|
|
6873
|
+
* (`originalClass`) when it is genuinely FINER than the macro class; otherwise
|
|
6874
|
+
* nothing.
|
|
6875
|
+
*
|
|
6876
|
+
* "Finer" is load-bearing, and `originalClass !== className` alone does not
|
|
6877
|
+
* establish it. The two fields come from different time bases — `className` is
|
|
6878
|
+
* the tracker's confidence-weighted LIFETIME class vote, `originalClass` is the
|
|
6879
|
+
* class of the detection matched into the track THIS frame — and outside
|
|
6880
|
+
* `verbosity === 'full'` the runner ships no `debug.originalClass` at all, so
|
|
6881
|
+
* the FrameProcessor substitutes `det.macroClass`. A person track that a single
|
|
6882
|
+
* animal detection matched into therefore arrived here as
|
|
6883
|
+
* `{ className: 'person', originalClass: 'animal' }` and got `label: 'animal'`
|
|
6884
|
+
* written on it: a COARSE class in the slot reserved for finer information.
|
|
6885
|
+
* A macro class is never an answer, so it is never a label.
|
|
6846
6886
|
*/
|
|
6847
6887
|
function resolveDetectionLabel(input) {
|
|
6848
6888
|
const top = pickTopLabel(input.labels, input.className);
|
|
6849
6889
|
if (top) return top;
|
|
6850
|
-
|
|
6890
|
+
const raw = input.originalClass;
|
|
6891
|
+
if (raw === void 0 || raw === input.className || isMacroClass(raw)) return void 0;
|
|
6892
|
+
return raw;
|
|
6851
6893
|
}
|
|
6852
6894
|
var DEFAULT_ZONE_TRANSITION_CONFIG = {
|
|
6853
6895
|
enterFrames: 2,
|
|
@@ -11840,9 +11882,24 @@ var TrackDetailDispatcher = class {
|
|
|
11840
11882
|
dev.scheduler.onResult(req.trackId, req.stepId, topScore, Date.now());
|
|
11841
11883
|
}
|
|
11842
11884
|
/**
|
|
11843
|
-
*
|
|
11844
|
-
*
|
|
11845
|
-
*
|
|
11885
|
+
* Walk the crop-source ladder for one request, stopping at the first rung that
|
|
11886
|
+
* produces a result. Returns the detail list, or `null` when every rung failed.
|
|
11887
|
+
*
|
|
11888
|
+
* 1. `frameHandle` — cheapest when it hits: no payload moves and the runner's
|
|
11889
|
+
* leaf children re-cut their ROI at native resolution. Tried first, always.
|
|
11890
|
+
* 2. `frameJpeg` — the freshest frame's FULL raster, fetched from the node that
|
|
11891
|
+
* owns the handle and handed to the runner UNCUT, so the rectangle still
|
|
11892
|
+
* comes from the single derivation (D52). This rung exists because rung 1 is
|
|
11893
|
+
* structurally unreachable for most dispatches: the decode worker's native
|
|
11894
|
+
* lease is 1.2 s and a tick + inference routinely lands outside it, while
|
|
11895
|
+
* the retained store holds the frame for 3 s.
|
|
11896
|
+
* 3. `cropJpeg` — the pre-cut tile. Last, because the caller decided its
|
|
11897
|
+
* rectangle and because in practice the same native miss that emptied rung 1
|
|
11898
|
+
* empties this one too.
|
|
11899
|
+
*
|
|
11900
|
+
* On D9/D18: the payload that crosses in rung 2 is a COMPRESSED JPEG fetched on
|
|
11901
|
+
* demand, per TRACK per dispatch (≤2 in flight per device, ~1 Hz) and only after
|
|
11902
|
+
* rung 1 missed — never a frame-rate stream, and never raw pixels.
|
|
11846
11903
|
*/
|
|
11847
11904
|
async runOnce(deviceId, dev, req, frame) {
|
|
11848
11905
|
const parent = {
|
|
@@ -11859,7 +11916,39 @@ var TrackDetailDispatcher = class {
|
|
|
11859
11916
|
}, frame.nodeId);
|
|
11860
11917
|
if (primary !== null) return primary.details;
|
|
11861
11918
|
} catch (err) {
|
|
11862
|
-
this.deps.logger.debug("detail primary call failed — trying
|
|
11919
|
+
this.deps.logger.debug("detail primary call failed — trying frame fallback", {
|
|
11920
|
+
tags: { deviceId },
|
|
11921
|
+
meta: {
|
|
11922
|
+
trackId: req.trackId,
|
|
11923
|
+
stepId: req.stepId,
|
|
11924
|
+
error: String(err)
|
|
11925
|
+
}
|
|
11926
|
+
});
|
|
11927
|
+
}
|
|
11928
|
+
if (this.deps.captureFullFrameBase64 !== void 0 && frame.frameHandle !== void 0) try {
|
|
11929
|
+
const full = await this.deps.captureFullFrameBase64(frame);
|
|
11930
|
+
if (full !== null) if (full.width !== frame.frameWidth || full.height !== frame.frameHeight) this.deps.logger.warn("detail full-frame fallback refused — frame is not the bbox scale", {
|
|
11931
|
+
tags: { deviceId },
|
|
11932
|
+
meta: {
|
|
11933
|
+
trackId: req.trackId,
|
|
11934
|
+
stepId: req.stepId,
|
|
11935
|
+
frameWidth: full.width,
|
|
11936
|
+
frameHeight: full.height,
|
|
11937
|
+
bboxFrameWidth: frame.frameWidth,
|
|
11938
|
+
bboxFrameHeight: frame.frameHeight
|
|
11939
|
+
}
|
|
11940
|
+
});
|
|
11941
|
+
else {
|
|
11942
|
+
const derived = await this.deps.runDetailSubtree({
|
|
11943
|
+
deviceId,
|
|
11944
|
+
frameJpeg: full.base64,
|
|
11945
|
+
parent,
|
|
11946
|
+
steps
|
|
11947
|
+
}, frame.nodeId);
|
|
11948
|
+
if (derived !== null) return derived.details;
|
|
11949
|
+
}
|
|
11950
|
+
} catch (err) {
|
|
11951
|
+
this.deps.logger.debug("detail full-frame fallback call failed", {
|
|
11863
11952
|
tags: { deviceId },
|
|
11864
11953
|
meta: {
|
|
11865
11954
|
trackId: req.trackId,
|
|
@@ -11896,12 +11985,14 @@ var TrackDetailDispatcher = class {
|
|
|
11896
11985
|
const now = Date.now();
|
|
11897
11986
|
if (now - dev.lastFailWarnAt < FAIL_WARN_THROTTLE_MS) return;
|
|
11898
11987
|
dev.lastFailWarnAt = now;
|
|
11899
|
-
this.deps.logger.warn("runDetailSubtree produced no result (
|
|
11988
|
+
this.deps.logger.warn("runDetailSubtree produced no result (every crop source missed)", {
|
|
11900
11989
|
tags: { deviceId },
|
|
11901
11990
|
meta: {
|
|
11902
11991
|
trackId: req.trackId,
|
|
11903
11992
|
stepId: req.stepId,
|
|
11904
|
-
reason: req.reason
|
|
11993
|
+
reason: req.reason,
|
|
11994
|
+
triedFullFrame: this.deps.captureFullFrameBase64 !== void 0,
|
|
11995
|
+
triedCropJpeg: this.deps.captureCropBase64 !== void 0
|
|
11905
11996
|
}
|
|
11906
11997
|
});
|
|
11907
11998
|
}
|
|
@@ -18625,6 +18716,20 @@ function rowToSensorEvent(id, data) {
|
|
|
18625
18716
|
function isRecord(x) {
|
|
18626
18717
|
return x !== null && typeof x === "object" && !Array.isArray(x);
|
|
18627
18718
|
}
|
|
18719
|
+
/**
|
|
18720
|
+
* May the interval this attempt consumed be given back?
|
|
18721
|
+
*
|
|
18722
|
+
* Pure, and deliberately takes the CURRENT clock as well as the mark: a rollback
|
|
18723
|
+
* is only correct while this attempt is still the one the gate is standing on. A
|
|
18724
|
+
* later frame's pending mark, or a snapshot that landed in between, has already
|
|
18725
|
+
* moved the reference forward and restoring an older one would re-open a window
|
|
18726
|
+
* that is legitimately closed.
|
|
18727
|
+
*/
|
|
18728
|
+
function canRollbackSnapshot(mark, currentLastSnapshotAt) {
|
|
18729
|
+
if (mark === void 0) return false;
|
|
18730
|
+
if (mark.at !== currentLastSnapshotAt) return false;
|
|
18731
|
+
return mark.priorRollbacks < 3;
|
|
18732
|
+
}
|
|
18628
18733
|
//#endregion
|
|
18629
18734
|
//#region src/pipeline-analytics/store/recent-cursor.ts
|
|
18630
18735
|
function encodeRecentCursor(cursor) {
|
|
@@ -18958,6 +19063,8 @@ var TrackStore = class {
|
|
|
18958
19063
|
t.snapshots.push(snapshot);
|
|
18959
19064
|
t.lastSnapshotAt = snapshot.timestamp;
|
|
18960
19065
|
t.lastSnapshotBbox = { ...snapshot.position.bbox };
|
|
19066
|
+
delete t.snapshotPending;
|
|
19067
|
+
t.snapshotRollbacks = 0;
|
|
18961
19068
|
}
|
|
18962
19069
|
/**
|
|
18963
19070
|
* Synchronously advance the periodic-snapshot gate reference (clock + bbox) at
|
|
@@ -18969,18 +19076,57 @@ var TrackStore = class {
|
|
|
18969
19076
|
* snapshots. Mirrors the synchronous `lastFrameAtByTrack` advance for the
|
|
18970
19077
|
* rolling `lastFrame`.
|
|
18971
19078
|
*
|
|
18972
|
-
*
|
|
18973
|
-
*
|
|
18974
|
-
*
|
|
18975
|
-
*
|
|
18976
|
-
*
|
|
19079
|
+
* The advance is REVERSIBLE. It used to be permanent — this docblock said
|
|
19080
|
+
* "if the write later fails the slot is simply lost" — which made `snapshot`
|
|
19081
|
+
* the only media kind with no retry of any shape while every sibling advanced
|
|
19082
|
+
* its bookkeeping from the ids that actually persisted. See
|
|
19083
|
+
* {@link rollbackSnapshotPending} and `pipeline/snapshot-retry.ts`.
|
|
19084
|
+
*
|
|
19085
|
+
* `addSnapshot` re-stamps the same clock/bbox when the real snapshot lands, so
|
|
19086
|
+
* the two stay consistent. No-op for an unknown/expired track (the active entry
|
|
19087
|
+
* is dropped on expiry, so there is no separate map to leak).
|
|
18977
19088
|
*/
|
|
18978
19089
|
markSnapshotPending(trackId, timestamp, bbox) {
|
|
18979
19090
|
const t = this.active.get(trackId);
|
|
18980
19091
|
if (!t) return;
|
|
19092
|
+
t.snapshotPending = {
|
|
19093
|
+
at: timestamp,
|
|
19094
|
+
previous: {
|
|
19095
|
+
at: t.lastSnapshotAt,
|
|
19096
|
+
bbox: t.lastSnapshotBbox ? { ...t.lastSnapshotBbox } : void 0
|
|
19097
|
+
},
|
|
19098
|
+
priorRollbacks: t.snapshotRollbacks ?? 0
|
|
19099
|
+
};
|
|
18981
19100
|
t.lastSnapshotAt = timestamp;
|
|
18982
19101
|
t.lastSnapshotBbox = { ...bbox };
|
|
18983
19102
|
}
|
|
19103
|
+
/**
|
|
19104
|
+
* The capture decided at `timestamp` did NOT land: restore the gate reference
|
|
19105
|
+
* it displaced so the next frame re-evaluates instead of waiting out an
|
|
19106
|
+
* interval that was never used. Returns whether the slot was given back.
|
|
19107
|
+
*
|
|
19108
|
+
* Refused — deliberately — when the attempt is no longer the one the gate
|
|
19109
|
+
* stands on (a snapshot landed, or a newer frame already marked its own
|
|
19110
|
+
* pending), and once the bounded run of non-landings is spent
|
|
19111
|
+
* ({@link SNAPSHOT_MAX_ROLLBACKS}): a camera whose frames are permanently
|
|
19112
|
+
* unresolvable must not re-decide a snapshot on every frame forever.
|
|
19113
|
+
*/
|
|
19114
|
+
rollbackSnapshotPending(trackId, timestamp) {
|
|
19115
|
+
const t = this.active.get(trackId);
|
|
19116
|
+
if (!t) return false;
|
|
19117
|
+
const mark = t.snapshotPending;
|
|
19118
|
+
if (mark?.at !== timestamp) return false;
|
|
19119
|
+
if (!canRollbackSnapshot(mark, t.lastSnapshotAt)) {
|
|
19120
|
+
delete t.snapshotPending;
|
|
19121
|
+
return false;
|
|
19122
|
+
}
|
|
19123
|
+
t.lastSnapshotAt = mark.previous.at;
|
|
19124
|
+
if (mark.previous.bbox) t.lastSnapshotBbox = { ...mark.previous.bbox };
|
|
19125
|
+
else delete t.lastSnapshotBbox;
|
|
19126
|
+
t.snapshotRollbacks = mark.priorRollbacks + 1;
|
|
19127
|
+
delete t.snapshotPending;
|
|
19128
|
+
return true;
|
|
19129
|
+
}
|
|
18984
19130
|
lastSnapshotAt(trackId) {
|
|
18985
19131
|
return this.active.get(trackId)?.lastSnapshotAt ?? 0;
|
|
18986
19132
|
}
|
|
@@ -21101,6 +21247,35 @@ function createNativeFrameTransport(deps) {
|
|
|
21101
21247
|
};
|
|
21102
21248
|
};
|
|
21103
21249
|
const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
|
|
21250
|
+
const getFullFrameJpegBase64 = async (handle, maxWidth) => {
|
|
21251
|
+
if (!pipelineRunnerApi?.getNativeCrop) return null;
|
|
21252
|
+
try {
|
|
21253
|
+
const full = await pipelineRunnerApi.getNativeCrop.query({
|
|
21254
|
+
handle,
|
|
21255
|
+
bbox: {
|
|
21256
|
+
x: 0,
|
|
21257
|
+
y: 0,
|
|
21258
|
+
w: 1,
|
|
21259
|
+
h: 1
|
|
21260
|
+
},
|
|
21261
|
+
maxWidth,
|
|
21262
|
+
encodeJpeg: true
|
|
21263
|
+
}, require_dist.nodePin(handle.nodeId));
|
|
21264
|
+
if (!full?.jpeg || full.width <= 0 || full.height <= 0) return null;
|
|
21265
|
+
return {
|
|
21266
|
+
base64: full.jpeg,
|
|
21267
|
+
width: full.width,
|
|
21268
|
+
height: full.height,
|
|
21269
|
+
tier: full.tier === "ram-fullframe" ? "ram-fullframe" : "native"
|
|
21270
|
+
};
|
|
21271
|
+
} catch (err) {
|
|
21272
|
+
logger.debug("full-frame jpeg fetch failed", { meta: {
|
|
21273
|
+
nodeId: handle.nodeId,
|
|
21274
|
+
error: require_dist.errMsg(err)
|
|
21275
|
+
} });
|
|
21276
|
+
return null;
|
|
21277
|
+
}
|
|
21278
|
+
};
|
|
21104
21279
|
const cropMetricLogger = logger.child("NativeCrop");
|
|
21105
21280
|
let nativeHits = 0;
|
|
21106
21281
|
let nativeFallbacks = 0;
|
|
@@ -21151,6 +21326,7 @@ function createNativeFrameTransport(deps) {
|
|
|
21151
21326
|
getRemoteFrame,
|
|
21152
21327
|
getNativeFullFrameRgb,
|
|
21153
21328
|
getNativeKeyFrameRgb: fetchNativeFullFrameTiered,
|
|
21329
|
+
getFullFrameJpegBase64,
|
|
21154
21330
|
getNativeCropJpeg,
|
|
21155
21331
|
captureCrop: createCaptureCrop({
|
|
21156
21332
|
tryNativeCrop,
|
|
@@ -21515,23 +21691,47 @@ function wireSubscriptions(deps) {
|
|
|
21515
21691
|
* `composeEventKinds` builds the `listEventKinds` payload for a camera:
|
|
21516
21692
|
* (a) built-ins — motion + audio, always present;
|
|
21517
21693
|
* (b) detection classes actually OBSERVED on the device (track history);
|
|
21518
|
-
* (c) sensor kinds contributed by
|
|
21519
|
-
* `getLinkedDevices`), one descriptor per
|
|
21520
|
-
* the static `EVENT_KIND_BY_CAP` map.
|
|
21521
|
-
* (`getBindings`) — never a global cap
|
|
21694
|
+
* (c) sensor kinds contributed by the camera's OWN bindings and by its
|
|
21695
|
+
* LINKED devices (device-manager `getLinkedDevices`), one descriptor per
|
|
21696
|
+
* bound sensor cap present in the static `EVENT_KIND_BY_CAP` map.
|
|
21697
|
+
* Binding-driven per device (`getBindings`) — never a global cap
|
|
21698
|
+
* enumeration (D12).
|
|
21522
21699
|
*
|
|
21523
21700
|
* `LinkedCamerasCache` is the ingest-side reverse index (sensor device →
|
|
21524
|
-
* linked camera ids) with a TTL, so the
|
|
21525
|
-
* cheap at bus rate.
|
|
21701
|
+
* linked camera ids, plus the camera id set) with a TTL, so the
|
|
21702
|
+
* `DeviceStateChanged` handler stays cheap at bus rate.
|
|
21526
21703
|
*/
|
|
21527
21704
|
/**
|
|
21705
|
+
* Mapped sensor/control caps that a CAMERA must NOT contribute for ITSELF.
|
|
21706
|
+
*
|
|
21707
|
+
* A sensor cap bound on a camera makes the camera both the source and the
|
|
21708
|
+
* subject — a doorbell wrapper on camera 615, a native Reolink doorbell, a
|
|
21709
|
+
* camera-side button. Those have no other route into the per-camera event
|
|
21710
|
+
* substrate, so the camera includes itself.
|
|
21711
|
+
*
|
|
21712
|
+
* `motion` is the exception, and today the ONLY one: camera motion already
|
|
21713
|
+
* has a dedicated camera-native path (`MotionAnalysis` /
|
|
21714
|
+
* `MotionOnMotionChanged` → `MotionEvent` rows under the built-in `motion`
|
|
21715
|
+
* taxonomy lane). Every real camera provider registers a native `motion` cap
|
|
21716
|
+
* on the camera device, so self-inclusion would mint a SECOND, differently
|
|
21717
|
+
* named copy (`motion-sensor`) of every motion transition, at motion rate.
|
|
21718
|
+
*
|
|
21719
|
+
* Add a cap here only when the camera already emits that event through
|
|
21720
|
+
* another ingest path — never to reduce noise.
|
|
21721
|
+
*/
|
|
21722
|
+
var SELF_CAMERA_EXCLUDED_CAPS = new Set(["motion"]);
|
|
21723
|
+
/** True when a camera may contribute `capName` for itself. */
|
|
21724
|
+
function selfCameraMayEmit(capName) {
|
|
21725
|
+
return !SELF_CAMERA_EXCLUDED_CAPS.has(capName);
|
|
21726
|
+
}
|
|
21727
|
+
/**
|
|
21528
21728
|
* Full event-kind taxonomy a camera CAN emit (config-derived):
|
|
21529
21729
|
* (a) motion built-in — always;
|
|
21530
21730
|
* (b) each enabled detection macro + its taxonomy subs (reverse COCO);
|
|
21531
21731
|
* (c) audio macro + its subs when the classifier is enabled;
|
|
21532
21732
|
* (d) package kinds when a package zone rule is enabled;
|
|
21533
|
-
* (e) sensor/control kinds from
|
|
21534
|
-
* per (kind, source deviceId).
|
|
21733
|
+
* (e) sensor/control kinds from the CAMERA'S OWN bindings and from its
|
|
21734
|
+
* LINKED devices (binding-driven), deduped per (kind, source deviceId).
|
|
21535
21735
|
* Every descriptor carries parentKind/level/iconId/labelKey via the single
|
|
21536
21736
|
* taxonomy dictionary — no color/icon is declared here.
|
|
21537
21737
|
*/
|
|
@@ -21566,6 +21766,7 @@ async function composeEventKinds(deps, deviceId) {
|
|
|
21566
21766
|
const audioP = attempt(deps, "audioEnabled", () => deps.audioEnabled(deviceId));
|
|
21567
21767
|
const packageP = attempt(deps, "packageZonesEnabled", async () => deps.packageZonesEnabled ? deps.packageZonesEnabled(deviceId) : false);
|
|
21568
21768
|
const linkedP = attempt(deps, "getLinkedDevices", () => deps.linkedDevices.getLinkedDevices({ deviceId }));
|
|
21769
|
+
const selfBindingsP = attempt(deps, "getBindings", () => deps.bindings.getBindings({ deviceId }));
|
|
21569
21770
|
pushKind("motion");
|
|
21570
21771
|
for (const macro of await macrosP ?? []) {
|
|
21571
21772
|
pushKind(macro);
|
|
@@ -21582,26 +21783,30 @@ async function composeEventKinds(deps, deviceId) {
|
|
|
21582
21783
|
const devices = (await linkedP)?.devices ?? [];
|
|
21583
21784
|
const bindingsPerDevice = await Promise.all(devices.map((linked) => attempt(deps, "getBindings", () => deps.bindings.getBindings({ deviceId: linked.deviceId }))));
|
|
21584
21785
|
const seen = /* @__PURE__ */ new Set();
|
|
21585
|
-
|
|
21586
|
-
|
|
21587
|
-
if (entries === void 0) continue;
|
|
21786
|
+
const pushSensorKinds = (sourceDeviceId, entries, isSelf) => {
|
|
21787
|
+
if (entries === void 0) return;
|
|
21588
21788
|
for (const { capName } of entries) {
|
|
21789
|
+
if (isSelf && !selfCameraMayEmit(capName)) continue;
|
|
21589
21790
|
const descriptor = require_dist.EVENT_KIND_BY_CAP[capName];
|
|
21590
21791
|
if (descriptor === void 0) continue;
|
|
21591
|
-
const dedupeKey = `${descriptor.kind}:${
|
|
21792
|
+
const dedupeKey = `${descriptor.kind}:${sourceDeviceId}`;
|
|
21592
21793
|
if (seen.has(dedupeKey)) continue;
|
|
21593
21794
|
seen.add(dedupeKey);
|
|
21594
21795
|
pushKind(descriptor.kind, {
|
|
21595
21796
|
capName,
|
|
21596
|
-
deviceId:
|
|
21797
|
+
deviceId: sourceDeviceId
|
|
21597
21798
|
});
|
|
21598
21799
|
}
|
|
21599
|
-
}
|
|
21800
|
+
};
|
|
21801
|
+
pushSensorKinds(deviceId, (await selfBindingsP)?.entries, true);
|
|
21802
|
+
for (const [index, linked] of devices.entries()) pushSensorKinds(linked.deviceId, bindingsPerDevice[index]?.entries, false);
|
|
21600
21803
|
return out;
|
|
21601
21804
|
}
|
|
21602
21805
|
var DEFAULT_CACHE_TTL_MS = 6e4;
|
|
21603
21806
|
/**
|
|
21604
|
-
* TTL-cached reverse index: source deviceId → camera ids it is linked to
|
|
21807
|
+
* TTL-cached reverse index: source deviceId → camera ids it is linked to,
|
|
21808
|
+
* plus the camera id SET the index was built from (so the ingest can ask
|
|
21809
|
+
* "is this device itself a camera" without a second enumeration).
|
|
21605
21810
|
* Rebuilds lazily (single-flight) when stale, so the `DeviceStateChanged`
|
|
21606
21811
|
* handler pays one map lookup per event in the common case.
|
|
21607
21812
|
*/
|
|
@@ -21609,6 +21814,8 @@ var LinkedCamerasCache = class {
|
|
|
21609
21814
|
deps;
|
|
21610
21815
|
ttlMs;
|
|
21611
21816
|
index = /* @__PURE__ */ new Map();
|
|
21817
|
+
/** Every camera id seen by the last successful build. */
|
|
21818
|
+
cameras = /* @__PURE__ */ new Set();
|
|
21612
21819
|
/** Ms timestamp of the last build; null = never built / invalidated. */
|
|
21613
21820
|
builtAt = null;
|
|
21614
21821
|
building = null;
|
|
@@ -21618,23 +21825,37 @@ var LinkedCamerasCache = class {
|
|
|
21618
21825
|
}
|
|
21619
21826
|
/** Camera ids linked to `sourceDeviceId` ([] when none). */
|
|
21620
21827
|
async camerasFor(sourceDeviceId, nowMs = Date.now()) {
|
|
21621
|
-
|
|
21622
|
-
this.building ??= this.rebuild(nowMs).finally(() => {
|
|
21623
|
-
this.building = null;
|
|
21624
|
-
});
|
|
21625
|
-
await this.building;
|
|
21626
|
-
}
|
|
21828
|
+
await this.ensureFresh(nowMs);
|
|
21627
21829
|
return this.index.get(sourceDeviceId) ?? [];
|
|
21628
21830
|
}
|
|
21831
|
+
/**
|
|
21832
|
+
* True when `deviceId` is ITSELF a camera. Answered off the same build as
|
|
21833
|
+
* {@link camerasFor} — a sensor cap bound on a camera needs both facts in
|
|
21834
|
+
* the same event, and a second enumeration would double the cost of the
|
|
21835
|
+
* ingest path for nothing.
|
|
21836
|
+
*/
|
|
21837
|
+
async isCamera(deviceId, nowMs = Date.now()) {
|
|
21838
|
+
await this.ensureFresh(nowMs);
|
|
21839
|
+
return this.cameras.has(deviceId);
|
|
21840
|
+
}
|
|
21629
21841
|
/** Drop the cached index (e.g. on link-topology change events). */
|
|
21630
21842
|
invalidate() {
|
|
21631
21843
|
this.builtAt = null;
|
|
21632
21844
|
}
|
|
21633
21845
|
/** Test/maintenance hook: replace the index directly. */
|
|
21634
|
-
seed(index, builtAt) {
|
|
21846
|
+
seed(index, builtAt, cameraIds = []) {
|
|
21635
21847
|
this.index = new Map(index);
|
|
21848
|
+
this.cameras = new Set(cameraIds);
|
|
21636
21849
|
this.builtAt = builtAt;
|
|
21637
21850
|
}
|
|
21851
|
+
/** Rebuild (single-flight) when never built or past the TTL. */
|
|
21852
|
+
async ensureFresh(nowMs) {
|
|
21853
|
+
if (this.builtAt !== null && nowMs - this.builtAt < this.ttlMs) return;
|
|
21854
|
+
this.building ??= this.rebuild(nowMs).finally(() => {
|
|
21855
|
+
this.building = null;
|
|
21856
|
+
});
|
|
21857
|
+
await this.building;
|
|
21858
|
+
}
|
|
21638
21859
|
async rebuild(nowMs) {
|
|
21639
21860
|
try {
|
|
21640
21861
|
const cameraIds = await this.deps.cameras.listCameraIds();
|
|
@@ -21651,6 +21872,7 @@ var LinkedCamerasCache = class {
|
|
|
21651
21872
|
this.deps.onError?.("getLinkedDevices", err);
|
|
21652
21873
|
}
|
|
21653
21874
|
this.index = next;
|
|
21875
|
+
this.cameras = new Set(cameraIds);
|
|
21654
21876
|
this.builtAt = nowMs;
|
|
21655
21877
|
} catch (err) {
|
|
21656
21878
|
this.deps.onError?.("listCameraIds", err);
|
|
@@ -21659,16 +21881,25 @@ var LinkedCamerasCache = class {
|
|
|
21659
21881
|
}
|
|
21660
21882
|
};
|
|
21661
21883
|
/**
|
|
21662
|
-
* One `DeviceStateChanged` → N history rows
|
|
21663
|
-
*
|
|
21664
|
-
*
|
|
21884
|
+
* One `DeviceStateChanged` → N history rows, one per camera the event belongs
|
|
21885
|
+
* to: the emitting device's LINKED cameras, plus the emitting device ITSELF
|
|
21886
|
+
* when it is a camera carrying the sensor cap (a doorbell wrapper bound on
|
|
21887
|
+
* camera 615 — see `SELF_CAMERA_EXCLUDED_CAPS` for the one cap that is not
|
|
21888
|
+
* self-ingested). The EVENT_KIND_BY_CAP gate exits first so non-sensor cap
|
|
21889
|
+
* churn costs one map lookup. Returns the number of rows inserted (0 when
|
|
21890
|
+
* unmapped, or when the device belongs to no camera at all).
|
|
21665
21891
|
* Telemetry-lossy by design (D8) — inserts are best-effort.
|
|
21666
21892
|
*/
|
|
21667
21893
|
async function ingestSensorStateChange(deps, data, timestamp) {
|
|
21668
21894
|
const descriptor = require_dist.EVENT_KIND_BY_CAP[data.capName];
|
|
21669
21895
|
if (descriptor === void 0) return 0;
|
|
21670
|
-
const
|
|
21671
|
-
|
|
21896
|
+
const linkedCameraIds = await deps.cache.camerasFor(data.deviceId);
|
|
21897
|
+
const isCamera = await deps.cache.isCamera(data.deviceId);
|
|
21898
|
+
const cameraIds = isCamera && selfCameraMayEmit(data.capName) && !linkedCameraIds.includes(data.deviceId) ? [data.deviceId, ...linkedCameraIds] : linkedCameraIds;
|
|
21899
|
+
if (cameraIds.length === 0) {
|
|
21900
|
+
deps.onDropped?.(isCamera ? "camera-native-cap" : "no-camera", descriptor.kind);
|
|
21901
|
+
return 0;
|
|
21902
|
+
}
|
|
21672
21903
|
const slice = data.slice;
|
|
21673
21904
|
const value = slice !== null && slice !== void 0 && typeof slice === "object" && !Array.isArray(slice) ? slice : null;
|
|
21674
21905
|
const makeId = deps.makeId ?? (() => `pa-sensor-${(0, node_crypto.randomUUID)()}`);
|
|
@@ -21804,6 +22035,8 @@ var SNAPSHOT_MAX_WIDTH = 960;
|
|
|
21804
22035
|
/** Bound the once-per-owner display-fallback memo so a long-lived process never
|
|
21805
22036
|
* leaks it. */
|
|
21806
22037
|
var MAX_LOGGED_DISPLAY_FALLBACKS = 5e3;
|
|
22038
|
+
/** One `pinned-frame resolve missed` line per device per minute. */
|
|
22039
|
+
var PINNED_MISS_WARN_THROTTLE_MS = 6e4;
|
|
21807
22040
|
/**
|
|
21808
22041
|
* Map a detection-frame pixel box (`fromW`×`fromH`, the ≤640 raster the tracker
|
|
21809
22042
|
* ran on) onto the native full frame (`toW`×`toH`, the 960-downscaled native
|
|
@@ -21889,6 +22122,8 @@ var EventMediaDispatcher = class {
|
|
|
21889
22122
|
/** Once-per-owner (`${eventId}:${kind}`) memo for the DISPLAY-crop fallback
|
|
21890
22123
|
* info log — see {@link logDisplayFallbackOnce}. */
|
|
21891
22124
|
loggedDisplayFallbacks = /* @__PURE__ */ new Set();
|
|
22125
|
+
/** Per-device throttle for {@link warnPinnedFrameMiss}. */
|
|
22126
|
+
lastPinnedMissWarnAt = /* @__PURE__ */ new Map();
|
|
21892
22127
|
constructor(deps) {
|
|
21893
22128
|
this.deps = deps;
|
|
21894
22129
|
this.sharedNativeFullFrame = createSharedFrameResolver((handle) => this.deps.getNativeFullFrameRgb(handle, SNAPSHOT_MAX_WIDTH));
|
|
@@ -21902,30 +22137,48 @@ var EventMediaDispatcher = class {
|
|
|
21902
22137
|
* by which time the handle's ring slot is usually recycled and the by-handle
|
|
21903
22138
|
* resolve in {@link captureForFrame} finds nothing (the 2026-07-23 boxed-tile
|
|
21904
22139
|
* regression). ~0.7MB per pinned frame, freed with the batch — bounded by the
|
|
21905
|
-
* lane. Returns `null` on a genuine live-window miss
|
|
21906
|
-
*
|
|
21907
|
-
*
|
|
22140
|
+
* lane. Returns `null` on a genuine live-window miss.
|
|
22141
|
+
*
|
|
22142
|
+
* Both miss paths are WARN, throttled to one line per device per minute. They
|
|
22143
|
+
* are the FIRST half of a pair whose second half (`capture batch lost`) has
|
|
22144
|
+
* always been a warn: a batch that lost its frame is reported with
|
|
22145
|
+
* `pinned: false`, and until 2026-08-07 the reason the pin itself failed was
|
|
22146
|
+
* either at debug (the throw) or nowhere at all (the null return). 383 batch
|
|
22147
|
+
* losses in 3 h had `pinned: false` on 100% of them and nothing said why.
|
|
21908
22148
|
*/
|
|
21909
22149
|
async resolvePinnedFrame(deviceId, frameHandle) {
|
|
21910
22150
|
try {
|
|
21911
22151
|
const decoded = await resolveFrame(frameHandle, { getRemoteFrame: this.deps.getRemoteFrame });
|
|
21912
|
-
if (!decoded)
|
|
22152
|
+
if (!decoded) {
|
|
22153
|
+
this.warnPinnedFrameMiss(deviceId, frameHandle, "recycled-before-pin");
|
|
22154
|
+
return null;
|
|
22155
|
+
}
|
|
21913
22156
|
return {
|
|
21914
22157
|
...decoded,
|
|
21915
22158
|
data: Buffer.from(decoded.data)
|
|
21916
22159
|
};
|
|
21917
22160
|
} catch (err) {
|
|
21918
|
-
this.
|
|
21919
|
-
tags: { deviceId },
|
|
21920
|
-
meta: {
|
|
21921
|
-
deviceId,
|
|
21922
|
-
shmId: frameHandle.shmId,
|
|
21923
|
-
error: String(err)
|
|
21924
|
-
}
|
|
21925
|
-
});
|
|
22161
|
+
this.warnPinnedFrameMiss(deviceId, frameHandle, "threw", String(err));
|
|
21926
22162
|
return null;
|
|
21927
22163
|
}
|
|
21928
22164
|
}
|
|
22165
|
+
/** One `pinned-frame resolve missed` line per device per minute. Throttled
|
|
22166
|
+
* because it fires per capture batch, and the batch's own loss line is not. */
|
|
22167
|
+
warnPinnedFrameMiss(deviceId, frameHandle, reason, error) {
|
|
22168
|
+
const now = Date.now();
|
|
22169
|
+
if (now - (this.lastPinnedMissWarnAt.get(deviceId) ?? 0) < PINNED_MISS_WARN_THROTTLE_MS) return;
|
|
22170
|
+
this.lastPinnedMissWarnAt.set(deviceId, now);
|
|
22171
|
+
this.deps.logger.warn("event media: pinned-frame resolve missed (live window)", {
|
|
22172
|
+
tags: { deviceId },
|
|
22173
|
+
meta: {
|
|
22174
|
+
reason,
|
|
22175
|
+
shmId: frameHandle.shmId,
|
|
22176
|
+
handle: `${frameHandle.shmId}#${frameHandle.slot}#${frameHandle.seq}`,
|
|
22177
|
+
handleNodeId: frameHandle.nodeId,
|
|
22178
|
+
...error !== void 0 ? { error } : {}
|
|
22179
|
+
}
|
|
22180
|
+
});
|
|
22181
|
+
}
|
|
21929
22182
|
/**
|
|
21930
22183
|
* WARN-log a whole-batch capture abort: every early return in
|
|
21931
22184
|
* {@link captureForFrame} loses ALL of the frame's targets at once —
|
|
@@ -22778,13 +23031,19 @@ var SyntheticTrackMaterializer = class {
|
|
|
22778
23031
|
* Fetch the linked camera's snapshot under a bounded timeout. A hanging
|
|
22779
23032
|
* snapshot cap is skipped (returns null + warns) so the track still lands;
|
|
22780
23033
|
* a null / failed fetch is likewise reported and skipped.
|
|
23034
|
+
*
|
|
23035
|
+
* Deliberately NOT `force`. `snapshot.cap.ts` defines `force` as the
|
|
23036
|
+
* operator's refresh button — *"On battery cams this WILL wake the camera —
|
|
23037
|
+
* accept the cost only when the user explicitly asks for it"* — and a
|
|
23038
|
+
* synthetic track is materialised from an EVENT, with nobody asking. Forcing
|
|
23039
|
+
* here walked straight past the snapshot addon's sleep gate and woke a
|
|
23040
|
+
* sleeping battery camera per event (roadmap 4f). Without it the cache
|
|
23041
|
+
* answers: ≤10 s old on a mains camera, and on a battery camera the
|
|
23042
|
+
* intentionally stale-but-honest frame that costs no wake.
|
|
22781
23043
|
*/
|
|
22782
23044
|
async fetchSnapshot(cameraId) {
|
|
22783
23045
|
try {
|
|
22784
|
-
const snap = await withTimeout(this.deps.snapshot.getSnapshot({
|
|
22785
|
-
deviceId: cameraId,
|
|
22786
|
-
force: true
|
|
22787
|
-
}), this.snapshotTimeoutMs);
|
|
23046
|
+
const snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), this.snapshotTimeoutMs);
|
|
22788
23047
|
if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"));
|
|
22789
23048
|
return snap;
|
|
22790
23049
|
} catch (err) {
|
|
@@ -25766,7 +26025,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
25766
26025
|
logger: logger.child("EventMediaDispatcher")
|
|
25767
26026
|
});
|
|
25768
26027
|
this.buildRecognizers(api, logger, stores, transport);
|
|
25769
|
-
this.buildDetailDispatcher(api, logger);
|
|
26028
|
+
this.buildDetailDispatcher(api, logger, transport);
|
|
25770
26029
|
const capProviders = this.buildAnalyticsProviders(api, logger);
|
|
25771
26030
|
await this.buildNotificationCenter(api, logger, stores);
|
|
25772
26031
|
await this.serveEventMediaDataPlane();
|
|
@@ -25949,7 +26208,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
25949
26208
|
let storage = this.ctx.kernel.storage;
|
|
25950
26209
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
25951
26210
|
if (mediaRoot) {
|
|
25952
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
26211
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-CdY4IvEy.js"));
|
|
25953
26212
|
storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
|
|
25954
26213
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
25955
26214
|
}
|
|
@@ -26214,7 +26473,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
26214
26473
|
* demand via `pipelineRunner.runDetailSubtree`. The dispatcher owns the
|
|
26215
26474
|
* per-device schedulers, concurrency cap, and 1 s tick; result routing +
|
|
26216
26475
|
* the pinned call + crop fallback are injected here. */
|
|
26217
|
-
buildDetailDispatcher(api, logger) {
|
|
26476
|
+
buildDetailDispatcher(api, logger, transport) {
|
|
26218
26477
|
const runnerApi = api.pipelineRunner;
|
|
26219
26478
|
this.detailDispatcher = new TrackDetailDispatcher({
|
|
26220
26479
|
logger: logger.child("DetailDispatcher"),
|
|
@@ -26224,6 +26483,15 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
26224
26483
|
return runnerApi.runDetailSubtree.mutate(input);
|
|
26225
26484
|
},
|
|
26226
26485
|
routeResults: (deviceId, trackId, details, frame) => this.routeDetailResults(deviceId, trackId, details, frame),
|
|
26486
|
+
captureFullFrameBase64: async (frame) => {
|
|
26487
|
+
if (frame.frameHandle === void 0) return null;
|
|
26488
|
+
const full = await transport.getFullFrameJpegBase64(frame.frameHandle, frame.frameWidth);
|
|
26489
|
+
return full === null ? null : {
|
|
26490
|
+
base64: full.base64,
|
|
26491
|
+
width: full.width,
|
|
26492
|
+
height: full.height
|
|
26493
|
+
};
|
|
26494
|
+
},
|
|
26227
26495
|
captureCropBase64: async (frame) => {
|
|
26228
26496
|
if (frame.frameHandle === void 0 || !this.captureCrop) return null;
|
|
26229
26497
|
const buf = await this.captureCrop(frame.frameHandle, { ...frame.bbox }, frame.frameWidth, frame.frameHeight, DETAIL_FALLBACK_CROP_PADDING);
|
|
@@ -26848,12 +27116,15 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
26848
27116
|
inPrevActive: false,
|
|
26849
27117
|
positionsCount: positionsCountById.get(id) ?? 1
|
|
26850
27118
|
}) === "resurrection") {
|
|
26851
|
-
log.info("track resumed", {
|
|
26852
|
-
|
|
26853
|
-
|
|
26854
|
-
|
|
26855
|
-
|
|
26856
|
-
|
|
27119
|
+
log.info("track resumed", {
|
|
27120
|
+
tags: { deviceId },
|
|
27121
|
+
meta: {
|
|
27122
|
+
trackId: id,
|
|
27123
|
+
className: t.className,
|
|
27124
|
+
source,
|
|
27125
|
+
resurrected: true
|
|
27126
|
+
}
|
|
27127
|
+
});
|
|
26857
27128
|
if (this.eventMediaDispatcher) {
|
|
26858
27129
|
this.residents.setLastFrameAt(deviceId, id, result.timestamp);
|
|
26859
27130
|
if (!this.residents.isFirstFrameLanded(id) && !this.residents.isFirstFramePending(id)) this.residents.markFirstFramePending(deviceId, id);
|
|
@@ -26988,10 +27259,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
26988
27259
|
let lostTrackCount = 0;
|
|
26989
27260
|
for (const id of prevIds) if (!currentTrackIds.has(id)) {
|
|
26990
27261
|
lostTrackCount += 1;
|
|
26991
|
-
log.info("track lost", {
|
|
26992
|
-
|
|
26993
|
-
|
|
26994
|
-
|
|
27262
|
+
log.info("track lost", {
|
|
27263
|
+
tags: { deviceId },
|
|
27264
|
+
meta: {
|
|
27265
|
+
trackId: id,
|
|
27266
|
+
source
|
|
27267
|
+
}
|
|
27268
|
+
});
|
|
26995
27269
|
}
|
|
26996
27270
|
this.suppressedBirths.retain(key, currentTrackIds);
|
|
26997
27271
|
this.deferredBirths.retain(key, currentTrackIds);
|
|
@@ -27228,6 +27502,19 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
27228
27502
|
this.residents.markFirstFrameLanded(deviceId, trackId);
|
|
27229
27503
|
}
|
|
27230
27504
|
for (const trackId of res.lastFrameTrackIds) this.residents.setLastFrameAt(deviceId, trackId, dispatchTimestamp);
|
|
27505
|
+
const landedSnapshots = new Set(res.storedSnapshots.map((s) => s.trackId));
|
|
27506
|
+
for (const target of snapshotTargets) {
|
|
27507
|
+
if (!target.appendSnapshot || landedSnapshots.has(target.trackId)) continue;
|
|
27508
|
+
const gaveBack = this.trackStore?.rollbackSnapshotPending(target.trackId, target.timestamp);
|
|
27509
|
+
log.warn("snapshot capture did not land", {
|
|
27510
|
+
tags: { deviceId },
|
|
27511
|
+
meta: {
|
|
27512
|
+
trackId: target.trackId,
|
|
27513
|
+
timestamp: target.timestamp,
|
|
27514
|
+
intervalReturned: gaveBack === true
|
|
27515
|
+
}
|
|
27516
|
+
});
|
|
27517
|
+
}
|
|
27231
27518
|
})
|
|
27232
27519
|
}).catch(() => {});
|
|
27233
27520
|
}
|
|
@@ -29026,8 +29313,16 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
29026
29313
|
}
|
|
29027
29314
|
/**
|
|
29028
29315
|
* Sensor-event ingest handler — `DeviceStateChanged` of a device exposing a
|
|
29029
|
-
* mapped sensor cap. Resolves the
|
|
29030
|
-
*
|
|
29316
|
+
* mapped sensor cap. Resolves the camera set through the TTL cache (the
|
|
29317
|
+
* device's LINKED cameras, plus the device itself when it is a camera
|
|
29318
|
+
* carrying the cap) and inserts ONE row per camera. Best-effort
|
|
29319
|
+
* (telemetry-lossy).
|
|
29320
|
+
*
|
|
29321
|
+
* D8/D11 boundary, stated plainly: the ride here is the LOSSY
|
|
29322
|
+
* `DeviceStateChanged` bus, and there is no reconcile that replays a missed
|
|
29323
|
+
* press from the durable `doorbell.getStatus.lastPressedAt`. A dropped bus
|
|
29324
|
+
* event is a missing history row and a missing notification — never wrong
|
|
29325
|
+
* durable state.
|
|
29031
29326
|
*/
|
|
29032
29327
|
async handleSensorStateChanged(data, timestamp) {
|
|
29033
29328
|
const store = this.sensorEventStore;
|
|
@@ -29037,7 +29332,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
29037
29332
|
await ingestSensorStateChange({
|
|
29038
29333
|
sink: store,
|
|
29039
29334
|
cache,
|
|
29040
|
-
onPersisted: (ev) => this.notificationCenter?.onSensorEventPersisted(ev)
|
|
29335
|
+
onPersisted: (ev) => this.notificationCenter?.onSensorEventPersisted(ev),
|
|
29336
|
+
onDropped: (reason, kind) => this.logSensorIngestDrop(data, reason, kind)
|
|
29041
29337
|
}, data, timestamp);
|
|
29042
29338
|
} catch (err) {
|
|
29043
29339
|
this.ctx.logger.warn("sensor-event ingest failed", {
|
|
@@ -29053,7 +29349,16 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
29053
29349
|
if (materializer === null || descriptor === void 0) return;
|
|
29054
29350
|
try {
|
|
29055
29351
|
const cameraIds = await cache.camerasFor(data.deviceId);
|
|
29056
|
-
if (cameraIds.length === 0)
|
|
29352
|
+
if (cameraIds.length === 0) {
|
|
29353
|
+
this.ctx.logger.debug("sensor synthetic-track projection skipped — no linked camera", {
|
|
29354
|
+
tags: { deviceId: data.deviceId },
|
|
29355
|
+
meta: {
|
|
29356
|
+
capName: data.capName,
|
|
29357
|
+
kind: descriptor.kind
|
|
29358
|
+
}
|
|
29359
|
+
});
|
|
29360
|
+
return;
|
|
29361
|
+
}
|
|
29057
29362
|
const producingDeviceName = await this.resolveProducingDeviceName(data.deviceId);
|
|
29058
29363
|
for (const cameraId of cameraIds) await materializer.materialize({
|
|
29059
29364
|
cameraId,
|
|
@@ -29074,6 +29379,35 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
29074
29379
|
}
|
|
29075
29380
|
}
|
|
29076
29381
|
/**
|
|
29382
|
+
* Say out loud that a MAPPED sensor cap ingested nothing, and why.
|
|
29383
|
+
*
|
|
29384
|
+
* BOTH branches are `debug`, deliberately. After the self-camera fix neither
|
|
29385
|
+
* is a surprise any more — they are the two designed steady states, and both
|
|
29386
|
+
* ride at bus rate:
|
|
29387
|
+
*
|
|
29388
|
+
* - `camera-native-cap` is EVERY camera's `motion` slice. Every provider
|
|
29389
|
+
* binds a native `motion` cap on the camera device and motion churns
|
|
29390
|
+
* continuously (camera 590 flaps at ~2 Hz), so an `info` here would be a
|
|
29391
|
+
* log flood measured in millions of lines a day — its own outage.
|
|
29392
|
+
* - `no-camera` is every sensor in the fleet that no camera links.
|
|
29393
|
+
*
|
|
29394
|
+
* What must never happen again is the branch being INVISIBLE: a doorbell
|
|
29395
|
+
* press that ingested nothing produced not one line for months. A debug line
|
|
29396
|
+
* carrying `deviceId`, `capName`, `kind` and `reason` answers "why is 615
|
|
29397
|
+
* dark" the moment anyone asks it.
|
|
29398
|
+
*/
|
|
29399
|
+
logSensorIngestDrop(data, reason, kind) {
|
|
29400
|
+
const message = reason === "camera-native-cap" ? "sensor-event ingest produced NO row — the camera already emits this cap through its own path" : "sensor-event ingest produced NO row — the device is not a camera and is linked to none";
|
|
29401
|
+
this.ctx.logger.debug(message, {
|
|
29402
|
+
tags: { deviceId: data.deviceId },
|
|
29403
|
+
meta: {
|
|
29404
|
+
capName: data.capName,
|
|
29405
|
+
kind,
|
|
29406
|
+
reason
|
|
29407
|
+
}
|
|
29408
|
+
});
|
|
29409
|
+
}
|
|
29410
|
+
/**
|
|
29077
29411
|
* Resolve the NAME of a device (the linked sensor/control that produced a
|
|
29078
29412
|
* synthetic event) via the device-manager cap. Best-effort — returns
|
|
29079
29413
|
* undefined on any lookup failure or an unknown device so the synthetic
|