homebridge-roborock-matter 3.4.8 → 3.4.10
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/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/roborockLib/lib/b01Q7Adapter.js +192 -1
- package/roborockLib/roborockAPI.js +87 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.4.10
|
|
4
|
+
|
|
5
|
+
**The Q7 position that never resolved to a room is not a position at all.** 3.4.9 asked the two Q7s to report the range their room outlines occupy, and they answered: Garage sat in a map spanning cells 52–171 by 43–187, 1. Sal in one spanning 38–293 by 90–227. Back-computing through each map's own origin and resolution gives the same coordinate for both — exactly (1100.0, 1100.0), on two robots, two maps, and twelve minutes of active cleaning. A number that identical is arithmetic, not a place a robot stood, which means live-room tracking on these models has never worked from that field.
|
|
6
|
+
|
|
7
|
+
- **The miss line now surveys the payload rather than asserting anything about it.** It prints the size of every top-level field and every scalar inside the small ones, keyed by field path. Two consecutive lines are then a diff: the value that changed while the robot was driving is the position, and the submessage that grew is the trail behind it.
|
|
8
|
+
- **Varints are surveyed, not just floats.** The pose message carries an `update` flag alongside its coordinates, so a float-only dump would have printed two plausible-looking numbers and hidden the field saying they were stale.
|
|
9
|
+
- **The survey descends one level.** A pose trail's last point is by construction where the robot is now, and repeated paths overwrite, so the end of a trail lands in the log under a stable key.
|
|
10
|
+
- **A bare scalar on the map itself is now visible.** The parse loop only ever descended into submessages, so a position stored as a plain float would not have appeared anywhere.
|
|
11
|
+
- **It is bounded and it cannot throw.** The occupancy grid is measured rather than walked, the scalar count is capped, recursion is capped, and bytes that turn out not to be protobuf are swallowed. A diagnostic must never be the reason a robot stops reporting its room.
|
|
12
|
+
|
|
13
|
+
This changes no behaviour. It exists because guessing another field number would have been the third guess in a row on this code path, and the robots were running.
|
|
14
|
+
|
|
15
|
+
## 3.4.9
|
|
16
|
+
|
|
17
|
+
- **A live-room miss now says where the rooms actually are.** Two Q7s produced position cells around 22,000 while a Roborock map is a couple of thousand cells across at most — so those robots were never "between rooms", their computed position was nowhere near the map. One of them reported x exactly equal to y, which is arithmetic rather than a place a robot stood. The position on its own cannot separate a unit mismatch from a wrong origin, so the miss line now carries the range the room outlines occupy plus the map origin and resolution the transform used. This changes no behaviour; it turns the next log from a hypothesis into a measurement. The bounding box is computed only on the failure path, so a run that resolves every position pays nothing.
|
|
18
|
+
|
|
3
19
|
## 3.4.8
|
|
4
20
|
|
|
5
21
|
**Selecting "Vacuum" and getting a vacuum-and-mop was not a display bug — one timed-out command cancelled the one that mattered.** skmzwanke reported in [#8](https://github.com/mathiashornbek/homebridge-roborock-matter/issues/8) that he selected Vacuum for a single room and the robot mopped it anyway, and his 3.4.5 log names the cause outright.
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ This is the most feature-packed, most thoroughly engineered Roborock plugin for
|
|
|
37
37
|
- 📍 **See where it's cleaning — live.** Apple Home shows _"Cleaning — Kitchen"_ with the room the robot is actually inside, updating as it moves from room to room. Works even for cleans started from the robot's button or the Roborock app. No other Homebridge plugin does this.
|
|
38
38
|
- 🧭 **One robot, one tile — and as many robots as you own.** Sign in once and your whole fleet comes along: every vacuum on your account appears as its own clean, native accessory in Apple Home. No clutter of fake fans and helper switches, and rooms appear with the names you gave them in the Roborock app.
|
|
39
39
|
- ⚡ **Fast and reliable.** Commands go directly to the robot over your own network whenever possible, with the Roborock cloud as automatic backup — and built-in diagnostics in the settings if you ever want to look under the hood.
|
|
40
|
-
- 🛡️ **Verified by Homebridge.** Reviewed and endorsed by the Homebridge team.
|
|
40
|
+
- 🛡️ **Verified by Homebridge.** Reviewed and endorsed by the Homebridge team. 463 automated tests, zero known vulnerabilities, no analytics, and a startup designed to never crash your Homebridge — even when your Wi-Fi or the Roborock cloud has a bad day.
|
|
41
41
|
|
|
42
42
|
## Features
|
|
43
43
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-roborock-matter",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.10",
|
|
4
4
|
"description": "The most complete Roborock plugin for Apple Home. Supports the entire Roborock lineup — from the classic S-series to the new 2025 Q7 series that no other plugin can control. Sign in with your Roborock account and get native start/stop, room cleaning, suction levels, battery, and live 'cleaning in the kitchen' room tracking. Verified by Homebridge.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -186,6 +186,14 @@ function readVarint(buf, pos) {
|
|
|
186
186
|
throw new Error("Malformed varint in SCMap payload");
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// Bounds for the raw field survey below. They exist to keep a diagnostic
|
|
190
|
+
// from becoming a liability: the survey is pointed at fields whose schema is
|
|
191
|
+
// unknown, so it must not be able to produce an unbounded log line, walk the
|
|
192
|
+
// occupancy grid as if it were protobuf, or recurse without end.
|
|
193
|
+
const RAW_SURVEY_MAX_SCALARS = 48;
|
|
194
|
+
const RAW_SURVEY_MAX_SUBMESSAGE_BYTES = 16384;
|
|
195
|
+
const RAW_SURVEY_MAX_DEPTH = 2;
|
|
196
|
+
|
|
189
197
|
/** @param {Buffer} buf @param {number} pos @param {number} wireType */
|
|
190
198
|
function skipField(buf, pos, wireType) {
|
|
191
199
|
switch (wireType) {
|
|
@@ -284,6 +292,11 @@ function parseRoomsFromScMap(buffer) {
|
|
|
284
292
|
* @returns {{
|
|
285
293
|
* head: {sizeX: number, sizeY: number, minX: number, minY: number, resolution: number} | null,
|
|
286
294
|
* pose: {x: number, y: number} | null,
|
|
295
|
+
* rawSurvey: {
|
|
296
|
+
* fields: Array<{field: number, count: number, bytes: number}>,
|
|
297
|
+
* scalars: Record<string, number>,
|
|
298
|
+
* truncated: boolean,
|
|
299
|
+
* },
|
|
287
300
|
* rooms: Array<{roomId: number, roomName: string}>,
|
|
288
301
|
* roomChains: Array<{roomId: number, points: Array<{x: number, y: number}>}>,
|
|
289
302
|
* }}
|
|
@@ -293,6 +306,26 @@ function parseScMapLiveState(buffer) {
|
|
|
293
306
|
let head = null;
|
|
294
307
|
/** @type {{x: number, y: number} | null} */
|
|
295
308
|
let pose = null;
|
|
309
|
+
// Diagnostic. Both Q7s in the field reported a pose of exactly
|
|
310
|
+
// (1100.0, 1100.0) — the same value on two robots, two maps and twelve
|
|
311
|
+
// minutes of active cleaning. That is a constant, not a position, so the
|
|
312
|
+
// number being read as the robot's position is not the robot's position.
|
|
313
|
+
//
|
|
314
|
+
// The schema above is not obviously wrong, which is what makes guessing
|
|
315
|
+
// another field number a bad move: it would be the third guess in a row on
|
|
316
|
+
// this code path. Two candidates fit the evidence and the survey separates
|
|
317
|
+
// them without a second release. DeviceCurrentPoseInfo carries an `update`
|
|
318
|
+
// varint, so field 8 may simply be marked stale on this firmware — a
|
|
319
|
+
// float-only dump would not have shown it. And field 6 is a pose *trail*,
|
|
320
|
+
// whose last point is by construction where the robot is now — which needs
|
|
321
|
+
// one level of nesting to see. So the survey records varints as well as
|
|
322
|
+
// floats, and descends one level: repeated paths overwrite, which leaves
|
|
323
|
+
// the last point of a trail sitting in the log under a stable key.
|
|
324
|
+
/** @type {Array<{field: number, count: number, bytes: number}>} */
|
|
325
|
+
const surveyFields = [];
|
|
326
|
+
/** @type {Record<string, number>} */
|
|
327
|
+
const surveyScalars = {};
|
|
328
|
+
let surveyTruncated = false;
|
|
296
329
|
/** @type {Array<{roomId: number, roomChainPoints?: unknown}>} */
|
|
297
330
|
const roomChains = [];
|
|
298
331
|
|
|
@@ -347,6 +380,109 @@ function parseScMapLiveState(buffer) {
|
|
|
347
380
|
: null;
|
|
348
381
|
}
|
|
349
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Record that a top-level field was seen, and how many bytes it carried.
|
|
385
|
+
*
|
|
386
|
+
* The sizes matter as much as the values: a submessage that grows between
|
|
387
|
+
* two consecutive log lines while the robot is driving is a trail of where
|
|
388
|
+
* it has been, and the field that does that is the one worth reading.
|
|
389
|
+
*
|
|
390
|
+
* @param {number} field
|
|
391
|
+
* @param {number} bytes
|
|
392
|
+
*/
|
|
393
|
+
function noteField(field, bytes) {
|
|
394
|
+
const seen = surveyFields.find((entry) => entry.field === field);
|
|
395
|
+
if (seen) {
|
|
396
|
+
seen.count += 1;
|
|
397
|
+
seen.bytes += bytes;
|
|
398
|
+
} else if (surveyFields.length < RAW_SURVEY_MAX_SCALARS) {
|
|
399
|
+
surveyFields.push({ field, count: 1, bytes });
|
|
400
|
+
} else {
|
|
401
|
+
surveyTruncated = true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* @param {string} path
|
|
407
|
+
* @param {number} value
|
|
408
|
+
*/
|
|
409
|
+
function noteScalar(path, value) {
|
|
410
|
+
if (
|
|
411
|
+
surveyScalars[path] === undefined &&
|
|
412
|
+
Object.keys(surveyScalars).length >= RAW_SURVEY_MAX_SCALARS
|
|
413
|
+
) {
|
|
414
|
+
surveyTruncated = true;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
// Deliberately last-wins. A repeated submessage collapses to its final
|
|
418
|
+
// occurrence, which for a pose trail is the current position.
|
|
419
|
+
surveyScalars[path] = value;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Walk a submessage recording every scalar it contains, keyed by dotted
|
|
424
|
+
* field path, to a bounded depth.
|
|
425
|
+
*
|
|
426
|
+
* Diagnostic only, and defensive by necessity: it is pointed at fields
|
|
427
|
+
* whose schema is unknown, so bytes that are not protobuf at all will
|
|
428
|
+
* reach it. A throw here would take live-room tracking down with it, so
|
|
429
|
+
* the caller swallows the error and keeps whatever was collected.
|
|
430
|
+
*
|
|
431
|
+
* @param {Buffer} buf
|
|
432
|
+
* @param {string} prefix
|
|
433
|
+
* @param {number} depth
|
|
434
|
+
*/
|
|
435
|
+
function surveyMessage(buf, prefix, depth) {
|
|
436
|
+
let pos = 0;
|
|
437
|
+
while (pos < buf.length) {
|
|
438
|
+
const tag = readVarint(buf, pos);
|
|
439
|
+
pos = tag.pos;
|
|
440
|
+
const fieldNumber = Math.floor(tag.value / 8);
|
|
441
|
+
const wireType = tag.value % 8;
|
|
442
|
+
const path = `${prefix}.${fieldNumber}`;
|
|
443
|
+
if (wireType === 0) {
|
|
444
|
+
const value = readVarint(buf, pos);
|
|
445
|
+
pos = value.pos;
|
|
446
|
+
noteScalar(path, value.value);
|
|
447
|
+
} else if (wireType === 5) {
|
|
448
|
+
noteScalar(path, buf.readFloatLE(pos));
|
|
449
|
+
pos += 4;
|
|
450
|
+
} else if (wireType === 1) {
|
|
451
|
+
noteScalar(path, buf.readDoubleLE(pos));
|
|
452
|
+
pos += 8;
|
|
453
|
+
} else if (wireType === 2) {
|
|
454
|
+
const len = readVarint(buf, pos);
|
|
455
|
+
if (depth > 1 && len.value > 0) {
|
|
456
|
+
surveyMessage(
|
|
457
|
+
buf.subarray(len.pos, len.pos + len.value),
|
|
458
|
+
path,
|
|
459
|
+
depth - 1
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
pos = len.pos + len.value;
|
|
463
|
+
} else {
|
|
464
|
+
pos = skipField(buf, pos, wireType);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* @param {Buffer} buf
|
|
471
|
+
* @param {number} field
|
|
472
|
+
*/
|
|
473
|
+
function surveyTopLevelSubmessage(buf, field) {
|
|
474
|
+
if (buf.length > RAW_SURVEY_MAX_SUBMESSAGE_BYTES) {
|
|
475
|
+
// The occupancy grid is tens of kilobytes of raw cells, not protobuf.
|
|
476
|
+
// Its size is recorded; walking it would be nonsense and slow.
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
surveyMessage(buf, String(field), RAW_SURVEY_MAX_DEPTH);
|
|
481
|
+
} catch {
|
|
482
|
+
surveyTruncated = true;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
350
486
|
/** @param {Buffer} buf */
|
|
351
487
|
function parseChainPoint(buf) {
|
|
352
488
|
const point = { x: 0, y: 0 };
|
|
@@ -440,6 +576,8 @@ function parseScMapLiveState(buffer) {
|
|
|
440
576
|
if (wireType === 2) {
|
|
441
577
|
const len = readVarint(buffer, pos);
|
|
442
578
|
const body = buffer.subarray(len.pos, len.pos + len.value);
|
|
579
|
+
noteField(fieldNumber, len.value);
|
|
580
|
+
surveyTopLevelSubmessage(body, fieldNumber);
|
|
443
581
|
if (fieldNumber === 3) {
|
|
444
582
|
head = parseMapHead(body);
|
|
445
583
|
} else if (fieldNumber === 8) {
|
|
@@ -457,6 +595,17 @@ function parseScMapLiveState(buffer) {
|
|
|
457
595
|
}
|
|
458
596
|
pos = len.pos + len.value;
|
|
459
597
|
} else {
|
|
598
|
+
// A position could just as easily be a bare float or varint on the
|
|
599
|
+
// RobotMap itself; the loop above only ever looked at submessages, so
|
|
600
|
+
// such a field would never have been seen at all.
|
|
601
|
+
noteField(fieldNumber, 0);
|
|
602
|
+
if (wireType === 0) {
|
|
603
|
+
noteScalar(String(fieldNumber), readVarint(buffer, pos).value);
|
|
604
|
+
} else if (wireType === 5) {
|
|
605
|
+
noteScalar(String(fieldNumber), buffer.readFloatLE(pos));
|
|
606
|
+
} else if (wireType === 1) {
|
|
607
|
+
noteScalar(String(fieldNumber), buffer.readDoubleLE(pos));
|
|
608
|
+
}
|
|
460
609
|
pos = skipField(buffer, pos, wireType);
|
|
461
610
|
}
|
|
462
611
|
}
|
|
@@ -464,6 +613,11 @@ function parseScMapLiveState(buffer) {
|
|
|
464
613
|
return {
|
|
465
614
|
head,
|
|
466
615
|
pose,
|
|
616
|
+
rawSurvey: {
|
|
617
|
+
fields: surveyFields,
|
|
618
|
+
scalars: surveyScalars,
|
|
619
|
+
truncated: surveyTruncated,
|
|
620
|
+
},
|
|
467
621
|
rooms,
|
|
468
622
|
roomChains:
|
|
469
623
|
/** @type {Array<{roomId: number, points: Array<{x: number, y: number}>}>} */ (
|
|
@@ -504,7 +658,9 @@ function resolveLiveRoomId(liveState) {
|
|
|
504
658
|
* @returns {{roomId: number | null,
|
|
505
659
|
* reason: "resolved" | "no-map-header" | "no-pose" | "no-room-outlines" | "pose-outside-outlines",
|
|
506
660
|
* outlineCount: number,
|
|
507
|
-
* cell: {x: number, y: number} | null
|
|
661
|
+
* cell: {x: number, y: number} | null,
|
|
662
|
+
* outlineBounds?: {minX: number, minY: number, maxX: number, maxY: number} | null,
|
|
663
|
+
* head?: {minX: number, minY: number, resolution: number}}}
|
|
508
664
|
*/
|
|
509
665
|
function describeLiveRoomResolution(liveState) {
|
|
510
666
|
const head = liveState?.head;
|
|
@@ -541,9 +697,44 @@ function describeLiveRoomResolution(liveState) {
|
|
|
541
697
|
reason: "pose-outside-outlines",
|
|
542
698
|
outlineCount,
|
|
543
699
|
cell,
|
|
700
|
+
outlineBounds: outlineBoundingBox(chains),
|
|
701
|
+
head: { minX: head.minX, minY: head.minY, resolution },
|
|
544
702
|
};
|
|
545
703
|
}
|
|
546
704
|
|
|
705
|
+
/**
|
|
706
|
+
* Bounding box of every room outline, in the same cell space the
|
|
707
|
+
* point-in-polygon test uses.
|
|
708
|
+
*
|
|
709
|
+
* Field logs showed two Q7s reporting position cells around 22,000 while a
|
|
710
|
+
* Roborock map is at most a couple of thousand cells across — so the position
|
|
711
|
+
* is not "between rooms", it is nowhere near the map. Whether that is a unit
|
|
712
|
+
* mismatch (pose in millimetres against a resolution in metres) or a wrong
|
|
713
|
+
* origin cannot be told from the position alone: it needs the range the
|
|
714
|
+
* outlines actually occupy. Printing both next to each other turns a guess
|
|
715
|
+
* into a measurement.
|
|
716
|
+
*
|
|
717
|
+
* @param {Array<{points: Array<{x: number, y: number}>}>} chains
|
|
718
|
+
* @returns {{minX: number, minY: number, maxX: number, maxY: number} | null}
|
|
719
|
+
*/
|
|
720
|
+
function outlineBoundingBox(chains) {
|
|
721
|
+
let minX = Infinity;
|
|
722
|
+
let minY = Infinity;
|
|
723
|
+
let maxX = -Infinity;
|
|
724
|
+
let maxY = -Infinity;
|
|
725
|
+
|
|
726
|
+
for (const chain of chains) {
|
|
727
|
+
for (const point of chain.points || []) {
|
|
728
|
+
if (point.x < minX) minX = point.x;
|
|
729
|
+
if (point.y < minY) minY = point.y;
|
|
730
|
+
if (point.x > maxX) maxX = point.x;
|
|
731
|
+
if (point.y > maxY) maxY = point.y;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return Number.isFinite(minX) ? { minX, minY, maxX, maxY } : null;
|
|
736
|
+
}
|
|
737
|
+
|
|
547
738
|
/**
|
|
548
739
|
* Standard ray-casting point-in-polygon test over a room boundary chain.
|
|
549
740
|
* @param {number} x @param {number} y
|
|
@@ -95,6 +95,92 @@ const B01_LIVE_ROOM_MISS_REASONS = {
|
|
|
95
95
|
"the robot's position did not fall inside any known room outline (it may be between rooms, or the map may still be building)",
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The outline range and map origin, appended to a live-room miss.
|
|
100
|
+
*
|
|
101
|
+
* A position cell on its own cannot distinguish "the robot is between rooms"
|
|
102
|
+
* from "the position was computed in the wrong units" — and the field logs
|
|
103
|
+
* showed cells near 22,000 where a Roborock map is a couple of thousand cells
|
|
104
|
+
* at most. Printing the range the outlines occupy, plus the origin and
|
|
105
|
+
* resolution the transform used, makes the difference measurable from one log
|
|
106
|
+
* line instead of inferable from none.
|
|
107
|
+
*
|
|
108
|
+
* @param {{outlineBounds?: {minX: number, minY: number, maxX: number, maxY: number} | null,
|
|
109
|
+
* head?: {minX: number, minY: number, resolution: number}}} resolution
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
function describeOutlineBounds(resolution) {
|
|
113
|
+
const bounds = resolution?.outlineBounds;
|
|
114
|
+
if (!bounds) {
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const head = resolution.head;
|
|
119
|
+
const origin = head
|
|
120
|
+
? `, map origin ${head.minX},${head.minY} at ${head.resolution}/cell`
|
|
121
|
+
: "";
|
|
122
|
+
|
|
123
|
+
return `, outlines span ${Math.round(bounds.minX)}-${Math.round(bounds.maxX)} x ${Math.round(bounds.minY)}-${Math.round(bounds.maxY)}${origin}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The raw SCMap fields behind a live-room miss.
|
|
128
|
+
*
|
|
129
|
+
* Two Q7s reported a position of exactly (1100, 1100) — the same value on two
|
|
130
|
+
* robots, two maps and twelve minutes of active cleaning. A constant is not a
|
|
131
|
+
* position, so the number being read as the robot's position is not the
|
|
132
|
+
* robot's position.
|
|
133
|
+
*
|
|
134
|
+
* Rather than guess another field number, this prints what the payload
|
|
135
|
+
* actually contains: the size of every top-level field and every scalar in
|
|
136
|
+
* the small ones. Two consecutive lines are then a diff — the value that
|
|
137
|
+
* changed while the robot was driving is the position, and the submessage
|
|
138
|
+
* that grew is the trail it left. That turns the next fix into a reading
|
|
139
|
+
* rather than a fourth guess.
|
|
140
|
+
*
|
|
141
|
+
* @param {{rawSurvey?: {fields?: Array<{field: number, count: number, bytes: number}>,
|
|
142
|
+
* scalars?: Record<string, number>,
|
|
143
|
+
* truncated?: boolean} | null}} parsed
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
function describeRawMapFields(parsed) {
|
|
147
|
+
const survey = parsed?.rawSurvey;
|
|
148
|
+
const fields = survey?.fields;
|
|
149
|
+
if (!Array.isArray(fields) || fields.length === 0) {
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const shape = fields
|
|
154
|
+
.map(
|
|
155
|
+
(entry) =>
|
|
156
|
+
`${entry.field}:${entry.bytes}B${entry.count > 1 ? `x${entry.count}` : ""}`
|
|
157
|
+
)
|
|
158
|
+
.join(" ");
|
|
159
|
+
|
|
160
|
+
const scalars = Object.entries(survey?.scalars || {})
|
|
161
|
+
.map(([path, value]) => `${path}=${formatSurveyScalar(value)}`)
|
|
162
|
+
.join(" ");
|
|
163
|
+
|
|
164
|
+
return `, map fields ${shape}${scalars ? `, scalars ${scalars}` : ""}${
|
|
165
|
+
survey?.truncated ? " (truncated)" : ""
|
|
166
|
+
}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A survey value short enough to sit in a log line, precise enough to see a
|
|
171
|
+
* robot move. Three decimals of a metre is a millimetre; three decimals of a
|
|
172
|
+
* millimetre is far below anything a vacuum reports.
|
|
173
|
+
*
|
|
174
|
+
* @param {number} value
|
|
175
|
+
* @returns {string}
|
|
176
|
+
*/
|
|
177
|
+
function formatSurveyScalar(value) {
|
|
178
|
+
if (!Number.isFinite(value)) {
|
|
179
|
+
return String(value);
|
|
180
|
+
}
|
|
181
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(3);
|
|
182
|
+
}
|
|
183
|
+
|
|
98
184
|
const B01_STATUS_TICK_MS = 15000;
|
|
99
185
|
const B01_STATUS_FORCED_GAP_MS = 1500;
|
|
100
186
|
const B01_STATUS_ACTIVE_GAP_MS = 12000;
|
|
@@ -4291,7 +4377,7 @@ class Roborock {
|
|
|
4291
4377
|
// because they call for different fixes.
|
|
4292
4378
|
liveState.unresolvedPoseCount =
|
|
4293
4379
|
(liveState.unresolvedPoseCount || 0) + 1;
|
|
4294
|
-
const message = `Live room for ${this.describeDevice(duid)}: ${B01_LIVE_ROOM_MISS_REASONS[resolution2.reason]} (attempt ${liveState.unresolvedPoseCount} this run, ${resolution2.outlineCount} room outline(s) in the map${resolution2.cell ? `, position cell ${Math.round(resolution2.cell.x)},${Math.round(resolution2.cell.y)}` : ""}).`;
|
|
4380
|
+
const message = `Live room for ${this.describeDevice(duid)}: ${B01_LIVE_ROOM_MISS_REASONS[resolution2.reason]} (attempt ${liveState.unresolvedPoseCount} this run, ${resolution2.outlineCount} room outline(s) in the map${resolution2.cell ? `, position cell ${Math.round(resolution2.cell.x)},${Math.round(resolution2.cell.y)}` : ""}${describeOutlineBounds(resolution2)}${describeRawMapFields(parsed)}).`;
|
|
4295
4381
|
if (liveState.unresolvedPoseCount % 5 === 0) {
|
|
4296
4382
|
this.log.info(message);
|
|
4297
4383
|
} else {
|