@mega-yfue/eufy-sdk 0.0.4 → 0.1.0-beta.0
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/README.md +31 -12
- package/dist/client/device-registry.d.ts +343 -0
- package/dist/client/eufy-mega.d.ts +872 -0
- package/dist/client/index.d.ts +1 -6
- package/dist/client/map-channels.d.ts +22 -0
- package/dist/client/types.d.ts +383 -0
- package/dist/core/contracts.d.ts +898 -0
- package/dist/core/crypto.d.ts +98 -0
- package/dist/core/index.d.ts +9 -7
- package/dist/core/logger.d.ts +53 -0
- package/dist/core/lz4-block.d.ts +35 -0
- package/dist/core/raw-dp-hex.d.ts +32 -0
- package/dist/core/raw-dp-writer.d.ts +83 -0
- package/dist/core/store.d.ts +43 -0
- package/dist/core/types.d.ts +169 -0
- package/dist/core/util.d.ts +78 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +25526 -14
- package/dist/index.js.map +7 -1
- package/dist/model/capabilities/access.d.ts +127 -0
- package/dist/model/capabilities/arming.d.ts +201 -0
- package/dist/model/capabilities/audio.d.ts +154 -0
- package/dist/model/capabilities/battery.d.ts +363 -0
- package/dist/model/capabilities/camera.d.ts +564 -0
- package/dist/model/capabilities/co.d.ts +40 -0
- package/dist/model/capabilities/contact.d.ts +133 -0
- package/dist/model/capabilities/doorbell.d.ts +345 -0
- package/dist/model/capabilities/dp-catalog.d.ts +38 -0
- package/dist/model/capabilities/index.d.ts +561 -0
- package/dist/model/capabilities/info.d.ts +28 -0
- package/dist/model/capabilities/keypad.d.ts +61 -0
- package/dist/model/capabilities/leak.d.ts +43 -0
- package/dist/model/capabilities/light.d.ts +174 -0
- package/dist/model/capabilities/locate.d.ts +63 -0
- package/dist/model/capabilities/lock.d.ts +242 -0
- package/dist/model/capabilities/manifest.d.ts +107 -0
- package/dist/model/capabilities/members.d.ts +647 -0
- package/dist/model/capabilities/motion.d.ts +377 -0
- package/dist/model/capabilities/person-detection.d.ts +8 -0
- package/dist/model/capabilities/ptz.d.ts +289 -0
- package/dist/model/capabilities/rtsp.d.ts +221 -0
- package/dist/model/capabilities/siren.d.ts +218 -0
- package/dist/model/capabilities/smart-light.d.ts +172 -0
- package/dist/model/capabilities/smoke.d.ts +40 -0
- package/dist/model/capabilities/snapshot.d.ts +6 -0
- package/dist/model/capabilities/storage.d.ts +11 -0
- package/dist/model/capabilities/suction.d.ts +104 -0
- package/dist/model/capabilities/types.d.ts +484 -0
- package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
- package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
- package/dist/model/capabilities/video.d.ts +6 -0
- package/dist/model/classify.d.ts +78 -0
- package/dist/model/clean-record-detail.d.ts +65 -0
- package/dist/model/clean-records.d.ts +69 -0
- package/dist/model/device-family.d.ts +73 -0
- package/dist/model/device-types.d.ts +123 -0
- package/dist/model/device.d.ts +265 -0
- package/dist/model/index.d.ts +29 -4
- package/dist/model/infer.d.ts +23 -0
- package/dist/model/inspect.d.ts +61 -0
- package/dist/model/life-params.d.ts +21 -0
- package/dist/model/map-pixels.d.ts +70 -0
- package/dist/model/param-dictionary.d.ts +28 -0
- package/dist/model/param-namespace.d.ts +21 -0
- package/dist/model/proto-read.d.ts +53 -0
- package/dist/model/push-events.d.ts +147 -0
- package/dist/model/registry.d.ts +54 -0
- package/dist/model/types.d.ts +301 -0
- package/dist/model/vacuum-map-store.d.ts +92 -0
- package/dist/model/vacuum-map.d.ts +286 -0
- package/dist/model/vacuum-scenes.d.ts +76 -0
- package/dist/model/vacuum-schedules.d.ts +85 -0
- package/dist/transport/dp-preset.d.ts +102 -0
- package/dist/transport/ff09.d.ts +444 -0
- package/dist/transport/ffmpeg.d.ts +86 -0
- package/dist/transport/http/decodeImageV1.d.ts +20 -0
- package/dist/transport/http/decodeImageV2.d.ts +19 -0
- package/dist/transport/http/index.d.ts +5 -0
- package/dist/transport/http/light-catalog.d.ts +62 -0
- package/dist/transport/http/media-download.d.ts +14 -0
- package/dist/transport/http/mega-client.d.ts +514 -0
- package/dist/transport/http/phone-model.d.ts +21 -0
- package/dist/transport/index.d.ts +10 -7
- package/dist/transport/mqtt/app-client-id.d.ts +16 -0
- package/dist/transport/mqtt/availability.d.ts +14 -0
- package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
- package/dist/transport/mqtt/biz-stream.d.ts +98 -0
- package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
- package/dist/transport/mqtt/clean-codec.d.ts +14 -0
- package/dist/transport/mqtt/command-router.d.ts +285 -0
- package/dist/transport/mqtt/dp-codec.d.ts +58 -0
- package/dist/transport/mqtt/dp-color.d.ts +14 -0
- package/dist/transport/mqtt/engine.d.ts +16 -0
- package/dist/transport/mqtt/index.d.ts +5 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
- package/dist/transport/mqtt/topics.d.ts +80 -0
- package/dist/transport/p2p/adts.d.ts +91 -0
- package/dist/transport/p2p/annexb.d.ts +124 -0
- package/dist/transport/p2p/codec.d.ts +166 -0
- package/dist/transport/p2p/command-router.d.ts +655 -0
- package/dist/transport/p2p/commands.d.ts +550 -0
- package/dist/transport/p2p/envelope.d.ts +46 -0
- package/dist/transport/p2p/fmp4.d.ts +89 -0
- package/dist/transport/p2p/fragment-recording.d.ts +33 -0
- package/dist/transport/p2p/index.d.ts +13 -0
- package/dist/transport/p2p/lan-ip.d.ts +25 -0
- package/dist/transport/p2p/live-stream.d.ts +223 -0
- package/dist/transport/p2p/live-trace.d.ts +123 -0
- package/dist/transport/p2p/media.d.ts +105 -0
- package/dist/transport/p2p/p2p-session.d.ts +620 -0
- package/dist/transport/p2p/readable-egress.d.ts +27 -0
- package/dist/transport/p2p/session-manager.d.ts +154 -0
- package/dist/transport/p2p/shared-live-source.d.ts +431 -0
- package/dist/transport/p2p/talkback.d.ts +187 -0
- package/dist/transport/p2p/video.d.ts +150 -0
- package/dist/transport/p2p/write-commands.d.ts +21 -0
- package/dist/transport/protobuf.d.ts +5 -0
- package/dist/transport/push/fcm.d.ts +23 -0
- package/dist/transport/push/index.d.ts +6 -0
- package/dist/transport/push/message-tags.d.ts +26 -0
- package/dist/transport/push/parser.d.ts +27 -0
- package/dist/transport/push/proto.d.ts +11 -0
- package/dist/transport/push/push-client.d.ts +64 -0
- package/dist/transport/push/store.d.ts +23 -0
- package/dist/transport/push/types.d.ts +180 -0
- package/dist/transport/raw-dp.d.ts +6 -0
- package/dist/transport/stored-image-cache.d.ts +23 -0
- package/dist/transport/tuya/account.d.ts +44 -0
- package/dist/transport/tuya/client.d.ts +89 -0
- package/dist/transport/tuya/command-router.d.ts +79 -0
- package/dist/transport/tuya/dp-codec.d.ts +56 -0
- package/dist/transport/tuya/index.d.ts +28 -0
- package/dist/transport/tuya/request.d.ts +157 -0
- package/dist/transport/tuya/sign.d.ts +64 -0
- package/package.json +12 -13
- package/dist/client/index.js +0 -2
- package/dist/client/index.js.map +0 -1
- package/dist/core/index.js +0 -2
- package/dist/core/index.js.map +0 -1
- package/dist/model/index.js +0 -2
- package/dist/model/index.js.map +0 -1
- package/dist/transport/index.js +0 -2
- package/dist/transport/index.js.map +0 -1
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal eufy P2P (ThroughTek PPCS) UDP session for ONE station.
|
|
3
|
+
*
|
|
4
|
+
* Scope: realtime *device state / sensor events* from a HomeBase — not video.
|
|
5
|
+
* The connect handshake (local + cloud lookup → CHECK_CAM hole-punch → CAM_ID)
|
|
6
|
+
* and PING/PONG heartbeat are full, but command *sending* and video/audio
|
|
7
|
+
* streaming are intentionally omitted. Crucially, HomeBase control-channel
|
|
8
|
+
* notifications (sensor open/close, alarm mode, …) are AES-128-ECB encrypted with
|
|
9
|
+
* the *Level-1* key derived from sn + p2p_did alone — so we decrypt them without
|
|
10
|
+
* the gateway cipher negotiation.
|
|
11
|
+
*
|
|
12
|
+
* Reassembly note: control notifications are small and arrive in a single UDP
|
|
13
|
+
* datagram, so this parses one-frame-per-packet (with multiple frames per packet)
|
|
14
|
+
* and does not reassemble frames that span datagrams.
|
|
15
|
+
*/
|
|
16
|
+
import { EventEmitter } from "node:events";
|
|
17
|
+
import { type Address, type P2PDataFrameHeader } from "./codec.js";
|
|
18
|
+
import { type Logger } from "../../core/logger.js";
|
|
19
|
+
/**
|
|
20
|
+
* The channel a command addresses the station itself on, rather than one of its cameras, and the value a
|
|
21
|
+
* session's channel-taking methods resolve an omitted channel to.
|
|
22
|
+
*/
|
|
23
|
+
export declare const STATION_CHANNEL = 255;
|
|
24
|
+
/**
|
|
25
|
+
* Transport wiring for a PPCS session — internal to the SDK; a host reaches sessions through the facade.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export interface P2PSessionConfig {
|
|
29
|
+
stationSn: string;
|
|
30
|
+
p2pDid: string;
|
|
31
|
+
/** Cloud lookup servers (decodeP2PCloudIPs of the station's p2p_conn/app_conn). */
|
|
32
|
+
cloudAddresses?: Address[];
|
|
33
|
+
/** DSK key for the cloud-lookup payload. Optional — local lookup needs none. */
|
|
34
|
+
dskKey?: string;
|
|
35
|
+
/** Known LAN address of the station, for a direct (non-broadcast) local lookup. */
|
|
36
|
+
localAddress?: string;
|
|
37
|
+
/** Disable UDP broadcast local lookup (e.g. cloud-only). Default false. */
|
|
38
|
+
noBroadcast?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a station `cipher_id` → its ECC private key hex (from cloud `get_ciphers`). When
|
|
41
|
+
* provided, the session auto-negotiates the **level-2** session key on connect: it reads the
|
|
42
|
+
* `cipher_id` + ECIES envelope from the `CMD_GATEWAYINFO` reply, derives the key, and calls
|
|
43
|
+
* `setLevel2Key()` — so signCode 2/8 frames (camera info, event DB, event images) decrypt live.
|
|
44
|
+
*/
|
|
45
|
+
resolveCipherKey?: (cipherId: number) => Promise<string | undefined>;
|
|
46
|
+
/** Diagnostics sink. Omit for silence. */
|
|
47
|
+
logger?: Logger;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A decoded P2P data/notification frame.
|
|
51
|
+
*
|
|
52
|
+
* A decoded PPCS frame. Internal wire shape — surfaced on the facade's diagnostic event only.
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
export interface P2PFrame extends P2PDataFrameHeader {
|
|
56
|
+
stationSn: string;
|
|
57
|
+
/** Resolved command name (e.g. "CMD_NOTIFY_PAYLOAD"), or "CMD_<id>" if unknown. */
|
|
58
|
+
commandName: string;
|
|
59
|
+
dataType: number;
|
|
60
|
+
/** Payload after (attempted) decryption. */
|
|
61
|
+
data: Buffer;
|
|
62
|
+
/** Payload exactly as received, before any decryption (for diagnostics). */
|
|
63
|
+
raw: Buffer;
|
|
64
|
+
/** Parsed JSON when the payload is a NUL-terminated JSON document. */
|
|
65
|
+
json?: {
|
|
66
|
+
cmd?: number;
|
|
67
|
+
payload?: unknown;
|
|
68
|
+
} & Record<string, unknown>;
|
|
69
|
+
/**
|
|
70
|
+
* Device params the frame reported, when its JSON carries a `params` array of
|
|
71
|
+
* `{param_type, param_value}` — the shape a station uses to volunteer an attached device's state.
|
|
72
|
+
*
|
|
73
|
+
* Unwrapped here because it is pure framing: the entries are the SAME `param_type` ids the cloud
|
|
74
|
+
* record uses, so nothing in this payload needs a capability to interpret it, and a caller can land
|
|
75
|
+
* the values as device state without any per-capability decoder.
|
|
76
|
+
*/
|
|
77
|
+
params?: Record<number, string>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A live PPCS session. Internal transport; a host drives cameras through the capability surface.
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
export declare class P2PSession extends EventEmitter {
|
|
84
|
+
private readonly cfg;
|
|
85
|
+
private socket?;
|
|
86
|
+
private connected;
|
|
87
|
+
private connecting;
|
|
88
|
+
private closed;
|
|
89
|
+
private connectAddress?;
|
|
90
|
+
private seqNumber;
|
|
91
|
+
/**
|
|
92
|
+
* Sequence counter for frames sent on the **video** data-type channel, which the device tracks
|
|
93
|
+
* separately from the control channel's — today only {@link sendAudioFrame} rides it.
|
|
94
|
+
*/
|
|
95
|
+
private videoSeqNumber;
|
|
96
|
+
/**
|
|
97
|
+
* Audio frames sent but not yet acknowledged, keyed by their video-channel sequence number. Holding
|
|
98
|
+
* the datagram (not just the payload) means a retransmit is byte-identical, which is what the
|
|
99
|
+
* device's ordered channel expects.
|
|
100
|
+
*/
|
|
101
|
+
private unackedAudio;
|
|
102
|
+
/**
|
|
103
|
+
* Whether the audio channel is currently in a stall — set when a frame is abandoned, cleared by the
|
|
104
|
+
* next acknowledgement of any frame.
|
|
105
|
+
*
|
|
106
|
+
* A stall is an episode, not a per-frame event: once the device stops acknowledging, EVERY later
|
|
107
|
+
* frame is abandoned in turn, so reporting each one turns a single condition into hundreds of
|
|
108
|
+
* identical events. Measured live on a battery camera whose media session was stopped mid-clip by its
|
|
109
|
+
* power budget: 74 consecutive frames, one event each, all saying the same thing. The condition is
|
|
110
|
+
* reported once and again only if the channel recovers and stalls afresh.
|
|
111
|
+
*/
|
|
112
|
+
private audioStalled;
|
|
113
|
+
private audioRetransmitTimer?;
|
|
114
|
+
private lastPongData?;
|
|
115
|
+
/** When this connection last received a PONG — `undefined` until the first, see {@link pathSilentMs}. */
|
|
116
|
+
private lastPongAt?;
|
|
117
|
+
/** Whether the silence has already been stated, so it is traced once per connection rather than per read. */
|
|
118
|
+
private pathStaleTraced;
|
|
119
|
+
private lookupTimer?;
|
|
120
|
+
private heartbeatTimer?;
|
|
121
|
+
private connectTimer?;
|
|
122
|
+
/** Our own bound host:port, self-reported inside LOOKUP_WITH_KEY requests (see sendLookups). */
|
|
123
|
+
private selfAddress?;
|
|
124
|
+
/** In-flight multi-datagram frame per data channel (see onData). */
|
|
125
|
+
private readonly pendingByDataType;
|
|
126
|
+
/** Last datagram sequence number seen per dataType — used to detect a lost/reordered datagram
|
|
127
|
+
* mid-frame and drop the (now unrecoverable) partial frame instead of splicing wrong bytes. */
|
|
128
|
+
private readonly lastSeqByType;
|
|
129
|
+
private tracedDatagramGaps;
|
|
130
|
+
private readonly level1Key;
|
|
131
|
+
/** Negotiated 32-byte level-2/gateway key (AES-256-GCM). Set via setLevel2Key once known. */
|
|
132
|
+
private level2Key?;
|
|
133
|
+
private level2Seq;
|
|
134
|
+
private rsaPrivateKey?;
|
|
135
|
+
private rsaModulusHex?;
|
|
136
|
+
/** Guards the one-shot level-2 key negotiation kicked off by the GATEWAYINFO reply. */
|
|
137
|
+
private level2Negotiating;
|
|
138
|
+
/**
|
|
139
|
+
* Whether waiting for a level-2 key can still change the answer: `false` once one has been negotiated,
|
|
140
|
+
* once the one-shot negotiation concluded without one, and from the start when nothing can negotiate one.
|
|
141
|
+
*/
|
|
142
|
+
private level2Pending;
|
|
143
|
+
/** Waiters parked in {@link awaitLevel2Key}, woken the moment the negotiation settles either way. */
|
|
144
|
+
private readonly level2Waiters;
|
|
145
|
+
/** Whether this connection has already been asked a second time for its gateway info — see {@link repromptLevel2Key}. */
|
|
146
|
+
private level2Reprompted;
|
|
147
|
+
/** When this session connected — the instant the level-2 negotiation had its chance to start. */
|
|
148
|
+
private connectedAtMs?;
|
|
149
|
+
/** Connection generation that owns every asynchronous result derived from its gateway envelope. */
|
|
150
|
+
private connectionGeneration;
|
|
151
|
+
/**
|
|
152
|
+
* Own-session channels with an active live start → the encryption variant of the start frame we
|
|
153
|
+
* last sent (`"l2"` GCM / `"l1"` ECB). A keepalive tick re-issues the start if the variant should
|
|
154
|
+
* change (the level-2 key arrived after an initial level-1 start), otherwise sends the 1139 nudge.
|
|
155
|
+
*/
|
|
156
|
+
private readonly liveStartedChannels;
|
|
157
|
+
private readonly unackedLiveStarts;
|
|
158
|
+
private liveStartRetransmitTimer?;
|
|
159
|
+
private readonly logger;
|
|
160
|
+
constructor(cfg: P2PSessionConfig);
|
|
161
|
+
/**
|
|
162
|
+
* This session's opaque handle for tracing — `station-N` by order of construction in this process.
|
|
163
|
+
*
|
|
164
|
+
* Not the serial: a trace carrying one could not be retained by a host, which is the whole point of the
|
|
165
|
+
* phase vocabulary. It groups one station's records within a run and resolves to nothing outside it.
|
|
166
|
+
*/
|
|
167
|
+
readonly traceId: string;
|
|
168
|
+
/**
|
|
169
|
+
* How long this connection's path has been silent, or nothing where it has never answered.
|
|
170
|
+
*
|
|
171
|
+
* A PONG is the station stating that the path is alive. `undefined` is neither alive nor dead: it is a station
|
|
172
|
+
* that has said nothing either way.
|
|
173
|
+
*/
|
|
174
|
+
get pathSilentMs(): number | undefined;
|
|
175
|
+
/**
|
|
176
|
+
* Whether this path can still be committed to, on the evidence the heartbeat gives.
|
|
177
|
+
*
|
|
178
|
+
* False where a pong arrived and then stopped for {@link PATH_SILENCE_MS}. A station that has never ponged is
|
|
179
|
+
* not known to be dead, so it answers true.
|
|
180
|
+
*
|
|
181
|
+
* Traces the silence once per connection, on the read that first observes it.
|
|
182
|
+
*/
|
|
183
|
+
get pathAnswering(): boolean;
|
|
184
|
+
/** Emit a live trace under this session's handle. */
|
|
185
|
+
private trace;
|
|
186
|
+
/** Provide the negotiated 32-byte session key so level-2 (signCode 2/8) frames can be decrypted. */
|
|
187
|
+
setLevel2Key(key: Buffer): void;
|
|
188
|
+
/** Whether the level-2 session key has been negotiated/set. */
|
|
189
|
+
get hasLevel2Key(): boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Resolve with whether a level-2 key is available, waiting only while waiting can still change that.
|
|
192
|
+
*
|
|
193
|
+
* A `"session"` grace is measured once, not restarted per call. Best-effort media uses it because every
|
|
194
|
+
* egress asks this same session and can proceed without the key on an own-session camera; a per-call
|
|
195
|
+
* budget there makes a station that offers no key charge its full budget on every later stream.
|
|
196
|
+
*
|
|
197
|
+
* A `"call"` grace gives the full wait to a command that cannot be framed without the key. Such a
|
|
198
|
+
* command may arrive on an old session before a delayed `CMD_GATEWAYINFO`, so session age says nothing
|
|
199
|
+
* about whether the key can still arrive during this command.
|
|
200
|
+
*
|
|
201
|
+
* The session grace runs from connect, when the station is prompted for `CMD_GATEWAYINFO`. A negotiation
|
|
202
|
+
* beginning later does not restart it: best-effort media can proceed without the key, and restarting
|
|
203
|
+
* would charge another grace to a source that may already be streaming. A session whose negotiation has
|
|
204
|
+
* settled answers without waiting at all, since being one-shot is what makes that answer final.
|
|
205
|
+
*
|
|
206
|
+
* An own-session camera whose grace expires here is NOT thereby a camera that will fail to stream. Measured
|
|
207
|
+
* on one account: own-session cameras of two device types negotiated a key, three others never did, and
|
|
208
|
+
* cameras from that second group streamed normally at level-1 — including one of the same firmware as an
|
|
209
|
+
* own-session camera that delivered no video at all for a reason of its own. An expired grace therefore
|
|
210
|
+
* separates nothing on this path, and a start failure on such a session is not evidence about it.
|
|
211
|
+
*/
|
|
212
|
+
awaitLevel2Key(graceMs: number, graceFrom?: "call" | "session"): Promise<boolean>;
|
|
213
|
+
/**
|
|
214
|
+
* Ask the station for its gateway info a second time, re-opening a negotiation that concluded without a key.
|
|
215
|
+
*
|
|
216
|
+
* The negotiation is one-shot per connection: the station is prompted once on connect, and a reply that
|
|
217
|
+
* never lands settles the wait so {@link awaitLevel2Key} answers `false` at once forever after. That is the
|
|
218
|
+
* right answer for best-effort media, which proceeds at level-1 — but an operation whose ONLY wire is
|
|
219
|
+
* level-2 is then refused for the whole life of that connection, while a fresh session over the same
|
|
220
|
+
* device negotiates a key normally. Measured: a session that had settled refused every such operation
|
|
221
|
+
* until it was rebuilt, at which point the station answered with a cipher id straight away.
|
|
222
|
+
*
|
|
223
|
+
* Bounded to one extra ask per connection, so a burst of such operations cannot turn a silent station into a
|
|
224
|
+
* flood, and answers whether it asked — `false` when a key is already held, when nothing can negotiate one,
|
|
225
|
+
* when the ask was already spent, or when there is nowhere to send it. Callers with a level-1 path must not
|
|
226
|
+
* use this: re-prompting on their behalf would be noise for an answer they do not need.
|
|
227
|
+
*/
|
|
228
|
+
repromptLevel2Key(): boolean;
|
|
229
|
+
/** Record that the level-2 negotiation has finished, with or without a key, and wake every waiter. */
|
|
230
|
+
private settleLevel2;
|
|
231
|
+
/**
|
|
232
|
+
* Negotiate the level-2 session key from the decrypted CMD_GATEWAYINFO payload: read its
|
|
233
|
+
* `cipher_id`, resolve that cipher's ECC private key (cloud `get_ciphers`, via the configured
|
|
234
|
+
* `resolveCipherKey`), run the ECIES unwrap, and `setLevel2Key()`. One-shot; emits `level2Ready`
|
|
235
|
+
* on success and `error` on failure (non-fatal — level-1 traffic keeps working regardless).
|
|
236
|
+
*
|
|
237
|
+
* Every outcome settles the wait in {@link awaitLevel2Key}, because being one-shot is what makes a
|
|
238
|
+
* failure final for this connection generation: nothing will retry it there, so a later command must be
|
|
239
|
+
* told at once rather than left to time out against a key that is not coming.
|
|
240
|
+
*/
|
|
241
|
+
private negotiateLevel2Key;
|
|
242
|
+
/**
|
|
243
|
+
* Decrypt a level-2 P2P frame payload (AES-256-GCM, key = negotiated session key, AAD
|
|
244
|
+
* "eufy security"). Both signCodes use the SAME key + layout, differing only by a 4-byte
|
|
245
|
+
* cleartext sub-header — reversed from `libmega_media_sdk.so` + verified (GCM tag authenticates):
|
|
246
|
+
*
|
|
247
|
+
* signCode 8 (app→station commands): tag(16) ‖ nonce(12) ‖ `[seq,03,02,01]`(4) ‖ ciphertext → ct@32
|
|
248
|
+
* signCode 2 (station media/db/notify): tag(16) ‖ nonce(12) ‖ ciphertext → ct@28
|
|
249
|
+
*
|
|
250
|
+
* (signCode 2 carries the station's content — `CMD_CAMERA_INFO`, `CMD_DATABASE` responses,
|
|
251
|
+
* `CMD_DATABASE_IMAGE` JPEGs, notify results. signCode 2 and 8 share the one AES-256-GCM session
|
|
252
|
+
* key; only the ciphertext offset differs, by the 4 sub-header bytes present on signCode 8.)
|
|
253
|
+
*/
|
|
254
|
+
private decryptLevel2;
|
|
255
|
+
get isConnected(): boolean;
|
|
256
|
+
/** Open the socket and start the lookup → hole-punch handshake. */
|
|
257
|
+
connect(): Promise<void>;
|
|
258
|
+
/** Cached across every `P2PSession` in this process — the local outbound IPv4 doesn't vary by
|
|
259
|
+
* station, so there's no reason to re-probe it per session (e.g. once per station in a fleet). Only
|
|
260
|
+
* a SUCCESSFUL probe is cached (see {@link detectLocalIp}) — a transient failure must not be memoized
|
|
261
|
+
* forever. */
|
|
262
|
+
private static localIpPromise?;
|
|
263
|
+
/** Best-effort local outbound IPv4 (the address the OS would route through to reach the internet) —
|
|
264
|
+
* connects a throwaway UDP socket (no packets sent, just kernel routing) and reads its bound
|
|
265
|
+
* address. Needed to self-report our own host:port inside a LOOKUP_WITH_KEY request. Bounded by its
|
|
266
|
+
* own timeout so a hung probe (sandboxed/offline network) can never block anything indefinitely;
|
|
267
|
+
* resolves to `undefined` on any failure so callers skip the self-report rather than send a bogus
|
|
268
|
+
* `0.0.0.0` wildcard address (unconfirmed whether the cloud lookup server treats that specially).
|
|
269
|
+
*
|
|
270
|
+
* Two failure modes guarded here:
|
|
271
|
+
* - **Permanent poisoning**: the underlying probe promise always RESOLVES (never rejects), even on
|
|
272
|
+
* failure — so a naive `if (!cached) probe()` memoizes `undefined` exactly like a real address,
|
|
273
|
+
* forever, the first time this races (e.g. a transient blip on the very first `connect()` in the
|
|
274
|
+
* process). Every later session would then be silently stuck on the KEY2 (relay-only) lookup
|
|
275
|
+
* variant for the process's whole lifetime. Fixed by only caching a successful (non-undefined)
|
|
276
|
+
* result — a failure clears `localIpPromise` so the NEXT call re-probes.
|
|
277
|
+
* - **Teardown races**: the timeout, the `connect` callback, and the `error` handler all race to
|
|
278
|
+
* finish the same probe. Without a `settled` guard, a timeout that fires first (closes + resolves)
|
|
279
|
+
* can be followed by the `connect` callback still firing on the now-closed socket — `.address()` or
|
|
280
|
+
* a second `.close()` on an already-closed dgram socket both throw `ERR_SOCKET_DGRAM_NOT_RUNNING`
|
|
281
|
+
* SYNCHRONOUSLY, off any promise chain, surfacing as an uncaught exception. `settled` + try/catch
|
|
282
|
+
* around the teardown close that window.
|
|
283
|
+
*/
|
|
284
|
+
private static detectLocalIp;
|
|
285
|
+
private sendLookups;
|
|
286
|
+
/**
|
|
287
|
+
* Route one inbound UDP datagram by its message type, tracing every non-DATA one and any type this session
|
|
288
|
+
* does not model.
|
|
289
|
+
*
|
|
290
|
+
* An unmodelled type is not by itself evidence about a session that is failing. `0xf169` — a relay-pool
|
|
291
|
+
* listing answering a cloud lookup, which a connected session has no use for — reaches the UNHANDLED branch
|
|
292
|
+
* on the sessions of cameras that stream and cameras that do not alike. `0xf121` has been observed
|
|
293
|
+
* straddling a level-2 wait on a camera whose failure to deliver video had a separate cause, and did not
|
|
294
|
+
* recur across later probes of it. Correlate an unmodelled type against a WORKING session before reading it
|
|
295
|
+
* as a cause.
|
|
296
|
+
*/
|
|
297
|
+
private onMessage;
|
|
298
|
+
private beginCheckCam;
|
|
299
|
+
private onConnected;
|
|
300
|
+
/**
|
|
301
|
+
* Start the realtime media stream for a camera `channel` (the device's `device_channel`; defaults to
|
|
302
|
+
* the station channel). The camera then streams `CMD_VIDEO_FRAME` (1300) + `CMD_AUDIO_FRAME` (1301),
|
|
303
|
+
* surfaced via the `data` event. Two start protocols, selected by runtime topology (never by device
|
|
304
|
+
* family):
|
|
305
|
+
* - `homeBaseAttached` (the camera rides a HomeBase's session): `CMD_SET_PAYLOAD` (1350) wrapping
|
|
306
|
+
* `{cmd:1003, mChannel:channel}` at level-2, where `mChannel` picks the camera on the base.
|
|
307
|
+
* - own-session (the session is the camera itself): `CMD_CONTROL_PAYLOAD` (1700) / inner cmd 1000
|
|
308
|
+
* START_LIVE, then the 1139 keepalive to hold the stream. A bare 1003 is read as a status query
|
|
309
|
+
* there, not a stream.
|
|
310
|
+
*
|
|
311
|
+
* An own-session start's encryption follows the session key, not the device family: level-2 GCM once a
|
|
312
|
+
* key is negotiated, level-1 ECB before that. A key that arrives after a level-1 start makes the next
|
|
313
|
+
* keepalive tick re-issue the start at level-2, so a camera that only accepts level-2 recovers from a
|
|
314
|
+
* `live()` that raced ahead of key negotiation.
|
|
315
|
+
*
|
|
316
|
+
* Each start is retained until its DATA acknowledgement and repeated every `LIVE_START_RETRANSMIT_MS` until
|
|
317
|
+
* the device acknowledges one, bounded by `LIVE_START_ACK_DEADLINE_MS` rather than by a send count. Both are
|
|
318
|
+
* internal to this module, so they are named as code: a public comment cannot link to what the reference does
|
|
319
|
+
* not carry. Use the `LiveStream` helper for a managed feed with keepalive.
|
|
320
|
+
*
|
|
321
|
+
* `opts.force` sends a real start on a channel this session already counts as started, and yields to a
|
|
322
|
+
* start still awaiting acknowledgement — that one is already being repeated byte-identically and is
|
|
323
|
+
* abandoned at its own deadline.
|
|
324
|
+
*/
|
|
325
|
+
startLiveMedia(channel?: number, accountId?: string, homeBaseAttached?: boolean, opts?: {
|
|
326
|
+
force?: boolean;
|
|
327
|
+
}): void;
|
|
328
|
+
/**
|
|
329
|
+
* The own-session START_LIVE wrapper JSON (`{commandType: 1000, data: {…}}`).
|
|
330
|
+
*
|
|
331
|
+
* Every field is byte-verified against the current app's own start for an own-session camera, decrypted
|
|
332
|
+
* from a level-2 capture: `msg_id` is 1, `extValue` repeats the inner command id 1000, `streamtype` is 2,
|
|
333
|
+
* `video_type` is 12, and `transaction` is the millisecond timestamp as a string. The device answers a
|
|
334
|
+
* start carrying these values with a keyframe; `encryptkey` is the modulus it RSA-wraps each keyframe's
|
|
335
|
+
* AES media key with (unwrapped by {@link decodeVideoFrame}).
|
|
336
|
+
*/
|
|
337
|
+
private startLiveJson;
|
|
338
|
+
/**
|
|
339
|
+
* Send the own-session START_LIVE frame (outer `CMD_CONTROL_PAYLOAD` 1700, inner cmd 1000). Level-2
|
|
340
|
+
* (GCM, signCode 8, frame type 10) when the session has a negotiated key, else level-1 (AES-128-ECB,
|
|
341
|
+
* signCode 1, frame type 11) — chosen by the session key, not the device family. The camera RSA-wraps
|
|
342
|
+
* each keyframe's AES media key with the `encryptkey` modulus (unwrapped by {@link decodeVideoFrame}).
|
|
343
|
+
*/
|
|
344
|
+
private sendStartLiveOwnSession;
|
|
345
|
+
/**
|
|
346
|
+
* Repeat unacknowledged own-session live starts until the device takes one. Runs only while a start is
|
|
347
|
+
* outstanding.
|
|
348
|
+
*
|
|
349
|
+
* A start abandoned at {@link LIVE_START_ACK_DEADLINE_MS} is traced as `media-command-unacknowledged` and
|
|
350
|
+
* the channel's started state is forgotten. Both halves matter: the camera was never told to stream, so
|
|
351
|
+
* the warm-up that follows can only ever time out, and the trace is what separates that from a camera that
|
|
352
|
+
* got the start and stayed silent. Forgetting the state is what lets the next keepalive tick issue a real
|
|
353
|
+
* start under a fresh sequence — while the channel still counts as started, every tick sends only the 1139
|
|
354
|
+
* nudge, which holds a stream that was never started and cannot begin one.
|
|
355
|
+
*
|
|
356
|
+
* The abandonment emits `liveStartUnacknowledged` carrying the RESOLVED channel, so a listener matches it
|
|
357
|
+
* against {@link STATION_CHANNEL} where it started one without naming a channel.
|
|
358
|
+
*/
|
|
359
|
+
private armLiveStartRetransmit;
|
|
360
|
+
private clearLiveStartRetransmit;
|
|
361
|
+
/**
|
|
362
|
+
* Send a **string-payload control command** (the JSON control wrapper `1700` / media `1350`)
|
|
363
|
+
* over the **level-1** (AES-128-ECB) control channel — the WRITE
|
|
364
|
+
* primitive behind `setProperty`. `value` is the JSON the app wraps (`{commandType, data}`); the
|
|
365
|
+
* device unwraps and applies it. Fire-and-forget today (the device echoes the new state back as a
|
|
366
|
+
* param update); request/response correlation is a later refinement.
|
|
367
|
+
*/
|
|
368
|
+
sendStringPayloadCommand(commandType: number, value: string, channel?: number): void;
|
|
369
|
+
/**
|
|
370
|
+
* Send an **int+string control command** over the level-1 (AES-128-ECB) channel — the wire shape
|
|
371
|
+
* the app uses for the floodlight/spotlight switch (`CMD_SET_FLOODLIGHT_MANUAL_SWITCH` 1400) on
|
|
372
|
+
* IndoorOutdoor / SoloCam-spotlight / Cam2C-3 families: `value` (0/1), `valueSub` (channel), and
|
|
373
|
+
* `strValue` (admin `account_id`). See {@link buildIntStringCommandPayload}. Fire-and-forget.
|
|
374
|
+
*/
|
|
375
|
+
sendIntStringCommand(commandType: number, value: number, valueSub: number, strValue: string, channel?: number): void;
|
|
376
|
+
/**
|
|
377
|
+
* Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
|
|
378
|
+
* target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
|
|
379
|
+
* mechanism proven for camera selection in media start. Use for control commands routed through a
|
|
380
|
+
* HomeBase (where level-1 ECB is rejected). Returns `false` if the level-2 key isn't negotiated
|
|
381
|
+
* yet (caller can fall back to {@link sendStringPayloadCommand}).
|
|
382
|
+
*/
|
|
383
|
+
sendControlLevel2(cmd: number, channel: number, accountId: string, payload: Record<string, unknown>, mValue3?: number): boolean;
|
|
384
|
+
/**
|
|
385
|
+
* Send a **level-2 (GCM, signCode 8) frame whose plaintext is exactly `json`** — no
|
|
386
|
+
* `{account_id,cmd,mChannel,…}` envelope. The target device is selected by the frame-header
|
|
387
|
+
* `channel`. This is the form the eufy app uses for control commands (confirmed by live capture:
|
|
388
|
+
* floodlight = `{"commandType":1400,"data":{...}}`). Returns `false` if no level-2 key.
|
|
389
|
+
*/
|
|
390
|
+
sendRawLevel2(json: string, channel: number, outerCmd?: number): boolean;
|
|
391
|
+
/**
|
|
392
|
+
* Like {@link sendRawLevel2} but the plaintext is a raw byte buffer, not a UTF-8 string. Some
|
|
393
|
+
* "direct" commands (e.g. CAMERA_SWITCH 1035) carry a binary struct, not JSON.
|
|
394
|
+
*/
|
|
395
|
+
sendRawLevel2Bytes(payload: Buffer, channel: number, outerCmd?: number, signCode?: number): boolean;
|
|
396
|
+
/** Stop the realtime media stream (`CMD_STOP_REALTIME_MEDIA`, 1004) on a camera `channel`. */
|
|
397
|
+
stopLiveMedia(channel?: number, accountId?: string): void;
|
|
398
|
+
/**
|
|
399
|
+
* Open the device's talkback (host→device audio) path on a camera `channel`, after which
|
|
400
|
+
* {@link sendAudioFrame} is accepted until {@link stopTalkback}. Two protocols, selected by runtime
|
|
401
|
+
* topology exactly as {@link startLiveMedia} selects its own:
|
|
402
|
+
* - `homeBaseAttached`: the direct `CMD_START_TALKBACK` (1005) frame at level-2, whose entire
|
|
403
|
+
* plaintext is the camera channel as a `uint32` — a 4-byte body, no envelope and no account id.
|
|
404
|
+
* - own-session: `CMD_CONTROL_PAYLOAD` (1700) wrapping `{commandType:1001, data:{transaction}}`,
|
|
405
|
+
* where `transaction` is a millisecond clock the device only echoes.
|
|
406
|
+
*
|
|
407
|
+
* Returns `false` when the HomeBase path is asked for without a negotiated level-2 key. The device
|
|
408
|
+
* replies with the generic 132-byte acknowledgement rather than a talkback-specific result, and the
|
|
409
|
+
* app does not gate its audio on it, so this is fire-and-forget like the other control sends.
|
|
410
|
+
*/
|
|
411
|
+
startTalkback(channel?: number, homeBaseAttached?: boolean): boolean;
|
|
412
|
+
/** Close the talkback path opened by {@link startTalkback} — the `1006` / inner-`1002` counterpart. */
|
|
413
|
+
stopTalkback(channel?: number, homeBaseAttached?: boolean): boolean;
|
|
414
|
+
/**
|
|
415
|
+
* The one place the talkback start/stop frame is built, so the topology branch is stated once.
|
|
416
|
+
* The own-session path picks its encryption from the session key rather than from topology — a
|
|
417
|
+
* standalone camera that negotiated a level-2 key sends GCM, one that never did sends ECB — which
|
|
418
|
+
* is the same rule `sendStartLiveOwnSession` follows.
|
|
419
|
+
*/
|
|
420
|
+
private sendTalkbackControl;
|
|
421
|
+
/**
|
|
422
|
+
* Push one **whole ADTS AAC frame** toward the device as `CMD_AUDIO_FRAME` (1301) on the video
|
|
423
|
+
* data-type channel, plaintext (signCode 0) in both topologies — the audio itself is never
|
|
424
|
+
* encrypted, only the start/stop control frames are. The 16-byte header the device expects ahead of
|
|
425
|
+
* the payload is `[uint32 frameLength][uint32 channel][8 zero bytes]`, where `frameLength` repeats
|
|
426
|
+
* the ADTS header's own length field. The trailing 8 bytes were zero across all 418 frames of the
|
|
427
|
+
* 2026-07-31 capture, on three cameras spanning both topologies — the app never populates them.
|
|
428
|
+
*
|
|
429
|
+
* The frames ride their own sequence counter, independent of the control channel's, starting at 0
|
|
430
|
+
* for the session. Fire-and-forget: the device acknowledges the datagram, not the audio.
|
|
431
|
+
*/
|
|
432
|
+
sendAudioFrame(channel: number, frame: Buffer): void;
|
|
433
|
+
/**
|
|
434
|
+
* How many audio frames are awaiting acknowledgement — a health signal, NOT a gate. Pacing must
|
|
435
|
+
* stay at the frame rate no matter how many are outstanding: the device plays a continuous stream,
|
|
436
|
+
* so slowing the feed to wait for acknowledgements starves it faster than any loss does.
|
|
437
|
+
*/
|
|
438
|
+
get audioInFlight(): number;
|
|
439
|
+
/**
|
|
440
|
+
* Resend audio frames the device has not acknowledged in time, and abandon the ones it never will.
|
|
441
|
+
* Runs only while frames are outstanding.
|
|
442
|
+
*
|
|
443
|
+
* A frame that exhausts {@link AUDIO_MAX_SENDS} emits `audioGap` with its sequence number as it is
|
|
444
|
+
* dropped. The audio channel is ordered, so that hole can stall everything queued behind it; without
|
|
445
|
+
* the event nothing observes it — the map stays bounded because the entry is evicted, so the
|
|
446
|
+
* in-flight count reads healthy while the speaker has gone quiet.
|
|
447
|
+
*/
|
|
448
|
+
private armAudioRetransmit;
|
|
449
|
+
/**
|
|
450
|
+
* One retransmission sweep over an acknowledged channel's retained datagrams: resend those past
|
|
451
|
+
* `retransmitMs` byte-identically, abandon those the caller reports spent, and answer whether any datagram
|
|
452
|
+
* is still outstanding — a caller stops its ticker once nothing is.
|
|
453
|
+
*
|
|
454
|
+
* Both acknowledged directions — outbound audio and the own-session live start — repeat on these terms, so
|
|
455
|
+
* the sweep has one implementation. What "spent" means is the caller's, because the two are bounded by
|
|
456
|
+
* different things: audio by a send count, since repeating it more amplifies the loss it is repairing; a
|
|
457
|
+
* live start by elapsed time, since the sends it needs are however many its acknowledgement latency
|
|
458
|
+
* demands. Datagrams are only ever retained by a send, and a send needs a connect address, so the
|
|
459
|
+
* no-address case has nothing retained to sweep and simply reports idle.
|
|
460
|
+
*/
|
|
461
|
+
private retransmitUnacked;
|
|
462
|
+
/**
|
|
463
|
+
* Clear acknowledged live starts and audio frames. The device's acknowledgement lists the sequence numbers it has
|
|
464
|
+
* taken on a given data-type channel: `[dataTypeHeader:2][count:2 BE][seq:2 BE]×count`.
|
|
465
|
+
*
|
|
466
|
+
* DATA acknowledgements release retained own-session starts. The video data-type acknowledgements release
|
|
467
|
+
* outbound talkback frames, whose ordered channel stalls on a gap.
|
|
468
|
+
*/
|
|
469
|
+
private onAck;
|
|
470
|
+
/**
|
|
471
|
+
* Send a CMD_SET_PAYLOAD(1350) wrapping `{account_id, cmd:<subCmd>, mChannel, mValue3:<subCmd>,
|
|
472
|
+
* payload}`, encrypted level-2 (AES-256-GCM, signCode 8) — the app's media-control path. The
|
|
473
|
+
* camera is selected by `mChannel` (= the device's `device_channel`) AND the frame-header channel.
|
|
474
|
+
*/
|
|
475
|
+
private sendMediaPayloadLevel2;
|
|
476
|
+
/**
|
|
477
|
+
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ [seq,03,02,01](4) ‖
|
|
478
|
+
* ciphertext`, AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of
|
|
479
|
+
* `decryptLevel2`. The 4-byte sub-header is cleartext (skipped on decrypt); `seq` is a counter.
|
|
480
|
+
*/
|
|
481
|
+
/**
|
|
482
|
+
* Decode a `CMD_VIDEO_FRAME` (1300) payload into clean Annex-B H.264 (the 22-byte frame header
|
|
483
|
+
* stripped). Reversed from the V6 app + live H.264 captures: the 22-byte header is
|
|
484
|
+
* `[0:4]len [4]keyframe [5]streamType [6:8]seq [8:10]fps [10:12]W [12:14]H [14:20]ts`. When the
|
|
485
|
+
* frame is encrypted (`signCode > 0` and len ≥ 128) the bytes `[22:150]` are the RSA-wrapped AES
|
|
486
|
+
* media key (decrypt with our private key, PKCS#1 v1.5 → AES key) and the video starts at offset
|
|
487
|
+
* 151 with its **first 128 bytes AES-ECB(NoPadding)-encrypted**; the rest is cleartext. Plaintext
|
|
488
|
+
* frames are just `[22 : 22+len]`. Returns undefined if the RSA key is missing/undecryptable.
|
|
489
|
+
*/
|
|
490
|
+
decodeVideoFrame(data: Buffer, signCode: number): Buffer | undefined;
|
|
491
|
+
/**
|
|
492
|
+
* Unwrap the RSA-wrapped AES media key from a keyframe. Standard PKCS#1 v1.5 — decrypt with
|
|
493
|
+
* `RSA_PKCS1_PADDING`. This needs Node ≥24.5 (OpenSSL 3.5.1), which re-enabled PKCS1 `privateDecrypt`
|
|
494
|
+
* after the intermediate OpenSSL (3.2–3.4) disabled it as a Marvin/CVE-2023-46809 mitigation — hence
|
|
495
|
+
* the pinned engine. Verified live: the strict path unwraps the 16-byte key on real E2E cameras
|
|
496
|
+
* (T8171 2560×1440, T8210 640×480). Returns `undefined` on failure (frame is skipped).
|
|
497
|
+
*/
|
|
498
|
+
private rsaUnwrapKey;
|
|
499
|
+
/**
|
|
500
|
+
* The RSA-1024 public-key modulus (128-byte hex) the station uses to wrap the per-stream media
|
|
501
|
+
* key. Lazily generates a keypair; `rsaPrivateKey` decrypts the media key the station returns.
|
|
502
|
+
*/
|
|
503
|
+
private rsaModulus;
|
|
504
|
+
private encryptLevel2;
|
|
505
|
+
/** Send a no-arg command frame (e.g. CMD_GATEWAYINFO) on a channel (default: the station channel). */
|
|
506
|
+
private sendCommand;
|
|
507
|
+
/**
|
|
508
|
+
* Request a stored image (event thumbnail / cover) over P2P. Sends a
|
|
509
|
+
* `CMD_SET_PAYLOAD` wrapping `{cmd: CMD_DATABASE_IMAGE, payload:[{file}]}` — the
|
|
510
|
+
* station replies with a `CMD_DATABASE_IMAGE` frame that this session decodes and
|
|
511
|
+
* emits as an `image` event `{ file, data }` (P2P images come back as plain JPEG,
|
|
512
|
+
* no v1/v2 obfuscation). `filePath` is the on-station path from a push payload
|
|
513
|
+
* (`pic_filepath`/`file_path`/`cover_path`). `accountId` is the admin user id when
|
|
514
|
+
* known (some firmware ignores it). NOTE: needs live-device validation — the
|
|
515
|
+
* control-command send path is exercised here for the first time.
|
|
516
|
+
*/
|
|
517
|
+
requestImage(filePath: string, opts?: {
|
|
518
|
+
accountId?: string;
|
|
519
|
+
channel?: number;
|
|
520
|
+
}): void;
|
|
521
|
+
/**
|
|
522
|
+
* Query an on-station database table over P2P (edge-AI face DB, event records, …).
|
|
523
|
+
* Sends `CMD_SET_PAYLOAD{cmd:CMD_DATABASE, payload:{cmd:DB_QUERY.FULL_TABLE, table}}`.
|
|
524
|
+
* The HomeBase streams back `CMD_DATABASE` (1306) frames `{cmd:10000,count,data:[…]}`,
|
|
525
|
+
* level-1-encrypted — decoded and emitted as `dbChunk` (decrypted text) per frame.
|
|
526
|
+
* Tables: `familiar_faces`, `person_basic_info`, `event_person_list`, `history_record_info`.
|
|
527
|
+
*/
|
|
528
|
+
queryDatabase(table: string, opts?: {
|
|
529
|
+
accountId?: string;
|
|
530
|
+
channel?: number;
|
|
531
|
+
query?: Record<string, unknown>;
|
|
532
|
+
innerCmd?: number;
|
|
533
|
+
}): void;
|
|
534
|
+
/**
|
|
535
|
+
* Standard "give me the whole table" query params the eufy app uses for a direct
|
|
536
|
+
* `CMD_DATABASE` read (inner `cmd 10000`). `count` bounds the row count.
|
|
537
|
+
*/
|
|
538
|
+
private fullTableQuery;
|
|
539
|
+
/**
|
|
540
|
+
* Request the on-device edge-AI **face roster** over P2P — the phone-free path.
|
|
541
|
+
*
|
|
542
|
+
* NOT a dedicated face command (the `1194`/`1195` path never responds on a HomeBase). The
|
|
543
|
+
* app reads the DB directly: a `CMD_DATABASE` (1306) query of `person_basic_info` with inner
|
|
544
|
+
* `cmd 10000` on `mChannel 255`. The HomeBase replies with the level-1 `CMD_DATABASE` response
|
|
545
|
+
* (reassembled by `onData`, surfaced via `dbChunk`) listing every person: `{person_id, name,
|
|
546
|
+
* relation, group_id, …}` — `stranger\d+` names are auto-assigned (unfamiliar). ~10 KB.
|
|
547
|
+
* Verified live against the app's own decrypted request. `account_id` must be the station
|
|
548
|
+
* `admin_user_id` (a wrong/absent id or a camera session answers result `-104`).
|
|
549
|
+
*/
|
|
550
|
+
requestFaces(opts?: {
|
|
551
|
+
accountId?: string;
|
|
552
|
+
channel?: number;
|
|
553
|
+
}): void;
|
|
554
|
+
/**
|
|
555
|
+
* Request the **face feature rows** over P2P (`face_feature_info`, inner `cmd 10000`). Each row
|
|
556
|
+
* carries `{person_id, face_name, face_id, face_picture_content, face_feature_file_path}` — where
|
|
557
|
+
* `face_picture_content` is the on-station **path** to that person's enrolled face JPEG (fetch it
|
|
558
|
+
* with `requestImage()` → plain JPEG via the `image` event). Surfaced via `dbChunk`.
|
|
559
|
+
*/
|
|
560
|
+
requestFaceFeatures(opts?: {
|
|
561
|
+
accountId?: string;
|
|
562
|
+
channel?: number;
|
|
563
|
+
}): void;
|
|
564
|
+
/**
|
|
565
|
+
* Low-level: send a `CMD_SET_PAYLOAD` (1350) wrapping `{account_id, cmd:<subCmd>, mChannel,
|
|
566
|
+
* payload, transaction}` over the level-1 channel. The reply arrives as a `NOTIFY_PAYLOAD`
|
|
567
|
+
* (1351) frame (level-1 decrypted, surfaced via the `data` event / `frame.json`).
|
|
568
|
+
*/
|
|
569
|
+
sendSetPayload(subCmd: number, payload?: unknown, opts?: {
|
|
570
|
+
accountId?: string;
|
|
571
|
+
channel?: number;
|
|
572
|
+
omitPayload?: boolean;
|
|
573
|
+
wrapCmd?: number;
|
|
574
|
+
rawValue?: Record<string, unknown>;
|
|
575
|
+
}): void;
|
|
576
|
+
/**
|
|
577
|
+
* Request the on-device edge-AI face roster over P2P. `COMMAND_GET_LOCAL_FACES` (1194) =
|
|
578
|
+
* familiar/enrolled people; `COMMAND_GET_LOCAL_CANDIDATE_FACES` (1195) = strangers. The
|
|
579
|
+
* HomeBase replies with a `NOTIFY_PAYLOAD` (1351) frame carrying the JSON roster (reassembled
|
|
580
|
+
* + level-1 decrypted; listen on the `data` event for commandId 1351).
|
|
581
|
+
*/
|
|
582
|
+
requestLocalFaces(opts?: {
|
|
583
|
+
candidate?: boolean;
|
|
584
|
+
accountId?: string;
|
|
585
|
+
channel?: number;
|
|
586
|
+
}): void;
|
|
587
|
+
/**
|
|
588
|
+
* Acknowledge and reassemble one DATA datagram, sequenced independently per data type.
|
|
589
|
+
*
|
|
590
|
+
* The device numbers each data type's datagrams in its own 16-bit space and repeats what it thinks was
|
|
591
|
+
* lost, so a datagram that does not advance the sequence — a duplicate, or one already superseded — is a
|
|
592
|
+
* retransmission of something already reassembled: it is acknowledged, then ignored. Distance is measured
|
|
593
|
+
* modulo the sequence space and read as backwards beyond {@link SEQUENCE_LOOKBACK}, which is what lets the
|
|
594
|
+
* numbering wrap without the next datagram looking like a jump of nearly a full space.
|
|
595
|
+
*
|
|
596
|
+
* A datagram numbered further back than {@link STALE_RETRANSMIT_DEPTH} is not a repeat the device could
|
|
597
|
+
* still be making: the numbering itself has restarted, which a device does when it begins a fresh stream
|
|
598
|
+
* on a connection that is already up. That resynchronizes — the high-water mark moves to the restarted
|
|
599
|
+
* numbering and the half-assembled frame goes — because ignoring it would freeze the mark, and every
|
|
600
|
+
* datagram of the new numbering would then be read as behind it too, for as long as it took to climb back.
|
|
601
|
+
*
|
|
602
|
+
* Only a forward gap means a datagram is genuinely missing. A logical frame's payload spans datagrams that
|
|
603
|
+
* carry no header of their own, so the bytes cannot be reassembled around the hole: whatever was pending
|
|
604
|
+
* for that data type is discarded, and the frame is rebuilt from the next header.
|
|
605
|
+
*/
|
|
606
|
+
private onData;
|
|
607
|
+
/**
|
|
608
|
+
* Forget where each data type's sequence numbering had reached, and drop any half-reassembled frame.
|
|
609
|
+
*
|
|
610
|
+
* A device numbers datagrams per connection and starts over on the next one, so carrying the previous
|
|
611
|
+
* connection's high-water mark across would make the new connection's first datagrams look like
|
|
612
|
+
* retransmissions from behind and drop them all. Half a frame from a connection that is gone can never be
|
|
613
|
+
* completed either.
|
|
614
|
+
*/
|
|
615
|
+
private resetInboundSequencing;
|
|
616
|
+
private ackTypeHeader;
|
|
617
|
+
private handleFrame;
|
|
618
|
+
private send;
|
|
619
|
+
close(): Promise<void>;
|
|
620
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readable egress — mint a `node:stream` Readable over a {@link Consumer} of a {@link SharedLiveSource}.
|
|
3
|
+
*
|
|
4
|
+
* This is the in-process pull surface: the Readable can be piped anywhere a `node:stream` goes, without
|
|
5
|
+
* touching the P2P internals. Backpressure is honored — when the Readable's internal buffer fills, the
|
|
6
|
+
* consumer is paused; the consumer's own bounded queue then applies the V2 drop-to-keyframe policy if
|
|
7
|
+
* the sink stays slow, so one stuck reader never stalls the shared upstream or its peers. Destroying the
|
|
8
|
+
* Readable detaches the consumer (refcount--).
|
|
9
|
+
*
|
|
10
|
+
* Two modes: raw Annex-B **bytes** (default) or **objectMode** {@link LiveVideoFrame}s, which carry the
|
|
11
|
+
* codec/keyframe metadata per access unit.
|
|
12
|
+
*
|
|
13
|
+
* @module p2p/readable-egress
|
|
14
|
+
*/
|
|
15
|
+
import { Readable } from "node:stream";
|
|
16
|
+
import type { Consumer } from "./shared-live-source.js";
|
|
17
|
+
export interface ReadableEgressOptions {
|
|
18
|
+
/** Emit {@link LiveVideoFrame} objects instead of raw Annex-B bytes (default false = bytes). */
|
|
19
|
+
objectMode?: boolean;
|
|
20
|
+
/** Readable highWaterMark (bytes, or object count in objectMode). */
|
|
21
|
+
highWaterMark?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Wrap a shared-source {@link Consumer} in a fresh Readable. The consumer is detached when the
|
|
25
|
+
* Readable is destroyed/ended, so callers own the lifetime by owning the stream.
|
|
26
|
+
*/
|
|
27
|
+
export declare function openReadableFromConsumer(consumer: Consumer, opts?: ReadableEgressOptions): Readable;
|