@apocaliss92/nodedreame 1.9.0 → 1.10.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/dist/index.cjs +233 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +233 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1591,6 +1591,22 @@ declare class VacuumDevice extends BaseDevice<VacuumDeviceEvents> {
|
|
|
1591
1591
|
* `applyVacuumPFrame` merge primitive ships separately for that work.
|
|
1592
1592
|
*/
|
|
1593
1593
|
getMap(input: VacuumGetMapInput): Promise<VacuumMap>;
|
|
1594
|
+
/** Drop the P-frame merge base so the next I-frame re-seeds the stream. */
|
|
1595
|
+
resetMapStream(): void;
|
|
1596
|
+
/**
|
|
1597
|
+
* Fetch the latest advertised map frame and fold it into a continuously
|
|
1598
|
+
* updating map. An I-frame (re)seeds the merge base and renders standalone; a
|
|
1599
|
+
* P-frame is merged onto the base via {@link applyVacuumPFrame} so the live
|
|
1600
|
+
* grid stays COMPLETE (a P-frame decoded standalone is only byte-deltas). On an
|
|
1601
|
+
* out-of-order P-frame or a map-id change the base is dropped and `null`
|
|
1602
|
+
* returned — the next I-frame re-seeds. Returns `null` when no frame is
|
|
1603
|
+
* advertised yet, or a P-frame arrives before any I-frame.
|
|
1604
|
+
*
|
|
1605
|
+
* Caches {@link lastMap} and emits `'map'` exactly like {@link getMap}, so a
|
|
1606
|
+
* map-watching consumer (e.g. the camstack map child) gets a fresh complete
|
|
1607
|
+
* frame on every push during a live clean.
|
|
1608
|
+
*/
|
|
1609
|
+
fetchLatestMapStreaming(opts?: Omit<VacuumGetMapInput, 'filename'>): Promise<VacuumMap | null>;
|
|
1594
1610
|
/** Props worth seeding on start() / polling — exported for the facade. */
|
|
1595
1611
|
static readonly DEFAULT_PROPS: readonly [{
|
|
1596
1612
|
readonly siid: 2;
|
package/dist/index.d.ts
CHANGED
|
@@ -1591,6 +1591,22 @@ declare class VacuumDevice extends BaseDevice<VacuumDeviceEvents> {
|
|
|
1591
1591
|
* `applyVacuumPFrame` merge primitive ships separately for that work.
|
|
1592
1592
|
*/
|
|
1593
1593
|
getMap(input: VacuumGetMapInput): Promise<VacuumMap>;
|
|
1594
|
+
/** Drop the P-frame merge base so the next I-frame re-seeds the stream. */
|
|
1595
|
+
resetMapStream(): void;
|
|
1596
|
+
/**
|
|
1597
|
+
* Fetch the latest advertised map frame and fold it into a continuously
|
|
1598
|
+
* updating map. An I-frame (re)seeds the merge base and renders standalone; a
|
|
1599
|
+
* P-frame is merged onto the base via {@link applyVacuumPFrame} so the live
|
|
1600
|
+
* grid stays COMPLETE (a P-frame decoded standalone is only byte-deltas). On an
|
|
1601
|
+
* out-of-order P-frame or a map-id change the base is dropped and `null`
|
|
1602
|
+
* returned — the next I-frame re-seeds. Returns `null` when no frame is
|
|
1603
|
+
* advertised yet, or a P-frame arrives before any I-frame.
|
|
1604
|
+
*
|
|
1605
|
+
* Caches {@link lastMap} and emits `'map'` exactly like {@link getMap}, so a
|
|
1606
|
+
* map-watching consumer (e.g. the camstack map child) gets a fresh complete
|
|
1607
|
+
* frame on every push during a live clean.
|
|
1608
|
+
*/
|
|
1609
|
+
fetchLatestMapStreaming(opts?: Omit<VacuumGetMapInput, 'filename'>): Promise<VacuumMap | null>;
|
|
1594
1610
|
/** Props worth seeding on start() / polling — exported for the facade. */
|
|
1595
1611
|
static readonly DEFAULT_PROPS: readonly [{
|
|
1596
1612
|
readonly siid: 2;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/support/version.ts
|
|
2
2
|
var LIBRARY_NAME = "nodedreame";
|
|
3
|
-
var LIBRARY_VERSION = "1.
|
|
3
|
+
var LIBRARY_VERSION = "1.10.0";
|
|
4
4
|
|
|
5
5
|
// src/transport/errors.ts
|
|
6
6
|
var DreameError = class extends Error {
|
|
@@ -2552,6 +2552,157 @@ function decodeCleanedAreaPixels(pixels, width, height) {
|
|
|
2552
2552
|
return { cleaned, dirty };
|
|
2553
2553
|
}
|
|
2554
2554
|
|
|
2555
|
+
// src/models/vacuum/map/merge.ts
|
|
2556
|
+
var OutOfOrderFrameError = class extends Error {
|
|
2557
|
+
expectedFrameId;
|
|
2558
|
+
actualFrameId;
|
|
2559
|
+
constructor(expectedFrameId, actualFrameId) {
|
|
2560
|
+
super(`map: out-of-order P-frame (expected frame_id=${expectedFrameId}, got ${actualFrameId})`);
|
|
2561
|
+
this.name = "OutOfOrderFrameError";
|
|
2562
|
+
this.expectedFrameId = expectedFrameId;
|
|
2563
|
+
this.actualFrameId = actualFrameId;
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
function mergePFrame(prevInflated, pFrameInflated) {
|
|
2567
|
+
const prevHeader = parseMapHeader(prevInflated);
|
|
2568
|
+
const pHeader = parseMapHeader(pFrameInflated);
|
|
2569
|
+
if (pHeader.frameType !== "P") {
|
|
2570
|
+
throw new MapDecodeError(`mergePFrame: expected P-frame, got frame_type=${pHeader.frameType}`);
|
|
2571
|
+
}
|
|
2572
|
+
if (pHeader.mapId !== prevHeader.mapId) {
|
|
2573
|
+
throw new MapDecodeError(
|
|
2574
|
+
`mergePFrame: map_id mismatch (prev=${prevHeader.mapId}, P=${pHeader.mapId}) \u2014 request a fresh I-frame`
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
if (pHeader.frameId !== prevHeader.frameId + 1) {
|
|
2578
|
+
throw new OutOfOrderFrameError(prevHeader.frameId + 1, pHeader.frameId);
|
|
2579
|
+
}
|
|
2580
|
+
if (prevHeader.gridSize !== pHeader.gridSize && pHeader.width > 0 && pHeader.height > 0) {
|
|
2581
|
+
throw new MapDecodeError(
|
|
2582
|
+
`mergePFrame: grid_size changed mid-stream (${prevHeader.gridSize} \u2192 ${pHeader.gridSize})`
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
const prevTail = parseMapJsonTail(sliceTailText(prevInflated, prevHeader));
|
|
2586
|
+
const pTail = parseMapJsonTail(sliceTailText(pFrameInflated, pHeader));
|
|
2587
|
+
const prevLeft = prevTail.origin?.[0] ?? prevHeader.left;
|
|
2588
|
+
const prevTop = prevTail.origin?.[1] ?? prevHeader.top;
|
|
2589
|
+
const grid = prevHeader.gridSize;
|
|
2590
|
+
const prevRight = prevLeft + prevHeader.width * grid;
|
|
2591
|
+
const prevBottom = prevTop + prevHeader.height * grid;
|
|
2592
|
+
const hasPixelDelta = pHeader.width > 0 && pHeader.height > 0;
|
|
2593
|
+
let unionLeft = prevLeft;
|
|
2594
|
+
let unionTop = prevTop;
|
|
2595
|
+
let unionWidth = prevHeader.width;
|
|
2596
|
+
let unionHeight = prevHeader.height;
|
|
2597
|
+
let pLeft = 0;
|
|
2598
|
+
let pTop = 0;
|
|
2599
|
+
if (hasPixelDelta) {
|
|
2600
|
+
pLeft = pTail.origin?.[0] ?? pHeader.left;
|
|
2601
|
+
pTop = pTail.origin?.[1] ?? pHeader.top;
|
|
2602
|
+
const pRight = pLeft + pHeader.width * grid;
|
|
2603
|
+
const pBottom = pTop + pHeader.height * grid;
|
|
2604
|
+
if ((pLeft - prevLeft) % grid !== 0 || (pTop - prevTop) % grid !== 0) {
|
|
2605
|
+
throw new MapDecodeError(
|
|
2606
|
+
`mergePFrame: P-frame origin not aligned to prev grid (offset=${pLeft - prevLeft},${pTop - prevTop} vs grid=${grid})`
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
unionLeft = Math.min(prevLeft, pLeft);
|
|
2610
|
+
unionTop = Math.min(prevTop, pTop);
|
|
2611
|
+
const unionRight = Math.max(prevRight, pRight);
|
|
2612
|
+
const unionBottom = Math.max(prevBottom, pBottom);
|
|
2613
|
+
unionWidth = (unionRight - unionLeft) / grid;
|
|
2614
|
+
unionHeight = (unionBottom - unionTop) / grid;
|
|
2615
|
+
}
|
|
2616
|
+
const newPixels = Buffer.alloc(unionWidth * unionHeight);
|
|
2617
|
+
const prevPixelEnd = HEADER_SIZE + prevHeader.width * prevHeader.height;
|
|
2618
|
+
if (prevInflated.length < prevPixelEnd) {
|
|
2619
|
+
throw new MapDecodeError(
|
|
2620
|
+
`mergePFrame: prev buffer truncated (need ${prevPixelEnd} bytes for header+pixels, got ${prevInflated.length})`
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
const prevPixels = prevInflated.subarray(HEADER_SIZE, prevPixelEnd);
|
|
2624
|
+
const prevDxPx = (prevLeft - unionLeft) / grid;
|
|
2625
|
+
const prevDyPx = (prevTop - unionTop) / grid;
|
|
2626
|
+
for (let y = 0; y < prevHeader.height; y++) {
|
|
2627
|
+
const srcOff = y * prevHeader.width;
|
|
2628
|
+
const dstOff = (prevDyPx + y) * unionWidth + prevDxPx;
|
|
2629
|
+
prevPixels.copy(newPixels, dstOff, srcOff, srcOff + prevHeader.width);
|
|
2630
|
+
}
|
|
2631
|
+
if (hasPixelDelta) {
|
|
2632
|
+
const pPixelEnd = HEADER_SIZE + pHeader.width * pHeader.height;
|
|
2633
|
+
if (pFrameInflated.length < pPixelEnd) {
|
|
2634
|
+
throw new MapDecodeError(
|
|
2635
|
+
`mergePFrame: P buffer truncated (need ${pPixelEnd} bytes for header+pixels, got ${pFrameInflated.length})`
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
const pPixels = pFrameInflated.subarray(HEADER_SIZE, pPixelEnd);
|
|
2639
|
+
const pDxPx = (pLeft - unionLeft) / grid;
|
|
2640
|
+
const pDyPx = (pTop - unionTop) / grid;
|
|
2641
|
+
for (let y = 0; y < pHeader.height; y++) {
|
|
2642
|
+
const dstRow = (pDyPx + y) * unionWidth + pDxPx;
|
|
2643
|
+
const srcRow = y * pHeader.width;
|
|
2644
|
+
for (let x = 0; x < pHeader.width; x++) {
|
|
2645
|
+
newPixels[dstRow + x] = newPixels[dstRow + x] + pPixels[srcRow + x] & 255;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
const newHeader = Buffer.alloc(HEADER_SIZE);
|
|
2650
|
+
newHeader.writeInt16LE(prevHeader.mapId, 0);
|
|
2651
|
+
newHeader.writeInt16LE(pHeader.frameId, 2);
|
|
2652
|
+
newHeader[4] = FRAME_TYPE.I;
|
|
2653
|
+
newHeader.writeInt16LE(pHeader.robotX, 5);
|
|
2654
|
+
newHeader.writeInt16LE(pHeader.robotY, 7);
|
|
2655
|
+
newHeader.writeInt16LE(pHeader.robotA, 9);
|
|
2656
|
+
newHeader.writeInt16LE(pHeader.chargerX, 11);
|
|
2657
|
+
newHeader.writeInt16LE(pHeader.chargerY, 13);
|
|
2658
|
+
newHeader.writeInt16LE(pHeader.chargerA, 15);
|
|
2659
|
+
newHeader.writeInt16LE(grid, 17);
|
|
2660
|
+
newHeader.writeInt16LE(unionWidth, 19);
|
|
2661
|
+
newHeader.writeInt16LE(unionHeight, 21);
|
|
2662
|
+
newHeader.writeInt16LE(unionLeft, 23);
|
|
2663
|
+
newHeader.writeInt16LE(unionTop, 25);
|
|
2664
|
+
const mergedTail = mergeTails(prevTail, pTail, unionLeft, unionTop);
|
|
2665
|
+
const tailBytes = Buffer.from(JSON.stringify(mergedTail), "utf8");
|
|
2666
|
+
return Buffer.concat([newHeader, newPixels, tailBytes]);
|
|
2667
|
+
}
|
|
2668
|
+
function mergePFrameEnvelope(prev, pframe, prevOpts, pframeOpts) {
|
|
2669
|
+
const prevBuf = typeof prev === "string" ? unwrapEnvelope(prev, prevOpts) : prev;
|
|
2670
|
+
const pBuf = typeof pframe === "string" ? unwrapEnvelope(pframe, pframeOpts) : pframe;
|
|
2671
|
+
return mergePFrame(prevBuf, pBuf);
|
|
2672
|
+
}
|
|
2673
|
+
function mergeTails(prev, p, unionLeft, unionTop) {
|
|
2674
|
+
const merged = { ...p };
|
|
2675
|
+
merged.origin = [unionLeft, unionTop];
|
|
2676
|
+
const prevTr = typeof prev.tr === "string" ? prev.tr : "";
|
|
2677
|
+
const pTr = typeof p.tr === "string" ? p.tr : "";
|
|
2678
|
+
if (prevTr || pTr) {
|
|
2679
|
+
merged.tr = prevTr + pTr;
|
|
2680
|
+
} else {
|
|
2681
|
+
delete merged.tr;
|
|
2682
|
+
}
|
|
2683
|
+
if (!("seg_inf" in p) && "seg_inf" in prev) {
|
|
2684
|
+
merged.seg_inf = prev.seg_inf;
|
|
2685
|
+
}
|
|
2686
|
+
if (!("sa" in p) && "sa" in prev) {
|
|
2687
|
+
merged.sa = prev.sa;
|
|
2688
|
+
}
|
|
2689
|
+
for (const key2 of PERSISTENT_TAIL_KEYS) {
|
|
2690
|
+
if (!(key2 in p) && key2 in prev) {
|
|
2691
|
+
merged[key2] = prev[key2];
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
return merged;
|
|
2695
|
+
}
|
|
2696
|
+
var PERSISTENT_TAIL_KEYS = [
|
|
2697
|
+
"vw",
|
|
2698
|
+
"vws",
|
|
2699
|
+
"sneak_areas",
|
|
2700
|
+
"sneak_areas_end",
|
|
2701
|
+
"walls_info",
|
|
2702
|
+
"rism",
|
|
2703
|
+
"decmap"
|
|
2704
|
+
];
|
|
2705
|
+
|
|
2555
2706
|
// src/models/vacuum/map/decode.ts
|
|
2556
2707
|
function decodeVacuumMap(input, opts = {}) {
|
|
2557
2708
|
const inflated = typeof input === "string" ? unwrapEnvelope(input, opts) : looksLikeBase64Zlib(input) ? unwrapEnvelope(input.toString("latin1"), opts) : input;
|
|
@@ -2596,6 +2747,10 @@ function decodeVacuumMap(input, opts = {}) {
|
|
|
2596
2747
|
cleanedArea
|
|
2597
2748
|
};
|
|
2598
2749
|
}
|
|
2750
|
+
function applyVacuumPFrame(prev, pframe, opts = {}) {
|
|
2751
|
+
const merged = typeof prev === "string" || typeof pframe === "string" ? mergePFrameEnvelope(prev, pframe, opts.prev, opts.pframe) : mergePFrame(prev, pframe);
|
|
2752
|
+
return { buffer: merged, data: decodeVacuumMap(merged) };
|
|
2753
|
+
}
|
|
2599
2754
|
function mergeDimensions(header, tail) {
|
|
2600
2755
|
const left = tail.origin?.[0] ?? header.left;
|
|
2601
2756
|
const top = tail.origin?.[1] ?? header.top;
|
|
@@ -3533,6 +3688,14 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
|
|
|
3533
3688
|
*/
|
|
3534
3689
|
async getMap(input) {
|
|
3535
3690
|
this.#requireCap(this.#caps.canMap, "getMap", "map decoding");
|
|
3691
|
+
const blob = await this.#fetchMapBlob(input);
|
|
3692
|
+
const map = decodeVacuumMap(blob, this.#decodeOpts(input));
|
|
3693
|
+
this.#lastMap = map;
|
|
3694
|
+
this.emit("map", map);
|
|
3695
|
+
return map;
|
|
3696
|
+
}
|
|
3697
|
+
/** Fetch the raw OSS blob (still the base64+zlib envelope) for a map frame. */
|
|
3698
|
+
async #fetchMapBlob(input) {
|
|
3536
3699
|
const session = this.currentSession();
|
|
3537
3700
|
const region = this.region;
|
|
3538
3701
|
const fetcher = input.fetcher ?? new OssFetcher();
|
|
@@ -3546,14 +3709,77 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
|
|
|
3546
3709
|
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {},
|
|
3547
3710
|
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3548
3711
|
};
|
|
3549
|
-
|
|
3550
|
-
|
|
3712
|
+
return fetcher.fetchBlob(fetchInput);
|
|
3713
|
+
}
|
|
3714
|
+
/** The AES key/iv decode options, threaded from a {@link VacuumGetMapInput}. */
|
|
3715
|
+
#decodeOpts(input) {
|
|
3716
|
+
return {
|
|
3551
3717
|
...input.key !== void 0 ? { key: input.key } : {},
|
|
3552
3718
|
...input.iv !== void 0 ? { iv: input.iv } : {}
|
|
3553
|
-
}
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
/**
|
|
3722
|
+
* The merge base for live P-frame streaming — always an INFLATED frame buffer.
|
|
3723
|
+
* An I-frame (re)seeds it; each P-frame merge replaces it with the merged
|
|
3724
|
+
* inflated buffer (so further P-frames stack). `null` until the first I-frame
|
|
3725
|
+
* (or after an out-of-order / map-id reset).
|
|
3726
|
+
*/
|
|
3727
|
+
#mapStreamBase = null;
|
|
3728
|
+
/** Drop the P-frame merge base so the next I-frame re-seeds the stream. */
|
|
3729
|
+
resetMapStream() {
|
|
3730
|
+
this.#mapStreamBase = null;
|
|
3731
|
+
}
|
|
3732
|
+
/** Inflate an OSS blob to a raw frame buffer (live envelope → zlib; verbatim if already inflated). */
|
|
3733
|
+
#inflateFrame(blob, opts) {
|
|
3734
|
+
return looksLikeBase64Zlib(blob) ? unwrapEnvelope(blob.toString("latin1"), opts) : blob;
|
|
3735
|
+
}
|
|
3736
|
+
/**
|
|
3737
|
+
* Fetch the latest advertised map frame and fold it into a continuously
|
|
3738
|
+
* updating map. An I-frame (re)seeds the merge base and renders standalone; a
|
|
3739
|
+
* P-frame is merged onto the base via {@link applyVacuumPFrame} so the live
|
|
3740
|
+
* grid stays COMPLETE (a P-frame decoded standalone is only byte-deltas). On an
|
|
3741
|
+
* out-of-order P-frame or a map-id change the base is dropped and `null`
|
|
3742
|
+
* returned — the next I-frame re-seeds. Returns `null` when no frame is
|
|
3743
|
+
* advertised yet, or a P-frame arrives before any I-frame.
|
|
3744
|
+
*
|
|
3745
|
+
* Caches {@link lastMap} and emits `'map'` exactly like {@link getMap}, so a
|
|
3746
|
+
* map-watching consumer (e.g. the camstack map child) gets a fresh complete
|
|
3747
|
+
* frame on every push during a live clean.
|
|
3748
|
+
*/
|
|
3749
|
+
async fetchLatestMapStreaming(opts = {}) {
|
|
3750
|
+
this.#requireCap(this.#caps.canMap, "fetchLatestMapStreaming", "map decoding");
|
|
3751
|
+
const filename = this.mapFilename;
|
|
3752
|
+
if (filename === null) return null;
|
|
3753
|
+
const input = { filename, ...opts };
|
|
3754
|
+
const decodeOpts = this.#decodeOpts(input);
|
|
3755
|
+
const blob = await this.#fetchMapBlob(input);
|
|
3756
|
+
const inflated = this.#inflateFrame(blob, decodeOpts);
|
|
3757
|
+
const frame = decodeVacuumMap(inflated, decodeOpts);
|
|
3758
|
+
if (frame.frameType === "I") {
|
|
3759
|
+
this.#mapStreamBase = inflated;
|
|
3760
|
+
this.#lastMap = frame;
|
|
3761
|
+
this.emit("map", frame);
|
|
3762
|
+
return frame;
|
|
3763
|
+
}
|
|
3764
|
+
if (frame.frameType === "P") {
|
|
3765
|
+
if (this.#mapStreamBase === null) return null;
|
|
3766
|
+
try {
|
|
3767
|
+
const { buffer, data } = applyVacuumPFrame(this.#mapStreamBase, inflated);
|
|
3768
|
+
this.#mapStreamBase = buffer;
|
|
3769
|
+
this.#lastMap = data;
|
|
3770
|
+
this.emit("map", data);
|
|
3771
|
+
return data;
|
|
3772
|
+
} catch (err) {
|
|
3773
|
+
if (err instanceof OutOfOrderFrameError || err instanceof MapDecodeError) {
|
|
3774
|
+
this.#mapStreamBase = null;
|
|
3775
|
+
return null;
|
|
3776
|
+
}
|
|
3777
|
+
throw err;
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
this.#lastMap = frame;
|
|
3781
|
+
this.emit("map", frame);
|
|
3782
|
+
return frame;
|
|
3557
3783
|
}
|
|
3558
3784
|
/** Props worth seeding on start() / polling — exported for the facade. */
|
|
3559
3785
|
static DEFAULT_PROPS = [
|