@7365admin1/layer-common 4.57.3 → 4.58.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/CHANGELOG.md +10 -4
- package/components/Robot/BuildingMap.vue +8 -0
- package/components/Robot/Detail.vue +23 -7
- package/components/Robot/LivePlan.vue +1 -1
- package/components/Robot/MiniMap.vue +9 -0
- package/package.json +1 -1
- package/utils/robot-control.ts +23 -12
- package/utils/robot-display.ts +32 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.58.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fd4c0c9: Robots: when a robot is online but its position isn't arriving, robot detail, the site mini-map and the building map say so in a calm banner ("<Robot> is online, but we're not receiving its position right now.") and detail keeps the last position seen faded, tagged "last known · HH:MM". The live-view segment fix now also catches a stray H.264 delimiter split across two TS packets. New `scripts/sync-robot-live.mjs` copies the web live-view player into the mobile apps.
|
|
8
|
+
|
|
3
9
|
## 4.57.3
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -1611,11 +1617,11 @@
|
|
|
1611
1617
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1612
1618
|
own rule from two endpoints the console already calls, unprojected:
|
|
1613
1619
|
|
|
1614
|
-
|
|
1615
|
-
|
|
1620
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1621
|
+
GET /api/roles/id/:role that membership's role document
|
|
1616
1622
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1623
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1624
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1619
1625
|
|
|
1620
1626
|
`role.default` is the marker because it is the only property of a platform
|
|
1621
1627
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -219,6 +219,7 @@
|
|
|
219
219
|
</div>
|
|
220
220
|
<div class="hero-bmap__traytext">{{ unplacedLine }}</div>
|
|
221
221
|
</div>
|
|
222
|
+
<div v-if="gapText" class="hero-notice" role="status">{{ gapText }}</div>
|
|
222
223
|
</aside>
|
|
223
224
|
</div>
|
|
224
225
|
</template>
|
|
@@ -246,6 +247,8 @@ import {
|
|
|
246
247
|
routeTags,
|
|
247
248
|
rowStatus,
|
|
248
249
|
unplacedRobots,
|
|
250
|
+
positionGap,
|
|
251
|
+
positionGapText,
|
|
249
252
|
unplacedText,
|
|
250
253
|
type TMapRobot,
|
|
251
254
|
type TPatrolRouteView,
|
|
@@ -430,6 +433,11 @@ const shafts = computed(() =>
|
|
|
430
433
|
);
|
|
431
434
|
|
|
432
435
|
const unplaced = computed(() => unplacedRobots(rows.value, states.value));
|
|
436
|
+
/** Online robots whose position isn't arriving: not drawn, named instead. */
|
|
437
|
+
const gapText = computed(() => {
|
|
438
|
+
const names = rows.value.filter((r) => positionGap(states.value[String(r._id)], states.value[String(r._id)]?.capabilities)).map((r) => String(r.name ?? ""));
|
|
439
|
+
return names.length ? positionGapText(names) : "";
|
|
440
|
+
});
|
|
433
441
|
const unplacedLine = computed(() => unplacedText(unplaced.value.map((r) => brandName(r.brand, brands.value))) ?? "");
|
|
434
442
|
|
|
435
443
|
const emptyNote = computed(() => {
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
<div v-if="!noInterface" class="hero-detail__grid">
|
|
75
75
|
<div>
|
|
76
76
|
<div class="hero-section__title">Location</div>
|
|
77
|
+
<div v-if="gapText" class="hero-notice" role="status">{{ gapText }}</div>
|
|
77
78
|
<div v-if="locationFacts.length" class="hero-facts hero-facts--tight" role="list" aria-label="Robot location">
|
|
78
79
|
<div v-for="f in locationFacts" :key="f.label" class="hero-fact" role="listitem">
|
|
79
80
|
<div class="hero-fact__label">{{ f.label }}</div>
|
|
@@ -95,12 +96,12 @@
|
|
|
95
96
|
</div>
|
|
96
97
|
<RobotLivePlan
|
|
97
98
|
:name="record?.name"
|
|
98
|
-
:status-label="statusLabel"
|
|
99
|
-
:point="posLive && robotOnShownMap ? point : null"
|
|
100
|
-
:stale="shown?.stale === true"
|
|
101
|
-
:heading="headingDeg(shown)"
|
|
102
|
-
:trail="trail"
|
|
103
|
-
:readout="readout"
|
|
99
|
+
:status-label="showLastKnown ? lastKnownLabel(lastKnown!.at, now) : statusLabel"
|
|
100
|
+
:point="posLive && robotOnShownMap ? point : showLastKnown ? lastKnown!.point : null"
|
|
101
|
+
:stale="shown?.stale === true || showLastKnown"
|
|
102
|
+
:heading="showLastKnown ? lastKnown!.heading : headingDeg(shown)"
|
|
103
|
+
:trail="showLastKnown ? [] : trail"
|
|
104
|
+
:readout="showLastKnown ? '' : readout"
|
|
104
105
|
:lost="posLost"
|
|
105
106
|
:route="route"
|
|
106
107
|
:routes="selectedMap ? mapRoutes : null"
|
|
@@ -242,6 +243,7 @@ import {
|
|
|
242
243
|
faultBadge,
|
|
243
244
|
headingDeg,
|
|
244
245
|
isMoving,
|
|
246
|
+
lastKnownLabel,
|
|
245
247
|
mergePose,
|
|
246
248
|
optimisticMode,
|
|
247
249
|
patrolRunHint,
|
|
@@ -250,6 +252,8 @@ import {
|
|
|
250
252
|
savedPointGroups,
|
|
251
253
|
unplacedStopsText,
|
|
252
254
|
planPoint,
|
|
255
|
+
positionGap,
|
|
256
|
+
positionGapText,
|
|
253
257
|
positionText,
|
|
254
258
|
robotAbilities,
|
|
255
259
|
robotErrorMessage,
|
|
@@ -379,6 +383,16 @@ const point = computed(() => planPoint(shown.value as any));
|
|
|
379
383
|
const posLive = computed(() => reportsPosition.value && online.value && !streamLost.value && !!point.value);
|
|
380
384
|
const posLost = computed(() => reportsPosition.value && !!live.value && (!live.value.ok || live.value.online === false || streamLost.value));
|
|
381
385
|
const trail = ref<Array<{ x: number; y: number }>>([]);
|
|
386
|
+
/* Online, but no coordinates arriving (a gap on our side, not "the robot is lost"): a calm banner, and the last position seen on this screen drawn faded. */
|
|
387
|
+
const gap = computed(() => positionGap(live.value, caps.value));
|
|
388
|
+
const lastKnown = ref<{ point: { left: string; top: string }; heading: number; at: number; mapId: string | null } | null>(null);
|
|
389
|
+
watch(point, (p) => {
|
|
390
|
+
if (p && posLive.value) lastKnown.value = { point: p, heading: headingDeg(shown.value), at: live.value?.positionAt ?? Date.now(), mapId: live.value?.mapId ?? null };
|
|
391
|
+
});
|
|
392
|
+
const showLastKnown = computed(
|
|
393
|
+
() => gap.value && !!lastKnown.value && (!selectedMap.value || !lastKnown.value.mapId || lastKnown.value.mapId === selectedMap.value.mapId),
|
|
394
|
+
);
|
|
395
|
+
const gapText = computed(() => (gap.value ? positionGapText(record.value?.name ?? "", showLastKnown.value) : ""));
|
|
382
396
|
watch(
|
|
383
397
|
() => shown.value?.planPosition,
|
|
384
398
|
(p) => {
|
|
@@ -455,7 +469,9 @@ watch(
|
|
|
455
469
|
/** Without robot maps the placeholder plan shows the robot as before; with them, only on its own map. */
|
|
456
470
|
const robotOnShownMap = computed(() => !selectedMap.value || selectedMap.value.current);
|
|
457
471
|
const currentMapLabel = computed(() => mapViews.value.find((m) => m.current)?.label ?? "");
|
|
458
|
-
const hereText = computed(() =>
|
|
472
|
+
const hereText = computed(() =>
|
|
473
|
+
currentMapLabel.value ? `The robot is on ${currentMapLabel.value}` : gap.value ? "" : "We're not receiving which map the robot is on right now",
|
|
474
|
+
);
|
|
459
475
|
/** The routes saved on the map shown (a route without a map id goes with the robot's current map). */
|
|
460
476
|
const mapRoutes = computed(() =>
|
|
461
477
|
(patrol.value?.routes ?? []).filter((r) => (r.mapId ? r.mapId === selectedMap.value?.mapId : selectedMap.value?.current)),
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<span v-for="t in trailDots(trail)" :key="t.key" class="hero-trail" :style="{ left: t.left, top: t.top, opacity: t.op }" aria-hidden="true" />
|
|
19
19
|
<span class="hero-cross hero-cross--h" :style="{ top: point.top, transition: `top ${tickMs}ms linear` }" aria-hidden="true" />
|
|
20
20
|
<span class="hero-cross hero-cross--v" :style="{ left: point.left, transition: `left ${tickMs}ms linear` }" aria-hidden="true" />
|
|
21
|
-
<div class="hero-readout">{{ readout }}</div>
|
|
21
|
+
<div v-if="readout" class="hero-readout">{{ readout }}</div>
|
|
22
22
|
<div class="hero-me" :class="{ 'hero-me--stale': stale }" :style="{ left: point.left, top: point.top, transition: `left ${tickMs}ms linear, top ${tickMs}ms linear` }">
|
|
23
23
|
<div class="hero-me__tag">{{ name }} <span>{{ statusLabel }}</span></div>
|
|
24
24
|
<div class="hero-me__radar" aria-hidden="true" />
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
<span v-if="floor !== null" class="hero-mini__count">{{ robots.length }} on {{ floorName(floor) }}</span>
|
|
56
56
|
</div>
|
|
57
57
|
<div v-if="unplaced" class="hero-notice">{{ unplaced }}</div>
|
|
58
|
+
<div v-if="gapText" class="hero-notice" role="status">{{ gapText }}</div>
|
|
58
59
|
</div>
|
|
59
60
|
</template>
|
|
60
61
|
|
|
@@ -72,6 +73,8 @@ import {
|
|
|
72
73
|
mapRobots,
|
|
73
74
|
nameTagSide,
|
|
74
75
|
pickFloor,
|
|
76
|
+
positionGap,
|
|
77
|
+
positionGapText,
|
|
75
78
|
robotStatus,
|
|
76
79
|
unplacedRobots,
|
|
77
80
|
unplacedText,
|
|
@@ -159,6 +162,12 @@ const unplaced = computed(() =>
|
|
|
159
162
|
unplacedText(unplacedRobots(props.rows, props.states).map((r) => brandName(r.brand, props.brands))),
|
|
160
163
|
);
|
|
161
164
|
|
|
165
|
+
/** Online robots whose position isn't arriving: not drawn (never a guessed dot), named here instead. */
|
|
166
|
+
const gapText = computed(() => {
|
|
167
|
+
const names = props.rows.filter((r) => positionGap(props.states[String(r._id)], props.states[String(r._id)]?.capabilities)).map((r) => String(r.name ?? ""));
|
|
168
|
+
return names.length ? positionGapText(names) : "";
|
|
169
|
+
});
|
|
170
|
+
|
|
162
171
|
const TONE: Record<string, string> = {
|
|
163
172
|
success: "rgb(var(--v-theme-success))",
|
|
164
173
|
warning: "rgb(var(--v-theme-warning))",
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.58.0",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
package/utils/robot-control.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Robot controls: the live-view player's URL handling and the manual-drive
|
|
3
|
+
* client. Not part of the mobile-synced `robot-display.ts`. The live-view
|
|
4
|
+
* player also runs on the phones: `scripts/sync-robot-live.mjs` bundles
|
|
5
|
+
* `startLivePlayer` into a WebView page for the RN apps. Manual control stays
|
|
6
|
+
* web-only (parity exception, like map editing, decision 81).
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
/* ============================================================ live view */
|
|
@@ -76,7 +77,10 @@ export type THlsCtor = { isSupported(): boolean; new (config?: Record<string, un
|
|
|
76
77
|
*/
|
|
77
78
|
export function neutraliseStrayAud(data: ArrayBuffer | Uint8Array): number {
|
|
78
79
|
const b = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
79
|
-
|
|
80
|
+
// Per video PID: where its last 4 payload bytes sit, so a header split across
|
|
81
|
+
// two TS packets is still found (a remuxed stream can put one there - 6 of 240
|
|
82
|
+
// in a local ffmpeg remux, and one missed header stops the picture).
|
|
83
|
+
const tails = new Map<number, number[]>();
|
|
80
84
|
let n = 0;
|
|
81
85
|
for (let i = 0; i + 188 <= b.length; i += 188) {
|
|
82
86
|
if (b[i] !== 0x47) continue;
|
|
@@ -85,17 +89,24 @@ export function neutraliseStrayAud(data: ArrayBuffer | Uint8Array): number {
|
|
|
85
89
|
const pid = ((b[i + 1]! & 0x1f) << 8) | b[i + 2]!;
|
|
86
90
|
let p = i + 4;
|
|
87
91
|
if (afc & 2) p += 1 + b[p]!;
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
const end = i + 188;
|
|
93
|
+
if (p >= end) continue;
|
|
94
|
+
// A PES start names its stream: 0xE0-0xEF = video. The PES header is never inside a NAL unit.
|
|
95
|
+
if (b[i + 1]! & 0x40 && b[p] === 0 && b[p + 1] === 0 && b[p + 2] === 1 && (b[p + 3]! & 0xf0) === 0xe0) tails.set(pid, []);
|
|
96
|
+
const tail = tails.get(pid);
|
|
97
|
+
if (!tail) continue;
|
|
98
|
+
const t = tail.length;
|
|
99
|
+
const at = (k: number) => (k < t ? tail[k]! : p + k - t);
|
|
100
|
+
const len = t + end - p;
|
|
101
|
+
for (let k = 0; k + 4 < len; k++) {
|
|
102
|
+
if (b[at(k)] === 0 && b[at(k + 1)] === 0 && b[at(k + 2)] === 1 && b[at(k + 3)] === 0x09 && (b[at(k + 4)]! & 7) === 0) {
|
|
103
|
+
b[at(k + 3)] = 0x60;
|
|
104
|
+
b[at(k + 4)] = 0x01;
|
|
95
105
|
n++;
|
|
96
106
|
k += 4;
|
|
97
107
|
}
|
|
98
108
|
}
|
|
109
|
+
tails.set(pid, Array.from({ length: Math.min(4, len) }, (_, j) => at(len - Math.min(4, len) + j)));
|
|
99
110
|
}
|
|
100
111
|
return n;
|
|
101
112
|
}
|
package/utils/robot-display.ts
CHANGED
|
@@ -1204,13 +1204,44 @@ export function positionText(live: TLiveRobotState | null | undefined, capabilit
|
|
|
1204
1204
|
if (capabilities?.readLocation !== true) return "Not reported";
|
|
1205
1205
|
const p = live?.position;
|
|
1206
1206
|
if (!live?.ok || live.online === false) return "Reconnecting";
|
|
1207
|
-
// Answering, but
|
|
1207
|
+
// Answering, but no position came with this reading - said, not guessed (see positionGap).
|
|
1208
1208
|
if (!p || !finite(p.x) || !finite(p.y)) return "Position not reported right now";
|
|
1209
1209
|
const deg = finite(p.theta) ? Math.round(((((p.theta * 180) / Math.PI) % 360) + 360) % 360) : 0;
|
|
1210
1210
|
const seen = staleText(live, now);
|
|
1211
1211
|
return `x ${p.x.toFixed(1)} · y ${p.y.toFixed(1)} · ${deg}°${seen ? ` · ${seen}` : ""}`;
|
|
1212
1212
|
}
|
|
1213
1213
|
|
|
1214
|
+
/**
|
|
1215
|
+
* The robot is online and its brand is meant to report where it is, but this
|
|
1216
|
+
* reading carries no coordinates. Said as a gap in what we receive - never as
|
|
1217
|
+
* "the robot is lost": only the brand can say that, and none does today.
|
|
1218
|
+
*/
|
|
1219
|
+
export function positionGap(live: TLiveRobotState | null | undefined, capabilities: Record<string, unknown> | null | undefined): boolean {
|
|
1220
|
+
if (capabilities?.readLocation !== true || !live?.ok || live.online !== true || live.monitorable === false) return false;
|
|
1221
|
+
const p = live.position;
|
|
1222
|
+
return !p || !finite(p.x) || !finite(p.y);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* "HERO-OTH is online, but we're not receiving its position right now. Its last
|
|
1227
|
+
* known position is shown faded." Several robots: one sentence, no faded part.
|
|
1228
|
+
*/
|
|
1229
|
+
export function positionGapText(names: string | string[], lastKnownShown = false): string {
|
|
1230
|
+
const list = (Array.isArray(names) ? names : [names]).map((n) => (typeof n === "string" && n.trim()) || "The robot");
|
|
1231
|
+
if (!list.length) return "";
|
|
1232
|
+
const one = list.length === 1;
|
|
1233
|
+
const who = one ? list[0] : `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
|
|
1234
|
+
const head = `${who} ${one ? "is" : "are"} online, but we're not receiving ${one ? "its position" : "their positions"} right now.`;
|
|
1235
|
+
return one && lastKnownShown ? `${head} Its last known position is shown faded.` : head;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/** The faded marker's tag: "last known · 14:05" (device clock); "last known · 2 d ago" once older than 20 h. */
|
|
1239
|
+
export function lastKnownLabel(at: unknown, now: number = Date.now()): string {
|
|
1240
|
+
const t = typeof at === "number" ? at : at instanceof Date ? at.getTime() : NaN;
|
|
1241
|
+
if (!Number.isFinite(t)) return "last known";
|
|
1242
|
+
return now - t > 20 * 3_600_000 ? `last known · ${agoText(t, now)}` : `last known · ${new Date(t).toTimeString().slice(0, 5)}`;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1214
1245
|
export type TRobotFact = { label: string; value: string; dim: boolean };
|
|
1215
1246
|
|
|
1216
1247
|
/**
|