@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as object, B as errMsg, C as buildEventKindDescriptor, E as defineCustomActions, F as readDeviceStateFrom, G as isDeviceScopedCap, H as DeviceType, I as subKindsOf, J as _enum, K as nodePin, L as vectorDimFromBase64, M as notificationRulesCapability, N as pipelineAnalyticsCapability, O as encodeVectorBase64, P as plateGalleryCapability, Q as number, R as videoclipsCapability, S as audioMetricsCapability, T as customAction, U as createEvent, V as BaseAddon, W as hydrateSchema, X as boolean, Y as array, Z as literal, _ as TimelapseRuleInputSchema, a as MACRO_LABELS, b as addonWidgetsSourceCapability, c as NcConditionDescriptorSchema, d as NcRuleSchema, et as record, f as NcSnoozeInputSchema, g as OpsLogEntrySchema, h as NcTaxonomySchema, i as EVENT_PAD_MS, j as kebabToCamel, k as faceGalleryCapability, l as NcRuleInputSchema, m as NcSnoozeSuppressedSchema, n as DEFAULT_EVENT_COLOR, nt as unknown, o as NC_CONDITION_CATALOG, p as NcSnoozeSchema, q as sleep, r as EVENT_KIND_BY_CAP, rt as EventCategory, s as NC_TAXONOMY, t as BaseDevice, tt as string, u as NcRulePatchSchema, v as TimelapseRuleSchema, w as cosineSimilarity, x as alarmPanelCapability, y as TrackSourceSchema, z as zoneAnalyticsCapability } from "../dist-
|
|
1
|
+
import { $ as object, B as errMsg, C as buildEventKindDescriptor, E as defineCustomActions, F as readDeviceStateFrom, G as isDeviceScopedCap, H as DeviceType, I as subKindsOf, J as _enum, K as nodePin, L as vectorDimFromBase64, M as notificationRulesCapability, N as pipelineAnalyticsCapability, O as encodeVectorBase64, P as plateGalleryCapability, Q as number, R as videoclipsCapability, S as audioMetricsCapability, T as customAction, U as createEvent, V as BaseAddon, W as hydrateSchema, X as boolean, Y as array, Z as literal, _ as TimelapseRuleInputSchema, a as MACRO_LABELS, b as addonWidgetsSourceCapability, c as NcConditionDescriptorSchema, d as NcRuleSchema, et as record, f as NcSnoozeInputSchema, g as OpsLogEntrySchema, h as NcTaxonomySchema, i as EVENT_PAD_MS, j as kebabToCamel, k as faceGalleryCapability, l as NcRuleInputSchema, m as NcSnoozeSuppressedSchema, n as DEFAULT_EVENT_COLOR, nt as unknown, o as NC_CONDITION_CATALOG, p as NcSnoozeSchema, q as sleep, r as EVENT_KIND_BY_CAP, rt as EventCategory, s as NC_TAXONOMY, t as BaseDevice, tt as string, u as NcRulePatchSchema, v as TimelapseRuleSchema, w as cosineSimilarity, x as alarmPanelCapability, y as TrackSourceSchema, z as zoneAnalyticsCapability } from "../dist-DYOxxL_e.mjs";
|
|
2
2
|
import { promises } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -6806,6 +6806,34 @@ function isEdgeAwareNewBest(current, candidate, hysteresis) {
|
|
|
6806
6806
|
//#endregion
|
|
6807
6807
|
//#region src/pipeline-analytics/pipeline/label-utils.ts
|
|
6808
6808
|
/**
|
|
6809
|
+
* Every macro class the pipeline can carry on a detection. A `label` exists to
|
|
6810
|
+
* say something FINER than the class — a species, a vehicle type, a plate read,
|
|
6811
|
+
* an identity — so a value drawn from this set is by definition not a label,
|
|
6812
|
+
* whatever produced it.
|
|
6813
|
+
*
|
|
6814
|
+
* Typed against the macro unions so a new macro class is a compile error here
|
|
6815
|
+
* rather than a silent hole. `package` is a first-level macro of the default
|
|
6816
|
+
* detector that predates `FirstLevelMacro`, hence the explicit member.
|
|
6817
|
+
*/
|
|
6818
|
+
var MACRO_CLASSES = new Set([
|
|
6819
|
+
"person",
|
|
6820
|
+
"vehicle",
|
|
6821
|
+
"animal",
|
|
6822
|
+
"package",
|
|
6823
|
+
"face",
|
|
6824
|
+
"plate",
|
|
6825
|
+
"audio"
|
|
6826
|
+
]);
|
|
6827
|
+
/**
|
|
6828
|
+
* Whether a class token is a macro class. EXACT match, no case folding: plate
|
|
6829
|
+
* OCR text is upper-case and identities are capitalised, so `PLATE` (a real
|
|
6830
|
+
* read) and `Person` (a real name) must survive while `plate` / `person` (the
|
|
6831
|
+
* taxonomy tokens) must not.
|
|
6832
|
+
*/
|
|
6833
|
+
function isMacroClass(value) {
|
|
6834
|
+
return MACRO_CLASSES.has(value);
|
|
6835
|
+
}
|
|
6836
|
+
/**
|
|
6809
6837
|
* Opaque internal label markers that carry no human-readable meaning and must
|
|
6810
6838
|
* never surface on an event/track. The face-embedding (arcface) step pushes a
|
|
6811
6839
|
* `embedding:<dim>d` marker (score 1.0) onto the parent's labels — it is a
|
|
@@ -6836,12 +6864,26 @@ function pickTopLabel(labels, excludeClass) {
|
|
|
6836
6864
|
/**
|
|
6837
6865
|
* Resolve the label to stamp on an ObjectEvent / track: an enrichment label
|
|
6838
6866
|
* (identity, plate text) takes precedence; otherwise the raw detector class
|
|
6839
|
-
* (`originalClass`) when it
|
|
6867
|
+
* (`originalClass`) when it is genuinely FINER than the macro class; otherwise
|
|
6868
|
+
* nothing.
|
|
6869
|
+
*
|
|
6870
|
+
* "Finer" is load-bearing, and `originalClass !== className` alone does not
|
|
6871
|
+
* establish it. The two fields come from different time bases — `className` is
|
|
6872
|
+
* the tracker's confidence-weighted LIFETIME class vote, `originalClass` is the
|
|
6873
|
+
* class of the detection matched into the track THIS frame — and outside
|
|
6874
|
+
* `verbosity === 'full'` the runner ships no `debug.originalClass` at all, so
|
|
6875
|
+
* the FrameProcessor substitutes `det.macroClass`. A person track that a single
|
|
6876
|
+
* animal detection matched into therefore arrived here as
|
|
6877
|
+
* `{ className: 'person', originalClass: 'animal' }` and got `label: 'animal'`
|
|
6878
|
+
* written on it: a COARSE class in the slot reserved for finer information.
|
|
6879
|
+
* A macro class is never an answer, so it is never a label.
|
|
6840
6880
|
*/
|
|
6841
6881
|
function resolveDetectionLabel(input) {
|
|
6842
6882
|
const top = pickTopLabel(input.labels, input.className);
|
|
6843
6883
|
if (top) return top;
|
|
6844
|
-
|
|
6884
|
+
const raw = input.originalClass;
|
|
6885
|
+
if (raw === void 0 || raw === input.className || isMacroClass(raw)) return void 0;
|
|
6886
|
+
return raw;
|
|
6845
6887
|
}
|
|
6846
6888
|
var DEFAULT_ZONE_TRANSITION_CONFIG = {
|
|
6847
6889
|
enterFrames: 2,
|
|
@@ -11834,9 +11876,24 @@ var TrackDetailDispatcher = class {
|
|
|
11834
11876
|
dev.scheduler.onResult(req.trackId, req.stepId, topScore, Date.now());
|
|
11835
11877
|
}
|
|
11836
11878
|
/**
|
|
11837
|
-
*
|
|
11838
|
-
*
|
|
11839
|
-
*
|
|
11879
|
+
* Walk the crop-source ladder for one request, stopping at the first rung that
|
|
11880
|
+
* produces a result. Returns the detail list, or `null` when every rung failed.
|
|
11881
|
+
*
|
|
11882
|
+
* 1. `frameHandle` — cheapest when it hits: no payload moves and the runner's
|
|
11883
|
+
* leaf children re-cut their ROI at native resolution. Tried first, always.
|
|
11884
|
+
* 2. `frameJpeg` — the freshest frame's FULL raster, fetched from the node that
|
|
11885
|
+
* owns the handle and handed to the runner UNCUT, so the rectangle still
|
|
11886
|
+
* comes from the single derivation (D52). This rung exists because rung 1 is
|
|
11887
|
+
* structurally unreachable for most dispatches: the decode worker's native
|
|
11888
|
+
* lease is 1.2 s and a tick + inference routinely lands outside it, while
|
|
11889
|
+
* the retained store holds the frame for 3 s.
|
|
11890
|
+
* 3. `cropJpeg` — the pre-cut tile. Last, because the caller decided its
|
|
11891
|
+
* rectangle and because in practice the same native miss that emptied rung 1
|
|
11892
|
+
* empties this one too.
|
|
11893
|
+
*
|
|
11894
|
+
* On D9/D18: the payload that crosses in rung 2 is a COMPRESSED JPEG fetched on
|
|
11895
|
+
* demand, per TRACK per dispatch (≤2 in flight per device, ~1 Hz) and only after
|
|
11896
|
+
* rung 1 missed — never a frame-rate stream, and never raw pixels.
|
|
11840
11897
|
*/
|
|
11841
11898
|
async runOnce(deviceId, dev, req, frame) {
|
|
11842
11899
|
const parent = {
|
|
@@ -11853,7 +11910,39 @@ var TrackDetailDispatcher = class {
|
|
|
11853
11910
|
}, frame.nodeId);
|
|
11854
11911
|
if (primary !== null) return primary.details;
|
|
11855
11912
|
} catch (err) {
|
|
11856
|
-
this.deps.logger.debug("detail primary call failed — trying
|
|
11913
|
+
this.deps.logger.debug("detail primary call failed — trying frame fallback", {
|
|
11914
|
+
tags: { deviceId },
|
|
11915
|
+
meta: {
|
|
11916
|
+
trackId: req.trackId,
|
|
11917
|
+
stepId: req.stepId,
|
|
11918
|
+
error: String(err)
|
|
11919
|
+
}
|
|
11920
|
+
});
|
|
11921
|
+
}
|
|
11922
|
+
if (this.deps.captureFullFrameBase64 !== void 0 && frame.frameHandle !== void 0) try {
|
|
11923
|
+
const full = await this.deps.captureFullFrameBase64(frame);
|
|
11924
|
+
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", {
|
|
11925
|
+
tags: { deviceId },
|
|
11926
|
+
meta: {
|
|
11927
|
+
trackId: req.trackId,
|
|
11928
|
+
stepId: req.stepId,
|
|
11929
|
+
frameWidth: full.width,
|
|
11930
|
+
frameHeight: full.height,
|
|
11931
|
+
bboxFrameWidth: frame.frameWidth,
|
|
11932
|
+
bboxFrameHeight: frame.frameHeight
|
|
11933
|
+
}
|
|
11934
|
+
});
|
|
11935
|
+
else {
|
|
11936
|
+
const derived = await this.deps.runDetailSubtree({
|
|
11937
|
+
deviceId,
|
|
11938
|
+
frameJpeg: full.base64,
|
|
11939
|
+
parent,
|
|
11940
|
+
steps
|
|
11941
|
+
}, frame.nodeId);
|
|
11942
|
+
if (derived !== null) return derived.details;
|
|
11943
|
+
}
|
|
11944
|
+
} catch (err) {
|
|
11945
|
+
this.deps.logger.debug("detail full-frame fallback call failed", {
|
|
11857
11946
|
tags: { deviceId },
|
|
11858
11947
|
meta: {
|
|
11859
11948
|
trackId: req.trackId,
|
|
@@ -11890,12 +11979,14 @@ var TrackDetailDispatcher = class {
|
|
|
11890
11979
|
const now = Date.now();
|
|
11891
11980
|
if (now - dev.lastFailWarnAt < FAIL_WARN_THROTTLE_MS) return;
|
|
11892
11981
|
dev.lastFailWarnAt = now;
|
|
11893
|
-
this.deps.logger.warn("runDetailSubtree produced no result (
|
|
11982
|
+
this.deps.logger.warn("runDetailSubtree produced no result (every crop source missed)", {
|
|
11894
11983
|
tags: { deviceId },
|
|
11895
11984
|
meta: {
|
|
11896
11985
|
trackId: req.trackId,
|
|
11897
11986
|
stepId: req.stepId,
|
|
11898
|
-
reason: req.reason
|
|
11987
|
+
reason: req.reason,
|
|
11988
|
+
triedFullFrame: this.deps.captureFullFrameBase64 !== void 0,
|
|
11989
|
+
triedCropJpeg: this.deps.captureCropBase64 !== void 0
|
|
11899
11990
|
}
|
|
11900
11991
|
});
|
|
11901
11992
|
}
|
|
@@ -18619,6 +18710,20 @@ function rowToSensorEvent(id, data) {
|
|
|
18619
18710
|
function isRecord(x) {
|
|
18620
18711
|
return x !== null && typeof x === "object" && !Array.isArray(x);
|
|
18621
18712
|
}
|
|
18713
|
+
/**
|
|
18714
|
+
* May the interval this attempt consumed be given back?
|
|
18715
|
+
*
|
|
18716
|
+
* Pure, and deliberately takes the CURRENT clock as well as the mark: a rollback
|
|
18717
|
+
* is only correct while this attempt is still the one the gate is standing on. A
|
|
18718
|
+
* later frame's pending mark, or a snapshot that landed in between, has already
|
|
18719
|
+
* moved the reference forward and restoring an older one would re-open a window
|
|
18720
|
+
* that is legitimately closed.
|
|
18721
|
+
*/
|
|
18722
|
+
function canRollbackSnapshot(mark, currentLastSnapshotAt) {
|
|
18723
|
+
if (mark === void 0) return false;
|
|
18724
|
+
if (mark.at !== currentLastSnapshotAt) return false;
|
|
18725
|
+
return mark.priorRollbacks < 3;
|
|
18726
|
+
}
|
|
18622
18727
|
//#endregion
|
|
18623
18728
|
//#region src/pipeline-analytics/store/recent-cursor.ts
|
|
18624
18729
|
function encodeRecentCursor(cursor) {
|
|
@@ -18952,6 +19057,8 @@ var TrackStore = class {
|
|
|
18952
19057
|
t.snapshots.push(snapshot);
|
|
18953
19058
|
t.lastSnapshotAt = snapshot.timestamp;
|
|
18954
19059
|
t.lastSnapshotBbox = { ...snapshot.position.bbox };
|
|
19060
|
+
delete t.snapshotPending;
|
|
19061
|
+
t.snapshotRollbacks = 0;
|
|
18955
19062
|
}
|
|
18956
19063
|
/**
|
|
18957
19064
|
* Synchronously advance the periodic-snapshot gate reference (clock + bbox) at
|
|
@@ -18963,18 +19070,57 @@ var TrackStore = class {
|
|
|
18963
19070
|
* snapshots. Mirrors the synchronous `lastFrameAtByTrack` advance for the
|
|
18964
19071
|
* rolling `lastFrame`.
|
|
18965
19072
|
*
|
|
18966
|
-
*
|
|
18967
|
-
*
|
|
18968
|
-
*
|
|
18969
|
-
*
|
|
18970
|
-
*
|
|
19073
|
+
* The advance is REVERSIBLE. It used to be permanent — this docblock said
|
|
19074
|
+
* "if the write later fails the slot is simply lost" — which made `snapshot`
|
|
19075
|
+
* the only media kind with no retry of any shape while every sibling advanced
|
|
19076
|
+
* its bookkeeping from the ids that actually persisted. See
|
|
19077
|
+
* {@link rollbackSnapshotPending} and `pipeline/snapshot-retry.ts`.
|
|
19078
|
+
*
|
|
19079
|
+
* `addSnapshot` re-stamps the same clock/bbox when the real snapshot lands, so
|
|
19080
|
+
* the two stay consistent. No-op for an unknown/expired track (the active entry
|
|
19081
|
+
* is dropped on expiry, so there is no separate map to leak).
|
|
18971
19082
|
*/
|
|
18972
19083
|
markSnapshotPending(trackId, timestamp, bbox) {
|
|
18973
19084
|
const t = this.active.get(trackId);
|
|
18974
19085
|
if (!t) return;
|
|
19086
|
+
t.snapshotPending = {
|
|
19087
|
+
at: timestamp,
|
|
19088
|
+
previous: {
|
|
19089
|
+
at: t.lastSnapshotAt,
|
|
19090
|
+
bbox: t.lastSnapshotBbox ? { ...t.lastSnapshotBbox } : void 0
|
|
19091
|
+
},
|
|
19092
|
+
priorRollbacks: t.snapshotRollbacks ?? 0
|
|
19093
|
+
};
|
|
18975
19094
|
t.lastSnapshotAt = timestamp;
|
|
18976
19095
|
t.lastSnapshotBbox = { ...bbox };
|
|
18977
19096
|
}
|
|
19097
|
+
/**
|
|
19098
|
+
* The capture decided at `timestamp` did NOT land: restore the gate reference
|
|
19099
|
+
* it displaced so the next frame re-evaluates instead of waiting out an
|
|
19100
|
+
* interval that was never used. Returns whether the slot was given back.
|
|
19101
|
+
*
|
|
19102
|
+
* Refused — deliberately — when the attempt is no longer the one the gate
|
|
19103
|
+
* stands on (a snapshot landed, or a newer frame already marked its own
|
|
19104
|
+
* pending), and once the bounded run of non-landings is spent
|
|
19105
|
+
* ({@link SNAPSHOT_MAX_ROLLBACKS}): a camera whose frames are permanently
|
|
19106
|
+
* unresolvable must not re-decide a snapshot on every frame forever.
|
|
19107
|
+
*/
|
|
19108
|
+
rollbackSnapshotPending(trackId, timestamp) {
|
|
19109
|
+
const t = this.active.get(trackId);
|
|
19110
|
+
if (!t) return false;
|
|
19111
|
+
const mark = t.snapshotPending;
|
|
19112
|
+
if (mark?.at !== timestamp) return false;
|
|
19113
|
+
if (!canRollbackSnapshot(mark, t.lastSnapshotAt)) {
|
|
19114
|
+
delete t.snapshotPending;
|
|
19115
|
+
return false;
|
|
19116
|
+
}
|
|
19117
|
+
t.lastSnapshotAt = mark.previous.at;
|
|
19118
|
+
if (mark.previous.bbox) t.lastSnapshotBbox = { ...mark.previous.bbox };
|
|
19119
|
+
else delete t.lastSnapshotBbox;
|
|
19120
|
+
t.snapshotRollbacks = mark.priorRollbacks + 1;
|
|
19121
|
+
delete t.snapshotPending;
|
|
19122
|
+
return true;
|
|
19123
|
+
}
|
|
18978
19124
|
lastSnapshotAt(trackId) {
|
|
18979
19125
|
return this.active.get(trackId)?.lastSnapshotAt ?? 0;
|
|
18980
19126
|
}
|
|
@@ -21095,6 +21241,35 @@ function createNativeFrameTransport(deps) {
|
|
|
21095
21241
|
};
|
|
21096
21242
|
};
|
|
21097
21243
|
const getNativeFullFrameRgb = async (handle, maxWidth) => (await fetchNativeFullFrameTiered(handle, maxWidth))?.frame ?? null;
|
|
21244
|
+
const getFullFrameJpegBase64 = async (handle, maxWidth) => {
|
|
21245
|
+
if (!pipelineRunnerApi?.getNativeCrop) return null;
|
|
21246
|
+
try {
|
|
21247
|
+
const full = await pipelineRunnerApi.getNativeCrop.query({
|
|
21248
|
+
handle,
|
|
21249
|
+
bbox: {
|
|
21250
|
+
x: 0,
|
|
21251
|
+
y: 0,
|
|
21252
|
+
w: 1,
|
|
21253
|
+
h: 1
|
|
21254
|
+
},
|
|
21255
|
+
maxWidth,
|
|
21256
|
+
encodeJpeg: true
|
|
21257
|
+
}, nodePin(handle.nodeId));
|
|
21258
|
+
if (!full?.jpeg || full.width <= 0 || full.height <= 0) return null;
|
|
21259
|
+
return {
|
|
21260
|
+
base64: full.jpeg,
|
|
21261
|
+
width: full.width,
|
|
21262
|
+
height: full.height,
|
|
21263
|
+
tier: full.tier === "ram-fullframe" ? "ram-fullframe" : "native"
|
|
21264
|
+
};
|
|
21265
|
+
} catch (err) {
|
|
21266
|
+
logger.debug("full-frame jpeg fetch failed", { meta: {
|
|
21267
|
+
nodeId: handle.nodeId,
|
|
21268
|
+
error: errMsg(err)
|
|
21269
|
+
} });
|
|
21270
|
+
return null;
|
|
21271
|
+
}
|
|
21272
|
+
};
|
|
21098
21273
|
const cropMetricLogger = logger.child("NativeCrop");
|
|
21099
21274
|
let nativeHits = 0;
|
|
21100
21275
|
let nativeFallbacks = 0;
|
|
@@ -21145,6 +21320,7 @@ function createNativeFrameTransport(deps) {
|
|
|
21145
21320
|
getRemoteFrame,
|
|
21146
21321
|
getNativeFullFrameRgb,
|
|
21147
21322
|
getNativeKeyFrameRgb: fetchNativeFullFrameTiered,
|
|
21323
|
+
getFullFrameJpegBase64,
|
|
21148
21324
|
getNativeCropJpeg,
|
|
21149
21325
|
captureCrop: createCaptureCrop({
|
|
21150
21326
|
tryNativeCrop,
|
|
@@ -21509,23 +21685,47 @@ function wireSubscriptions(deps) {
|
|
|
21509
21685
|
* `composeEventKinds` builds the `listEventKinds` payload for a camera:
|
|
21510
21686
|
* (a) built-ins — motion + audio, always present;
|
|
21511
21687
|
* (b) detection classes actually OBSERVED on the device (track history);
|
|
21512
|
-
* (c) sensor kinds contributed by
|
|
21513
|
-
* `getLinkedDevices`), one descriptor per
|
|
21514
|
-
* the static `EVENT_KIND_BY_CAP` map.
|
|
21515
|
-
* (`getBindings`) — never a global cap
|
|
21688
|
+
* (c) sensor kinds contributed by the camera's OWN bindings and by its
|
|
21689
|
+
* LINKED devices (device-manager `getLinkedDevices`), one descriptor per
|
|
21690
|
+
* bound sensor cap present in the static `EVENT_KIND_BY_CAP` map.
|
|
21691
|
+
* Binding-driven per device (`getBindings`) — never a global cap
|
|
21692
|
+
* enumeration (D12).
|
|
21516
21693
|
*
|
|
21517
21694
|
* `LinkedCamerasCache` is the ingest-side reverse index (sensor device →
|
|
21518
|
-
* linked camera ids) with a TTL, so the
|
|
21519
|
-
* cheap at bus rate.
|
|
21695
|
+
* linked camera ids, plus the camera id set) with a TTL, so the
|
|
21696
|
+
* `DeviceStateChanged` handler stays cheap at bus rate.
|
|
21520
21697
|
*/
|
|
21521
21698
|
/**
|
|
21699
|
+
* Mapped sensor/control caps that a CAMERA must NOT contribute for ITSELF.
|
|
21700
|
+
*
|
|
21701
|
+
* A sensor cap bound on a camera makes the camera both the source and the
|
|
21702
|
+
* subject — a doorbell wrapper on camera 615, a native Reolink doorbell, a
|
|
21703
|
+
* camera-side button. Those have no other route into the per-camera event
|
|
21704
|
+
* substrate, so the camera includes itself.
|
|
21705
|
+
*
|
|
21706
|
+
* `motion` is the exception, and today the ONLY one: camera motion already
|
|
21707
|
+
* has a dedicated camera-native path (`MotionAnalysis` /
|
|
21708
|
+
* `MotionOnMotionChanged` → `MotionEvent` rows under the built-in `motion`
|
|
21709
|
+
* taxonomy lane). Every real camera provider registers a native `motion` cap
|
|
21710
|
+
* on the camera device, so self-inclusion would mint a SECOND, differently
|
|
21711
|
+
* named copy (`motion-sensor`) of every motion transition, at motion rate.
|
|
21712
|
+
*
|
|
21713
|
+
* Add a cap here only when the camera already emits that event through
|
|
21714
|
+
* another ingest path — never to reduce noise.
|
|
21715
|
+
*/
|
|
21716
|
+
var SELF_CAMERA_EXCLUDED_CAPS = new Set(["motion"]);
|
|
21717
|
+
/** True when a camera may contribute `capName` for itself. */
|
|
21718
|
+
function selfCameraMayEmit(capName) {
|
|
21719
|
+
return !SELF_CAMERA_EXCLUDED_CAPS.has(capName);
|
|
21720
|
+
}
|
|
21721
|
+
/**
|
|
21522
21722
|
* Full event-kind taxonomy a camera CAN emit (config-derived):
|
|
21523
21723
|
* (a) motion built-in — always;
|
|
21524
21724
|
* (b) each enabled detection macro + its taxonomy subs (reverse COCO);
|
|
21525
21725
|
* (c) audio macro + its subs when the classifier is enabled;
|
|
21526
21726
|
* (d) package kinds when a package zone rule is enabled;
|
|
21527
|
-
* (e) sensor/control kinds from
|
|
21528
|
-
* per (kind, source deviceId).
|
|
21727
|
+
* (e) sensor/control kinds from the CAMERA'S OWN bindings and from its
|
|
21728
|
+
* LINKED devices (binding-driven), deduped per (kind, source deviceId).
|
|
21529
21729
|
* Every descriptor carries parentKind/level/iconId/labelKey via the single
|
|
21530
21730
|
* taxonomy dictionary — no color/icon is declared here.
|
|
21531
21731
|
*/
|
|
@@ -21560,6 +21760,7 @@ async function composeEventKinds(deps, deviceId) {
|
|
|
21560
21760
|
const audioP = attempt(deps, "audioEnabled", () => deps.audioEnabled(deviceId));
|
|
21561
21761
|
const packageP = attempt(deps, "packageZonesEnabled", async () => deps.packageZonesEnabled ? deps.packageZonesEnabled(deviceId) : false);
|
|
21562
21762
|
const linkedP = attempt(deps, "getLinkedDevices", () => deps.linkedDevices.getLinkedDevices({ deviceId }));
|
|
21763
|
+
const selfBindingsP = attempt(deps, "getBindings", () => deps.bindings.getBindings({ deviceId }));
|
|
21563
21764
|
pushKind("motion");
|
|
21564
21765
|
for (const macro of await macrosP ?? []) {
|
|
21565
21766
|
pushKind(macro);
|
|
@@ -21576,26 +21777,30 @@ async function composeEventKinds(deps, deviceId) {
|
|
|
21576
21777
|
const devices = (await linkedP)?.devices ?? [];
|
|
21577
21778
|
const bindingsPerDevice = await Promise.all(devices.map((linked) => attempt(deps, "getBindings", () => deps.bindings.getBindings({ deviceId: linked.deviceId }))));
|
|
21578
21779
|
const seen = /* @__PURE__ */ new Set();
|
|
21579
|
-
|
|
21580
|
-
|
|
21581
|
-
if (entries === void 0) continue;
|
|
21780
|
+
const pushSensorKinds = (sourceDeviceId, entries, isSelf) => {
|
|
21781
|
+
if (entries === void 0) return;
|
|
21582
21782
|
for (const { capName } of entries) {
|
|
21783
|
+
if (isSelf && !selfCameraMayEmit(capName)) continue;
|
|
21583
21784
|
const descriptor = EVENT_KIND_BY_CAP[capName];
|
|
21584
21785
|
if (descriptor === void 0) continue;
|
|
21585
|
-
const dedupeKey = `${descriptor.kind}:${
|
|
21786
|
+
const dedupeKey = `${descriptor.kind}:${sourceDeviceId}`;
|
|
21586
21787
|
if (seen.has(dedupeKey)) continue;
|
|
21587
21788
|
seen.add(dedupeKey);
|
|
21588
21789
|
pushKind(descriptor.kind, {
|
|
21589
21790
|
capName,
|
|
21590
|
-
deviceId:
|
|
21791
|
+
deviceId: sourceDeviceId
|
|
21591
21792
|
});
|
|
21592
21793
|
}
|
|
21593
|
-
}
|
|
21794
|
+
};
|
|
21795
|
+
pushSensorKinds(deviceId, (await selfBindingsP)?.entries, true);
|
|
21796
|
+
for (const [index, linked] of devices.entries()) pushSensorKinds(linked.deviceId, bindingsPerDevice[index]?.entries, false);
|
|
21594
21797
|
return out;
|
|
21595
21798
|
}
|
|
21596
21799
|
var DEFAULT_CACHE_TTL_MS = 6e4;
|
|
21597
21800
|
/**
|
|
21598
|
-
* TTL-cached reverse index: source deviceId → camera ids it is linked to
|
|
21801
|
+
* TTL-cached reverse index: source deviceId → camera ids it is linked to,
|
|
21802
|
+
* plus the camera id SET the index was built from (so the ingest can ask
|
|
21803
|
+
* "is this device itself a camera" without a second enumeration).
|
|
21599
21804
|
* Rebuilds lazily (single-flight) when stale, so the `DeviceStateChanged`
|
|
21600
21805
|
* handler pays one map lookup per event in the common case.
|
|
21601
21806
|
*/
|
|
@@ -21603,6 +21808,8 @@ var LinkedCamerasCache = class {
|
|
|
21603
21808
|
deps;
|
|
21604
21809
|
ttlMs;
|
|
21605
21810
|
index = /* @__PURE__ */ new Map();
|
|
21811
|
+
/** Every camera id seen by the last successful build. */
|
|
21812
|
+
cameras = /* @__PURE__ */ new Set();
|
|
21606
21813
|
/** Ms timestamp of the last build; null = never built / invalidated. */
|
|
21607
21814
|
builtAt = null;
|
|
21608
21815
|
building = null;
|
|
@@ -21612,23 +21819,37 @@ var LinkedCamerasCache = class {
|
|
|
21612
21819
|
}
|
|
21613
21820
|
/** Camera ids linked to `sourceDeviceId` ([] when none). */
|
|
21614
21821
|
async camerasFor(sourceDeviceId, nowMs = Date.now()) {
|
|
21615
|
-
|
|
21616
|
-
this.building ??= this.rebuild(nowMs).finally(() => {
|
|
21617
|
-
this.building = null;
|
|
21618
|
-
});
|
|
21619
|
-
await this.building;
|
|
21620
|
-
}
|
|
21822
|
+
await this.ensureFresh(nowMs);
|
|
21621
21823
|
return this.index.get(sourceDeviceId) ?? [];
|
|
21622
21824
|
}
|
|
21825
|
+
/**
|
|
21826
|
+
* True when `deviceId` is ITSELF a camera. Answered off the same build as
|
|
21827
|
+
* {@link camerasFor} — a sensor cap bound on a camera needs both facts in
|
|
21828
|
+
* the same event, and a second enumeration would double the cost of the
|
|
21829
|
+
* ingest path for nothing.
|
|
21830
|
+
*/
|
|
21831
|
+
async isCamera(deviceId, nowMs = Date.now()) {
|
|
21832
|
+
await this.ensureFresh(nowMs);
|
|
21833
|
+
return this.cameras.has(deviceId);
|
|
21834
|
+
}
|
|
21623
21835
|
/** Drop the cached index (e.g. on link-topology change events). */
|
|
21624
21836
|
invalidate() {
|
|
21625
21837
|
this.builtAt = null;
|
|
21626
21838
|
}
|
|
21627
21839
|
/** Test/maintenance hook: replace the index directly. */
|
|
21628
|
-
seed(index, builtAt) {
|
|
21840
|
+
seed(index, builtAt, cameraIds = []) {
|
|
21629
21841
|
this.index = new Map(index);
|
|
21842
|
+
this.cameras = new Set(cameraIds);
|
|
21630
21843
|
this.builtAt = builtAt;
|
|
21631
21844
|
}
|
|
21845
|
+
/** Rebuild (single-flight) when never built or past the TTL. */
|
|
21846
|
+
async ensureFresh(nowMs) {
|
|
21847
|
+
if (this.builtAt !== null && nowMs - this.builtAt < this.ttlMs) return;
|
|
21848
|
+
this.building ??= this.rebuild(nowMs).finally(() => {
|
|
21849
|
+
this.building = null;
|
|
21850
|
+
});
|
|
21851
|
+
await this.building;
|
|
21852
|
+
}
|
|
21632
21853
|
async rebuild(nowMs) {
|
|
21633
21854
|
try {
|
|
21634
21855
|
const cameraIds = await this.deps.cameras.listCameraIds();
|
|
@@ -21645,6 +21866,7 @@ var LinkedCamerasCache = class {
|
|
|
21645
21866
|
this.deps.onError?.("getLinkedDevices", err);
|
|
21646
21867
|
}
|
|
21647
21868
|
this.index = next;
|
|
21869
|
+
this.cameras = new Set(cameraIds);
|
|
21648
21870
|
this.builtAt = nowMs;
|
|
21649
21871
|
} catch (err) {
|
|
21650
21872
|
this.deps.onError?.("listCameraIds", err);
|
|
@@ -21653,16 +21875,25 @@ var LinkedCamerasCache = class {
|
|
|
21653
21875
|
}
|
|
21654
21876
|
};
|
|
21655
21877
|
/**
|
|
21656
|
-
* One `DeviceStateChanged` → N history rows
|
|
21657
|
-
*
|
|
21658
|
-
*
|
|
21878
|
+
* One `DeviceStateChanged` → N history rows, one per camera the event belongs
|
|
21879
|
+
* to: the emitting device's LINKED cameras, plus the emitting device ITSELF
|
|
21880
|
+
* when it is a camera carrying the sensor cap (a doorbell wrapper bound on
|
|
21881
|
+
* camera 615 — see `SELF_CAMERA_EXCLUDED_CAPS` for the one cap that is not
|
|
21882
|
+
* self-ingested). The EVENT_KIND_BY_CAP gate exits first so non-sensor cap
|
|
21883
|
+
* churn costs one map lookup. Returns the number of rows inserted (0 when
|
|
21884
|
+
* unmapped, or when the device belongs to no camera at all).
|
|
21659
21885
|
* Telemetry-lossy by design (D8) — inserts are best-effort.
|
|
21660
21886
|
*/
|
|
21661
21887
|
async function ingestSensorStateChange(deps, data, timestamp) {
|
|
21662
21888
|
const descriptor = EVENT_KIND_BY_CAP[data.capName];
|
|
21663
21889
|
if (descriptor === void 0) return 0;
|
|
21664
|
-
const
|
|
21665
|
-
|
|
21890
|
+
const linkedCameraIds = await deps.cache.camerasFor(data.deviceId);
|
|
21891
|
+
const isCamera = await deps.cache.isCamera(data.deviceId);
|
|
21892
|
+
const cameraIds = isCamera && selfCameraMayEmit(data.capName) && !linkedCameraIds.includes(data.deviceId) ? [data.deviceId, ...linkedCameraIds] : linkedCameraIds;
|
|
21893
|
+
if (cameraIds.length === 0) {
|
|
21894
|
+
deps.onDropped?.(isCamera ? "camera-native-cap" : "no-camera", descriptor.kind);
|
|
21895
|
+
return 0;
|
|
21896
|
+
}
|
|
21666
21897
|
const slice = data.slice;
|
|
21667
21898
|
const value = slice !== null && slice !== void 0 && typeof slice === "object" && !Array.isArray(slice) ? slice : null;
|
|
21668
21899
|
const makeId = deps.makeId ?? (() => `pa-sensor-${randomUUID()}`);
|
|
@@ -21798,6 +22029,8 @@ var SNAPSHOT_MAX_WIDTH = 960;
|
|
|
21798
22029
|
/** Bound the once-per-owner display-fallback memo so a long-lived process never
|
|
21799
22030
|
* leaks it. */
|
|
21800
22031
|
var MAX_LOGGED_DISPLAY_FALLBACKS = 5e3;
|
|
22032
|
+
/** One `pinned-frame resolve missed` line per device per minute. */
|
|
22033
|
+
var PINNED_MISS_WARN_THROTTLE_MS = 6e4;
|
|
21801
22034
|
/**
|
|
21802
22035
|
* Map a detection-frame pixel box (`fromW`×`fromH`, the ≤640 raster the tracker
|
|
21803
22036
|
* ran on) onto the native full frame (`toW`×`toH`, the 960-downscaled native
|
|
@@ -21883,6 +22116,8 @@ var EventMediaDispatcher = class {
|
|
|
21883
22116
|
/** Once-per-owner (`${eventId}:${kind}`) memo for the DISPLAY-crop fallback
|
|
21884
22117
|
* info log — see {@link logDisplayFallbackOnce}. */
|
|
21885
22118
|
loggedDisplayFallbacks = /* @__PURE__ */ new Set();
|
|
22119
|
+
/** Per-device throttle for {@link warnPinnedFrameMiss}. */
|
|
22120
|
+
lastPinnedMissWarnAt = /* @__PURE__ */ new Map();
|
|
21886
22121
|
constructor(deps) {
|
|
21887
22122
|
this.deps = deps;
|
|
21888
22123
|
this.sharedNativeFullFrame = createSharedFrameResolver((handle) => this.deps.getNativeFullFrameRgb(handle, SNAPSHOT_MAX_WIDTH));
|
|
@@ -21896,30 +22131,48 @@ var EventMediaDispatcher = class {
|
|
|
21896
22131
|
* by which time the handle's ring slot is usually recycled and the by-handle
|
|
21897
22132
|
* resolve in {@link captureForFrame} finds nothing (the 2026-07-23 boxed-tile
|
|
21898
22133
|
* regression). ~0.7MB per pinned frame, freed with the batch — bounded by the
|
|
21899
|
-
* lane. Returns `null` on a genuine live-window miss
|
|
21900
|
-
*
|
|
21901
|
-
*
|
|
22134
|
+
* lane. Returns `null` on a genuine live-window miss.
|
|
22135
|
+
*
|
|
22136
|
+
* Both miss paths are WARN, throttled to one line per device per minute. They
|
|
22137
|
+
* are the FIRST half of a pair whose second half (`capture batch lost`) has
|
|
22138
|
+
* always been a warn: a batch that lost its frame is reported with
|
|
22139
|
+
* `pinned: false`, and until 2026-08-07 the reason the pin itself failed was
|
|
22140
|
+
* either at debug (the throw) or nowhere at all (the null return). 383 batch
|
|
22141
|
+
* losses in 3 h had `pinned: false` on 100% of them and nothing said why.
|
|
21902
22142
|
*/
|
|
21903
22143
|
async resolvePinnedFrame(deviceId, frameHandle) {
|
|
21904
22144
|
try {
|
|
21905
22145
|
const decoded = await resolveFrame(frameHandle, { getRemoteFrame: this.deps.getRemoteFrame });
|
|
21906
|
-
if (!decoded)
|
|
22146
|
+
if (!decoded) {
|
|
22147
|
+
this.warnPinnedFrameMiss(deviceId, frameHandle, "recycled-before-pin");
|
|
22148
|
+
return null;
|
|
22149
|
+
}
|
|
21907
22150
|
return {
|
|
21908
22151
|
...decoded,
|
|
21909
22152
|
data: Buffer.from(decoded.data)
|
|
21910
22153
|
};
|
|
21911
22154
|
} catch (err) {
|
|
21912
|
-
this.
|
|
21913
|
-
tags: { deviceId },
|
|
21914
|
-
meta: {
|
|
21915
|
-
deviceId,
|
|
21916
|
-
shmId: frameHandle.shmId,
|
|
21917
|
-
error: String(err)
|
|
21918
|
-
}
|
|
21919
|
-
});
|
|
22155
|
+
this.warnPinnedFrameMiss(deviceId, frameHandle, "threw", String(err));
|
|
21920
22156
|
return null;
|
|
21921
22157
|
}
|
|
21922
22158
|
}
|
|
22159
|
+
/** One `pinned-frame resolve missed` line per device per minute. Throttled
|
|
22160
|
+
* because it fires per capture batch, and the batch's own loss line is not. */
|
|
22161
|
+
warnPinnedFrameMiss(deviceId, frameHandle, reason, error) {
|
|
22162
|
+
const now = Date.now();
|
|
22163
|
+
if (now - (this.lastPinnedMissWarnAt.get(deviceId) ?? 0) < PINNED_MISS_WARN_THROTTLE_MS) return;
|
|
22164
|
+
this.lastPinnedMissWarnAt.set(deviceId, now);
|
|
22165
|
+
this.deps.logger.warn("event media: pinned-frame resolve missed (live window)", {
|
|
22166
|
+
tags: { deviceId },
|
|
22167
|
+
meta: {
|
|
22168
|
+
reason,
|
|
22169
|
+
shmId: frameHandle.shmId,
|
|
22170
|
+
handle: `${frameHandle.shmId}#${frameHandle.slot}#${frameHandle.seq}`,
|
|
22171
|
+
handleNodeId: frameHandle.nodeId,
|
|
22172
|
+
...error !== void 0 ? { error } : {}
|
|
22173
|
+
}
|
|
22174
|
+
});
|
|
22175
|
+
}
|
|
21923
22176
|
/**
|
|
21924
22177
|
* WARN-log a whole-batch capture abort: every early return in
|
|
21925
22178
|
* {@link captureForFrame} loses ALL of the frame's targets at once —
|
|
@@ -22772,13 +23025,19 @@ var SyntheticTrackMaterializer = class {
|
|
|
22772
23025
|
* Fetch the linked camera's snapshot under a bounded timeout. A hanging
|
|
22773
23026
|
* snapshot cap is skipped (returns null + warns) so the track still lands;
|
|
22774
23027
|
* a null / failed fetch is likewise reported and skipped.
|
|
23028
|
+
*
|
|
23029
|
+
* Deliberately NOT `force`. `snapshot.cap.ts` defines `force` as the
|
|
23030
|
+
* operator's refresh button — *"On battery cams this WILL wake the camera —
|
|
23031
|
+
* accept the cost only when the user explicitly asks for it"* — and a
|
|
23032
|
+
* synthetic track is materialised from an EVENT, with nobody asking. Forcing
|
|
23033
|
+
* here walked straight past the snapshot addon's sleep gate and woke a
|
|
23034
|
+
* sleeping battery camera per event (roadmap 4f). Without it the cache
|
|
23035
|
+
* answers: ≤10 s old on a mains camera, and on a battery camera the
|
|
23036
|
+
* intentionally stale-but-honest frame that costs no wake.
|
|
22775
23037
|
*/
|
|
22776
23038
|
async fetchSnapshot(cameraId) {
|
|
22777
23039
|
try {
|
|
22778
|
-
const snap = await withTimeout(this.deps.snapshot.getSnapshot({
|
|
22779
|
-
deviceId: cameraId,
|
|
22780
|
-
force: true
|
|
22781
|
-
}), this.snapshotTimeoutMs);
|
|
23040
|
+
const snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), this.snapshotTimeoutMs);
|
|
22782
23041
|
if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"));
|
|
22783
23042
|
return snap;
|
|
22784
23043
|
} catch (err) {
|
|
@@ -25760,7 +26019,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
25760
26019
|
logger: logger.child("EventMediaDispatcher")
|
|
25761
26020
|
});
|
|
25762
26021
|
this.buildRecognizers(api, logger, stores, transport);
|
|
25763
|
-
this.buildDetailDispatcher(api, logger);
|
|
26022
|
+
this.buildDetailDispatcher(api, logger, transport);
|
|
25764
26023
|
const capProviders = this.buildAnalyticsProviders(api, logger);
|
|
25765
26024
|
await this.buildNotificationCenter(api, logger, stores);
|
|
25766
26025
|
await this.serveEventMediaDataPlane();
|
|
@@ -26208,7 +26467,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
26208
26467
|
* demand via `pipelineRunner.runDetailSubtree`. The dispatcher owns the
|
|
26209
26468
|
* per-device schedulers, concurrency cap, and 1 s tick; result routing +
|
|
26210
26469
|
* the pinned call + crop fallback are injected here. */
|
|
26211
|
-
buildDetailDispatcher(api, logger) {
|
|
26470
|
+
buildDetailDispatcher(api, logger, transport) {
|
|
26212
26471
|
const runnerApi = api.pipelineRunner;
|
|
26213
26472
|
this.detailDispatcher = new TrackDetailDispatcher({
|
|
26214
26473
|
logger: logger.child("DetailDispatcher"),
|
|
@@ -26218,6 +26477,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
26218
26477
|
return runnerApi.runDetailSubtree.mutate(input);
|
|
26219
26478
|
},
|
|
26220
26479
|
routeResults: (deviceId, trackId, details, frame) => this.routeDetailResults(deviceId, trackId, details, frame),
|
|
26480
|
+
captureFullFrameBase64: async (frame) => {
|
|
26481
|
+
if (frame.frameHandle === void 0) return null;
|
|
26482
|
+
const full = await transport.getFullFrameJpegBase64(frame.frameHandle, frame.frameWidth);
|
|
26483
|
+
return full === null ? null : {
|
|
26484
|
+
base64: full.base64,
|
|
26485
|
+
width: full.width,
|
|
26486
|
+
height: full.height
|
|
26487
|
+
};
|
|
26488
|
+
},
|
|
26221
26489
|
captureCropBase64: async (frame) => {
|
|
26222
26490
|
if (frame.frameHandle === void 0 || !this.captureCrop) return null;
|
|
26223
26491
|
const buf = await this.captureCrop(frame.frameHandle, { ...frame.bbox }, frame.frameWidth, frame.frameHeight, DETAIL_FALLBACK_CROP_PADDING);
|
|
@@ -26842,12 +27110,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
26842
27110
|
inPrevActive: false,
|
|
26843
27111
|
positionsCount: positionsCountById.get(id) ?? 1
|
|
26844
27112
|
}) === "resurrection") {
|
|
26845
|
-
log.info("track resumed", {
|
|
26846
|
-
|
|
26847
|
-
|
|
26848
|
-
|
|
26849
|
-
|
|
26850
|
-
|
|
27113
|
+
log.info("track resumed", {
|
|
27114
|
+
tags: { deviceId },
|
|
27115
|
+
meta: {
|
|
27116
|
+
trackId: id,
|
|
27117
|
+
className: t.className,
|
|
27118
|
+
source,
|
|
27119
|
+
resurrected: true
|
|
27120
|
+
}
|
|
27121
|
+
});
|
|
26851
27122
|
if (this.eventMediaDispatcher) {
|
|
26852
27123
|
this.residents.setLastFrameAt(deviceId, id, result.timestamp);
|
|
26853
27124
|
if (!this.residents.isFirstFrameLanded(id) && !this.residents.isFirstFramePending(id)) this.residents.markFirstFramePending(deviceId, id);
|
|
@@ -26982,10 +27253,13 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
26982
27253
|
let lostTrackCount = 0;
|
|
26983
27254
|
for (const id of prevIds) if (!currentTrackIds.has(id)) {
|
|
26984
27255
|
lostTrackCount += 1;
|
|
26985
|
-
log.info("track lost", {
|
|
26986
|
-
|
|
26987
|
-
|
|
26988
|
-
|
|
27256
|
+
log.info("track lost", {
|
|
27257
|
+
tags: { deviceId },
|
|
27258
|
+
meta: {
|
|
27259
|
+
trackId: id,
|
|
27260
|
+
source
|
|
27261
|
+
}
|
|
27262
|
+
});
|
|
26989
27263
|
}
|
|
26990
27264
|
this.suppressedBirths.retain(key, currentTrackIds);
|
|
26991
27265
|
this.deferredBirths.retain(key, currentTrackIds);
|
|
@@ -27222,6 +27496,19 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
27222
27496
|
this.residents.markFirstFrameLanded(deviceId, trackId);
|
|
27223
27497
|
}
|
|
27224
27498
|
for (const trackId of res.lastFrameTrackIds) this.residents.setLastFrameAt(deviceId, trackId, dispatchTimestamp);
|
|
27499
|
+
const landedSnapshots = new Set(res.storedSnapshots.map((s) => s.trackId));
|
|
27500
|
+
for (const target of snapshotTargets) {
|
|
27501
|
+
if (!target.appendSnapshot || landedSnapshots.has(target.trackId)) continue;
|
|
27502
|
+
const gaveBack = this.trackStore?.rollbackSnapshotPending(target.trackId, target.timestamp);
|
|
27503
|
+
log.warn("snapshot capture did not land", {
|
|
27504
|
+
tags: { deviceId },
|
|
27505
|
+
meta: {
|
|
27506
|
+
trackId: target.trackId,
|
|
27507
|
+
timestamp: target.timestamp,
|
|
27508
|
+
intervalReturned: gaveBack === true
|
|
27509
|
+
}
|
|
27510
|
+
});
|
|
27511
|
+
}
|
|
27225
27512
|
})
|
|
27226
27513
|
}).catch(() => {});
|
|
27227
27514
|
}
|
|
@@ -29020,8 +29307,16 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
29020
29307
|
}
|
|
29021
29308
|
/**
|
|
29022
29309
|
* Sensor-event ingest handler — `DeviceStateChanged` of a device exposing a
|
|
29023
|
-
* mapped sensor cap. Resolves the
|
|
29024
|
-
*
|
|
29310
|
+
* mapped sensor cap. Resolves the camera set through the TTL cache (the
|
|
29311
|
+
* device's LINKED cameras, plus the device itself when it is a camera
|
|
29312
|
+
* carrying the cap) and inserts ONE row per camera. Best-effort
|
|
29313
|
+
* (telemetry-lossy).
|
|
29314
|
+
*
|
|
29315
|
+
* D8/D11 boundary, stated plainly: the ride here is the LOSSY
|
|
29316
|
+
* `DeviceStateChanged` bus, and there is no reconcile that replays a missed
|
|
29317
|
+
* press from the durable `doorbell.getStatus.lastPressedAt`. A dropped bus
|
|
29318
|
+
* event is a missing history row and a missing notification — never wrong
|
|
29319
|
+
* durable state.
|
|
29025
29320
|
*/
|
|
29026
29321
|
async handleSensorStateChanged(data, timestamp) {
|
|
29027
29322
|
const store = this.sensorEventStore;
|
|
@@ -29031,7 +29326,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
29031
29326
|
await ingestSensorStateChange({
|
|
29032
29327
|
sink: store,
|
|
29033
29328
|
cache,
|
|
29034
|
-
onPersisted: (ev) => this.notificationCenter?.onSensorEventPersisted(ev)
|
|
29329
|
+
onPersisted: (ev) => this.notificationCenter?.onSensorEventPersisted(ev),
|
|
29330
|
+
onDropped: (reason, kind) => this.logSensorIngestDrop(data, reason, kind)
|
|
29035
29331
|
}, data, timestamp);
|
|
29036
29332
|
} catch (err) {
|
|
29037
29333
|
this.ctx.logger.warn("sensor-event ingest failed", {
|
|
@@ -29047,7 +29343,16 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
29047
29343
|
if (materializer === null || descriptor === void 0) return;
|
|
29048
29344
|
try {
|
|
29049
29345
|
const cameraIds = await cache.camerasFor(data.deviceId);
|
|
29050
|
-
if (cameraIds.length === 0)
|
|
29346
|
+
if (cameraIds.length === 0) {
|
|
29347
|
+
this.ctx.logger.debug("sensor synthetic-track projection skipped — no linked camera", {
|
|
29348
|
+
tags: { deviceId: data.deviceId },
|
|
29349
|
+
meta: {
|
|
29350
|
+
capName: data.capName,
|
|
29351
|
+
kind: descriptor.kind
|
|
29352
|
+
}
|
|
29353
|
+
});
|
|
29354
|
+
return;
|
|
29355
|
+
}
|
|
29051
29356
|
const producingDeviceName = await this.resolveProducingDeviceName(data.deviceId);
|
|
29052
29357
|
for (const cameraId of cameraIds) await materializer.materialize({
|
|
29053
29358
|
cameraId,
|
|
@@ -29068,6 +29373,35 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
29068
29373
|
}
|
|
29069
29374
|
}
|
|
29070
29375
|
/**
|
|
29376
|
+
* Say out loud that a MAPPED sensor cap ingested nothing, and why.
|
|
29377
|
+
*
|
|
29378
|
+
* BOTH branches are `debug`, deliberately. After the self-camera fix neither
|
|
29379
|
+
* is a surprise any more — they are the two designed steady states, and both
|
|
29380
|
+
* ride at bus rate:
|
|
29381
|
+
*
|
|
29382
|
+
* - `camera-native-cap` is EVERY camera's `motion` slice. Every provider
|
|
29383
|
+
* binds a native `motion` cap on the camera device and motion churns
|
|
29384
|
+
* continuously (camera 590 flaps at ~2 Hz), so an `info` here would be a
|
|
29385
|
+
* log flood measured in millions of lines a day — its own outage.
|
|
29386
|
+
* - `no-camera` is every sensor in the fleet that no camera links.
|
|
29387
|
+
*
|
|
29388
|
+
* What must never happen again is the branch being INVISIBLE: a doorbell
|
|
29389
|
+
* press that ingested nothing produced not one line for months. A debug line
|
|
29390
|
+
* carrying `deviceId`, `capName`, `kind` and `reason` answers "why is 615
|
|
29391
|
+
* dark" the moment anyone asks it.
|
|
29392
|
+
*/
|
|
29393
|
+
logSensorIngestDrop(data, reason, kind) {
|
|
29394
|
+
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";
|
|
29395
|
+
this.ctx.logger.debug(message, {
|
|
29396
|
+
tags: { deviceId: data.deviceId },
|
|
29397
|
+
meta: {
|
|
29398
|
+
capName: data.capName,
|
|
29399
|
+
kind,
|
|
29400
|
+
reason
|
|
29401
|
+
}
|
|
29402
|
+
});
|
|
29403
|
+
}
|
|
29404
|
+
/**
|
|
29071
29405
|
* Resolve the NAME of a device (the linked sensor/control that produced a
|
|
29072
29406
|
* synthetic event) via the device-manager cap. Best-effort — returns
|
|
29073
29407
|
* undefined on any lookup failure or an unknown device so the synthetic
|