@7365admin1/layer-common 4.55.0 → 4.56.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/assets/css/robot-hero.css +8 -0
- package/components/Robot/Detail.vue +128 -69
- package/components/Robot/Fleet.vue +1 -1
- package/components/Robot/SiteFleet.vue +13 -2
- package/package.json +1 -1
- package/utils/robot-display.ts +138 -7
- package/utils/robot-models.fixture.json +181 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.56.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2ef1a8d: Robot detail: "What this robot can do" card built from the robot's brand + model manifest; information grouped Status / Location / Patrol|Cleaning / Camera; unsupported facts hidden (never greyed); commands confirm what will happen and report the result; "Return to charger" everywhere; maker-app schedule text from the manifest. Site fleet Brand column names the model; the capability badge's tooltip lists what that robot does.
|
|
8
|
+
|
|
3
9
|
## 4.55.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -1581,11 +1587,11 @@
|
|
|
1581
1587
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1582
1588
|
own rule from two endpoints the console already calls, unprojected:
|
|
1583
1589
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1590
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1591
|
+
GET /api/roles/id/:role that membership's role document
|
|
1586
1592
|
|
|
1587
|
-
|
|
1588
|
-
|
|
1593
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1594
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1589
1595
|
|
|
1590
1596
|
`role.default` is the marker because it is the only property of a platform
|
|
1591
1597
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -781,6 +781,14 @@
|
|
|
781
781
|
.hero-fact__value { font-weight: 600; font-variant-numeric: tabular-nums; margin-top: 3px; }
|
|
782
782
|
.hero-fact__value--dim { opacity: 0.5; }
|
|
783
783
|
.hero-notreported { font-size: 12px; opacity: 0.7; }
|
|
784
|
+
.hero-facts--tight { margin-bottom: 12px; }
|
|
785
|
+
|
|
786
|
+
/* "What this robot can do": one short row per kind of ability, only what THIS brand + model offers. */
|
|
787
|
+
.hero-can { border: 1px solid var(--hero-divider); border-left: 3px solid var(--hero-accent); padding: 10px 14px; display: grid; gap: 6px; }
|
|
788
|
+
.hero-can__title { font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 600; opacity: 0.75; }
|
|
789
|
+
.hero-can__row { display: grid; grid-template-columns: 160px 1fr; gap: 12px; font-size: 13px; line-height: 1.45; }
|
|
790
|
+
.hero-can__label { font-weight: 700; }
|
|
791
|
+
@media (max-width: 560px) { .hero-can__row { grid-template-columns: 1fr; gap: 0; } }
|
|
784
792
|
|
|
785
793
|
.hero-section__title {
|
|
786
794
|
font-size: 10px;
|
|
@@ -38,20 +38,17 @@
|
|
|
38
38
|
|
|
39
39
|
<RobotNoMonitoring v-if="noInterface" :brand-name="brand" :model="live?.model || undefined" />
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
<!-- What THIS robot (brand + model) can do, in our words; nothing it can't. -->
|
|
42
|
+
<section v-if="abilities.length" class="hero-can" aria-label="What this robot can do">
|
|
43
|
+
<div class="hero-can__title">What this robot can do</div>
|
|
44
|
+
<div v-for="r in abilities" :key="r.label" class="hero-can__row">
|
|
45
|
+
<span class="hero-can__label">{{ r.label }}</span>
|
|
46
|
+
<span>{{ r.value }}</span>
|
|
45
47
|
</div>
|
|
46
|
-
</
|
|
47
|
-
<div v-if="notReported" class="hero-notreported">{{ notReported }}</div>
|
|
48
|
-
<div v-if="(caps?.liveVideo === true && online && !fault) || canDrive" class="hero-detail__video">
|
|
49
|
-
<v-btn v-if="caps?.liveVideo === true && online && !fault" variant="outlined" @click="liveOpen = true">Live view</v-btn>
|
|
50
|
-
<v-btn v-if="canDrive" variant="outlined" @click="driveOpen = true">Manual control</v-btn>
|
|
51
|
-
</div>
|
|
48
|
+
</section>
|
|
52
49
|
|
|
53
50
|
<div v-if="commands.length">
|
|
54
|
-
<div class="hero-section__title">
|
|
51
|
+
<div class="hero-section__title">Commands</div>
|
|
55
52
|
<RobotCommandBar
|
|
56
53
|
:capabilities="caps"
|
|
57
54
|
:can-command="canCommand"
|
|
@@ -64,8 +61,70 @@
|
|
|
64
61
|
</div>
|
|
65
62
|
<RobotViewOnlyNotice v-if="!canCommand && !noInterface" />
|
|
66
63
|
|
|
67
|
-
<div
|
|
64
|
+
<div>
|
|
65
|
+
<div class="hero-section__title">Status</div>
|
|
66
|
+
<div class="hero-facts" role="list" aria-label="Robot status">
|
|
67
|
+
<div v-for="f in statusFacts" :key="f.label" class="hero-fact" role="listitem">
|
|
68
|
+
<div class="hero-fact__label">{{ f.label }}</div>
|
|
69
|
+
<div class="hero-fact__value" :class="{ 'hero-fact__value--dim': f.dim }">{{ f.value }}</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div v-if="!noInterface" class="hero-detail__grid">
|
|
68
75
|
<div>
|
|
76
|
+
<div class="hero-section__title">Location</div>
|
|
77
|
+
<div v-if="locationFacts.length" class="hero-facts hero-facts--tight" role="list" aria-label="Robot location">
|
|
78
|
+
<div v-for="f in locationFacts" :key="f.label" class="hero-fact" role="listitem">
|
|
79
|
+
<div class="hero-fact__label">{{ f.label }}</div>
|
|
80
|
+
<div class="hero-fact__value" :class="{ 'hero-fact__value--dim': f.dim }">{{ f.value }}</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<template v-if="showPlan">
|
|
84
|
+
<div class="hero-aside__head">
|
|
85
|
+
<span class="hero-aside__title">{{ planTitle }}</span>
|
|
86
|
+
<span class="hero-aside__hint" style="flex-shrink: 0">{{ planHint }}</span>
|
|
87
|
+
</div>
|
|
88
|
+
<div v-if="mapViews.length > 1 || (mapViews.length === 1 && !mapViews[0]!.current)" class="hero-mapsel">
|
|
89
|
+
<select v-model="selectedMapId" class="hero-input hero-select hero-select--sm" aria-label="Robot map">
|
|
90
|
+
<option v-for="m in mapViews" :key="m.mapId" :value="m.mapId">{{ m.label }}{{ m.current ? " · Robot is here" : "" }}</option>
|
|
91
|
+
</select>
|
|
92
|
+
<span v-if="selectedMap?.current" class="hero-mapsel__here">Robot is here</span>
|
|
93
|
+
<span v-else-if="selectedMap" class="hero-mapsel__hint">{{ hereText }}</span>
|
|
94
|
+
<span v-if="selectedMap?.routeNames.length" class="hero-mapsel__hint">Routes: {{ selectedMap.routeNames.join(", ") }}</span>
|
|
95
|
+
</div>
|
|
96
|
+
<RobotLivePlan
|
|
97
|
+
: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"
|
|
104
|
+
:lost="posLost"
|
|
105
|
+
:route="route"
|
|
106
|
+
:routes="selectedMap ? mapRoutes : null"
|
|
107
|
+
:image="selectedMap?.imageUrl ?? null"
|
|
108
|
+
:aspect="selectedMap?.width && selectedMap?.height ? selectedMap.width / selectedMap.height : null"
|
|
109
|
+
:points="selectedMap?.points ?? []"
|
|
110
|
+
:route-moving="statusMode === 'WORKING' && robotOnShownMap"
|
|
111
|
+
:spot="spot"
|
|
112
|
+
:pick="spotOn"
|
|
113
|
+
:tick-ms="tickMs"
|
|
114
|
+
@pick="(p) => spot.push(p)"
|
|
115
|
+
/>
|
|
116
|
+
<div v-if="spotOn" class="hero-spotbar">
|
|
117
|
+
<v-btn color="primary" variant="flat" :disabled="!spot.length || busy === 'spot'" :loading="busy === 'spot'" @click="sendSpot">
|
|
118
|
+
Send spot clean · {{ spot.length }} points
|
|
119
|
+
</v-btn>
|
|
120
|
+
<v-btn variant="outlined" @click="spot = []">Clear</v-btn>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|
|
123
|
+
<div v-else-if="caps" class="hero-sched__empty">{{ brand }} doesn't share where this robot is with us yet.</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div>
|
|
127
|
+
<div class="hero-section__title">{{ robotType === "security" ? "Patrol" : "Cleaning" }}</div>
|
|
69
128
|
<div class="hero-aside__head">
|
|
70
129
|
<span class="hero-aside__title">{{ robotType === "security" ? "Patrol schedule" : "Cleaning schedule" }}</span>
|
|
71
130
|
<span class="hero-aside__hint">{{
|
|
@@ -73,14 +132,9 @@
|
|
|
73
132
|
}}</span>
|
|
74
133
|
</div>
|
|
75
134
|
<template v-if="!schedule.length && robotType === 'security' && patrol?.scheduleShared === false">
|
|
76
|
-
<div class="hero-sched__empty">
|
|
135
|
+
<div class="hero-sched__empty">{{ scheduleElsewhere }}</div>
|
|
136
|
+
<!-- The runs themselves are in Patrol history below (said once). -->
|
|
77
137
|
<div v-if="runHint" class="hero-sched__hint">{{ runHint }} - a guess from recent runs, not the schedule.</div>
|
|
78
|
-
<template v-if="recentRuns.length">
|
|
79
|
-
<div class="hero-aside__hint">Recent patrol runs</div>
|
|
80
|
-
<div v-for="r in recentRuns" :key="r.key" class="hero-sched__row">
|
|
81
|
-
<b>{{ r.when }}</b><span>{{ r.route }} · {{ r.result }}</span><span>{{ r.detail }}</span>
|
|
82
|
-
</div>
|
|
83
|
-
</template>
|
|
84
138
|
</template>
|
|
85
139
|
<div v-else-if="!schedule.length" class="hero-sched__empty">No schedule yet.</div>
|
|
86
140
|
<div v-for="p in schedule" :key="p.key" class="hero-sched__row">
|
|
@@ -93,50 +147,29 @@
|
|
|
93
147
|
</div>
|
|
94
148
|
</template>
|
|
95
149
|
</div>
|
|
150
|
+
</div>
|
|
96
151
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
<option v-for="m in mapViews" :key="m.mapId" :value="m.mapId">{{ m.label }}{{ m.current ? " · Robot is here" : "" }}</option>
|
|
105
|
-
</select>
|
|
106
|
-
<span v-if="selectedMap?.current" class="hero-mapsel__here">Robot is here</span>
|
|
107
|
-
<span v-else-if="selectedMap" class="hero-mapsel__hint">{{ hereText }}</span>
|
|
108
|
-
<span v-if="selectedMap?.routeNames.length" class="hero-mapsel__hint">Routes: {{ selectedMap.routeNames.join(", ") }}</span>
|
|
109
|
-
</div>
|
|
110
|
-
<RobotLivePlan
|
|
111
|
-
:name="record?.name"
|
|
112
|
-
:status-label="statusLabel"
|
|
113
|
-
:point="posLive && robotOnShownMap ? point : null"
|
|
114
|
-
:stale="shown?.stale === true"
|
|
115
|
-
:heading="headingDeg(shown)"
|
|
116
|
-
:trail="trail"
|
|
117
|
-
:readout="readout"
|
|
118
|
-
:lost="posLost"
|
|
119
|
-
:route="route"
|
|
120
|
-
:routes="selectedMap ? mapRoutes : null"
|
|
121
|
-
:image="selectedMap?.imageUrl ?? null"
|
|
122
|
-
:aspect="selectedMap?.width && selectedMap?.height ? selectedMap.width / selectedMap.height : null"
|
|
123
|
-
:points="selectedMap?.points ?? []"
|
|
124
|
-
:route-moving="statusMode === 'WORKING' && robotOnShownMap"
|
|
125
|
-
:spot="spot"
|
|
126
|
-
:pick="spotOn"
|
|
127
|
-
:tick-ms="tickMs"
|
|
128
|
-
@pick="(p) => spot.push(p)"
|
|
129
|
-
/>
|
|
130
|
-
<div v-if="spotOn" class="hero-spotbar">
|
|
131
|
-
<v-btn color="primary" variant="flat" :disabled="!spot.length || busy === 'spot'" :loading="busy === 'spot'" @click="sendSpot">
|
|
132
|
-
Send spot clean · {{ spot.length }} points
|
|
133
|
-
</v-btn>
|
|
134
|
-
<v-btn variant="outlined" @click="spot = []">Clear</v-btn>
|
|
135
|
-
</div>
|
|
152
|
+
<RobotPatrolHistory v-if="robotType === 'security' && caps?.patrolHistory === true" :robot-id="robotId" />
|
|
153
|
+
|
|
154
|
+
<div v-if="caps?.liveVideo === true || canDrive">
|
|
155
|
+
<div class="hero-section__title">Camera</div>
|
|
156
|
+
<div v-if="online || canDrive" class="hero-detail__video">
|
|
157
|
+
<v-btn v-if="caps?.liveVideo === true && online" variant="outlined" @click="liveOpen = true">Live view</v-btn>
|
|
158
|
+
<v-btn v-if="canDrive" variant="outlined" @click="driveOpen = true">Manual control</v-btn>
|
|
136
159
|
</div>
|
|
160
|
+
<div v-else class="hero-notreported">Live view opens when the robot is online.</div>
|
|
137
161
|
</div>
|
|
138
162
|
|
|
139
|
-
<
|
|
163
|
+
<v-dialog v-model="confirmOpen" width="440">
|
|
164
|
+
<div v-if="confirming" class="hero-dialog" role="alertdialog" aria-labelledby="hero-confirm-title">
|
|
165
|
+
<div id="hero-confirm-title" class="hero-dialog__title">{{ confirming.text.title }}</div>
|
|
166
|
+
<div class="hero-dialog__body">{{ confirming.text.body }}</div>
|
|
167
|
+
<div class="hero-dialog__actions">
|
|
168
|
+
<v-btn variant="outlined" @click="confirmOpen = false">Don't send</v-btn>
|
|
169
|
+
<v-btn color="primary" variant="flat" @click="confirmSend">{{ confirming.text.action }}</v-btn>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</v-dialog>
|
|
140
173
|
|
|
141
174
|
<v-dialog v-model="editOpen" width="520" persistent>
|
|
142
175
|
<RobotForm
|
|
@@ -208,14 +241,13 @@ import {
|
|
|
208
241
|
capabilityLabel,
|
|
209
242
|
cleaningScheduleRows,
|
|
210
243
|
commandBody,
|
|
244
|
+
commandConfirm,
|
|
211
245
|
commandLabel,
|
|
212
246
|
faultBadge,
|
|
213
247
|
headingDeg,
|
|
214
248
|
isMoving,
|
|
215
249
|
mergePose,
|
|
216
|
-
notReportedLine,
|
|
217
250
|
optimisticMode,
|
|
218
|
-
patrolRecordRow,
|
|
219
251
|
patrolRunHint,
|
|
220
252
|
patrolScheduleRows,
|
|
221
253
|
pointCommandBody,
|
|
@@ -223,9 +255,11 @@ import {
|
|
|
223
255
|
unplacedStopsText,
|
|
224
256
|
planPoint,
|
|
225
257
|
positionText,
|
|
258
|
+
robotAbilities,
|
|
226
259
|
robotErrorMessage,
|
|
227
260
|
robotFacts,
|
|
228
261
|
robotStatus,
|
|
262
|
+
scheduleElsewhereText,
|
|
229
263
|
showSpotClean,
|
|
230
264
|
sinceText,
|
|
231
265
|
speedText,
|
|
@@ -327,8 +361,16 @@ const statusLabel = computed(() => robotStatus(statusMode.value, robotType.value
|
|
|
327
361
|
const fault = computed(() => (live.value?.ok ? live.value.fault ?? null : record.value?.fault ?? null));
|
|
328
362
|
const faultSince = computed(() => sinceText(fault.value?.since, now.value));
|
|
329
363
|
|
|
330
|
-
|
|
331
|
-
const
|
|
364
|
+
// Only what THIS robot reports (hide, never grey); what its maker keeps back is said once, in the card.
|
|
365
|
+
const facts = computed(() => robotFacts({ record: record.value, live: live.value as any, now: now.value, hideUnreported: true }));
|
|
366
|
+
const LOCATION = new Set(["Floor", "Live position"]);
|
|
367
|
+
const statusFacts = computed(() => facts.value.filter((f) => !LOCATION.has(f.label)));
|
|
368
|
+
// The plan's readout already shows the live position; the cell only when there is no plan.
|
|
369
|
+
const locationFacts = computed(() => facts.value.filter((f) => LOCATION.has(f.label) && !(f.label === "Live position" && showPlan.value)));
|
|
370
|
+
const abilities = computed(() =>
|
|
371
|
+
robotAbilities({ capabilities: caps.value, profile: live.value?.ok ? live.value.profile : null, type: robotType.value, brand: brand.value }),
|
|
372
|
+
);
|
|
373
|
+
const scheduleElsewhere = computed(() => scheduleElsewhereText(robotType.value, brand.value, live.value?.ok ? live.value.profile : null));
|
|
332
374
|
|
|
333
375
|
const online = computed(() => live.value?.ok === true && live.value.online === true);
|
|
334
376
|
const commands = computed(() =>
|
|
@@ -352,8 +394,15 @@ const readout = computed(() => `${positionText(live.value, caps.value, now.value
|
|
|
352
394
|
const spot = ref<Array<{ x: number; y: number }>>([]);
|
|
353
395
|
const spotOn = computed(() => showSpotClean({ canCommand: props.canCommand, capabilities: caps.value, online: online.value, type: robotType.value }));
|
|
354
396
|
|
|
355
|
-
|
|
356
|
-
const
|
|
397
|
+
// The route drawn on the plan: the shown map's first route, else the first with placed checkpoints (the rest are listed by name).
|
|
398
|
+
const route = computed<TPatrolRouteView | null>(() =>
|
|
399
|
+
selectedMap.value ? mapRoutes.value[0] ?? null : patrol.value?.routes?.find((r) => r.checkpoints?.length) ?? null,
|
|
400
|
+
);
|
|
401
|
+
const floorText = computed(() => locationFacts.value.find((f) => f.label === "Floor")?.value ?? "");
|
|
402
|
+
// The plan only for a robot that reports where it is, has maps to show, takes spot clicks, or walks a route we can draw.
|
|
403
|
+
const showPlan = computed(
|
|
404
|
+
() => !caps.value || reportsPosition.value || spotOn.value || mapViews.value.length > 0 || !!route.value?.checkpoints?.length,
|
|
405
|
+
);
|
|
357
406
|
const planTitle = computed(() => {
|
|
358
407
|
if (spotOn.value) return "Spot clean";
|
|
359
408
|
if (selectedMap.value && !selectedMap.value.current) return `Map · ${selectedMap.value.label}`;
|
|
@@ -364,7 +413,7 @@ const planHint = computed(() => {
|
|
|
364
413
|
if (noInterface.value) return "No position feed for this model";
|
|
365
414
|
if (selectedMap.value) return `${selectedMap.value.points.length} saved points · ${mapRoutes.value.length} routes`;
|
|
366
415
|
if (route.value) return `${route.value.checkpoints.length} checkpoints · ${floorText.value}`;
|
|
367
|
-
return
|
|
416
|
+
return ""; // the Floor cell above already says it
|
|
368
417
|
});
|
|
369
418
|
|
|
370
419
|
/* ---- schedule: cleaning plans from the brand, or the patrol contract ---- */
|
|
@@ -376,7 +425,6 @@ const unplacedRoutes = computed(() =>
|
|
|
376
425
|
);
|
|
377
426
|
// The brand keeps its schedules to itself: recent runs (and a labelled guess) instead.
|
|
378
427
|
const runs = ref<any[]>([]);
|
|
379
|
-
const recentRuns = computed(() => runs.value.slice(0, 5).map((r) => patrolRecordRow(r, now.value)));
|
|
380
428
|
const runHint = computed(() => patrolRunHint(runs.value, now.value));
|
|
381
429
|
|
|
382
430
|
async function loadSchedule() {
|
|
@@ -454,11 +502,13 @@ async function run(kind: TRobotCommandKind | "spot", body: Record<string, unknow
|
|
|
454
502
|
const before = statusMode.value;
|
|
455
503
|
busy.value = kind;
|
|
456
504
|
optimistic.value = { mode: optimisticMode(kind, before), after: Number.POSITIVE_INFINITY };
|
|
457
|
-
|
|
505
|
+
const label = kind === "full" && robotType.value === "security" ? "Start patrol" : commandLabel(kind);
|
|
506
|
+
toast.value = `${label} → ${name} · sending…`;
|
|
458
507
|
try {
|
|
459
508
|
await sendRobotCommand(props.robotId, body as any);
|
|
460
509
|
// A poll already in flight may still carry the old status; wait past it.
|
|
461
510
|
optimistic.value = optimistic.value ? { ...optimistic.value, after: Date.now() + 300 } : null;
|
|
511
|
+
toast.value = `${label} → ${name} · accepted. Its status updates here.`;
|
|
462
512
|
return true;
|
|
463
513
|
} catch (e) {
|
|
464
514
|
optimistic.value = null;
|
|
@@ -491,7 +541,16 @@ async function send(kind: TRobotCommandKind) {
|
|
|
491
541
|
toast.value = `${commandLabel(kind)}: the robot's position is not known yet.`;
|
|
492
542
|
return;
|
|
493
543
|
}
|
|
494
|
-
|
|
544
|
+
// Say what will happen before it is sent (the point picker is its own confirm).
|
|
545
|
+
confirming.value = { kind, body, text: commandConfirm(kind, { type: robotType.value, name: record.value?.name }) };
|
|
546
|
+
confirmOpen.value = true;
|
|
547
|
+
}
|
|
548
|
+
const confirmOpen = ref(false);
|
|
549
|
+
const confirming = ref<{ kind: TRobotCommandKind; body: Record<string, unknown>; text: { title: string; body: string; action: string } } | null>(null);
|
|
550
|
+
function confirmSend() {
|
|
551
|
+
const c = confirming.value;
|
|
552
|
+
confirmOpen.value = false;
|
|
553
|
+
if (c) run(c.kind, c.body);
|
|
495
554
|
}
|
|
496
555
|
|
|
497
556
|
/** The robot's plan image size for its floor, for turning plan clicks into pixels. */
|
|
@@ -37,9 +37,10 @@
|
|
|
37
37
|
@click:row="onRow"
|
|
38
38
|
>
|
|
39
39
|
<template #item.name="{ item }"><span class="hero-table__name">{{ item.name }}</span></template>
|
|
40
|
-
<template #item.brand="{ item }"><span class="hero-dim">{{
|
|
40
|
+
<template #item.brand="{ item }"><span class="hero-dim">{{ brandModelText(item.brand, brands, live(item)) }}</span></template>
|
|
41
41
|
<template #item.capability="{ item }">
|
|
42
|
-
|
|
42
|
+
<!-- One badge per row; the model's abilities in its tooltip (a mixed fleet without clutter). -->
|
|
43
|
+
<span v-if="capsOf(item)" class="hero-tag hero-tag--neutral hero-tag--xs" :title="abilityTip(item)">{{ capabilityLabel(capsOf(item)) }}</span>
|
|
43
44
|
</template>
|
|
44
45
|
<template #item.status="{ item }">
|
|
45
46
|
<RobotStatusChip v-if="statusOf(item).kind === 'live'" :mode="statusOf(item).mode" :type="item.type" />
|
|
@@ -92,8 +93,10 @@ import RobotMiniMap from "./MiniMap.vue";
|
|
|
92
93
|
import useRobotLive, { siteStatesFeed } from "../../composables/useRobotLive";
|
|
93
94
|
import {
|
|
94
95
|
agoText,
|
|
96
|
+
brandModelText,
|
|
95
97
|
brandName,
|
|
96
98
|
capabilityLabel,
|
|
99
|
+
robotAbilities,
|
|
97
100
|
listMeta,
|
|
98
101
|
robotErrorMessage,
|
|
99
102
|
rowStatus,
|
|
@@ -227,6 +230,14 @@ function capsOf(row: any) {
|
|
|
227
230
|
if (s && s.monitorable === false) return {};
|
|
228
231
|
return null;
|
|
229
232
|
}
|
|
233
|
+
/** "Does: … · Reports: …" for the capability badge's tooltip. */
|
|
234
|
+
function abilityTip(row: any) {
|
|
235
|
+
const s = live(row);
|
|
236
|
+
return robotAbilities({ capabilities: capsOf(row), profile: s?.ok ? s.profile : null, type: row.type, brand: brandName(row.brand, brands.value) })
|
|
237
|
+
.map((r) => `${r.label}: ${r.value}`)
|
|
238
|
+
.join("
|
|
239
|
+
");
|
|
240
|
+
}
|
|
230
241
|
/** The live fault when the robot answers, else its stored open fault. */
|
|
231
242
|
const faultOf = (row: any) => (live(row)?.ok ? live(row)?.fault ?? null : row.fault ?? null);
|
|
232
243
|
function seenText(row: any) {
|
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.56.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-display.ts
CHANGED
|
@@ -395,7 +395,7 @@ export type TRobotCommandKind =
|
|
|
395
395
|
const COMMANDS: ReadonlyArray<{ kind: TRobotCommandKind; label: string }> = [
|
|
396
396
|
{ kind: "full", label: "Clean all" },
|
|
397
397
|
{ kind: "area", label: "Clean area" },
|
|
398
|
-
{ kind: "return", label: "Return to
|
|
398
|
+
{ kind: "return", label: "Return to charger" },
|
|
399
399
|
{ kind: "pause", label: "Pause" },
|
|
400
400
|
{ kind: "resume", label: "Resume" },
|
|
401
401
|
{ kind: "cancel", label: "Cancel" },
|
|
@@ -610,11 +610,14 @@ export type TLiveRobotState = {
|
|
|
610
610
|
position?: { x: number; y: number; theta: number } | null;
|
|
611
611
|
/** Where it is on its own floor plan, 0-100 from the top-left; absent when the brand cannot say. */
|
|
612
612
|
planPosition?: { x: number; y: number } | null;
|
|
613
|
-
/** The brand's own status words ("CLEANING_AUTO"), quoted as "
|
|
613
|
+
/** The brand's own status words ("CLEANING_AUTO"), quoted as "Robot reports". */
|
|
614
614
|
statusName?: string;
|
|
615
615
|
model?: string;
|
|
616
616
|
fault?: { code: string; message?: string; since?: number; imageUrl?: string | null; thermalImageUrl?: string | null } | null;
|
|
617
|
+
/** Brand defaults + this MODEL's differences (core `robotManifest`): two models of one brand can differ. */
|
|
617
618
|
capabilities?: Record<string, unknown>;
|
|
619
|
+
/** Plain-words facts for this brand + model (core `robotManifest`). */
|
|
620
|
+
profile?: TRobotProfile;
|
|
618
621
|
lastSeenAt?: string | null;
|
|
619
622
|
/** Epoch ms of the last answer that was read (set by `mergeLiveStates`). */
|
|
620
623
|
readAt?: number;
|
|
@@ -1212,14 +1215,16 @@ export type TRobotFact = { label: string; value: string; dim: boolean };
|
|
|
1212
1215
|
|
|
1213
1216
|
/**
|
|
1214
1217
|
* The detail screen's facts grid (README §3): Connection · Battery · Floor ·
|
|
1215
|
-
* Live position ·
|
|
1218
|
+
* Live position · Robot reports · Model · Serial · Last seen. A fact the brand
|
|
1216
1219
|
* cannot report reads "Not reported" at 50%; a brand with no interface
|
|
1217
|
-
* reports nothing live at all.
|
|
1220
|
+
* reports nothing live at all. `hideUnreported` drops those cells instead
|
|
1221
|
+
* (owner 2026-09-26: show only what THIS robot supports - hide, never grey).
|
|
1218
1222
|
*/
|
|
1219
1223
|
export function robotFacts(input: {
|
|
1220
1224
|
record?: Record<string, any> | null;
|
|
1221
1225
|
live?: (TLiveRobotState & { statusName?: string; model?: string }) | null;
|
|
1222
1226
|
now?: number;
|
|
1227
|
+
hideUnreported?: boolean;
|
|
1223
1228
|
}): TRobotFact[] {
|
|
1224
1229
|
const live = input.live ?? null;
|
|
1225
1230
|
const record = input.record ?? {};
|
|
@@ -1239,16 +1244,19 @@ export function robotFacts(input: {
|
|
|
1239
1244
|
const position = !reports("readLocation")
|
|
1240
1245
|
? nr("Live position")
|
|
1241
1246
|
: fact("Live position", live ? positionText(live, caps ?? { readLocation: true }) : "");
|
|
1242
|
-
|
|
1247
|
+
const all = [
|
|
1243
1248
|
connection,
|
|
1244
1249
|
battery,
|
|
1245
1250
|
floor,
|
|
1246
1251
|
position,
|
|
1247
|
-
|
|
1248
|
-
fact("
|
|
1252
|
+
// A bare vendor code ("CLEANING_AUTO") is not words: the status chip already says it in ours.
|
|
1253
|
+
none ? nr("Robot reports") : fact("Robot reports", live?.ok && !/^[A-Z0-9_]+$/.test(live.statusName ?? "") ? live.statusName : ""),
|
|
1254
|
+
fact("Model", live?.ok ? live.profile?.model || live.model : ""),
|
|
1249
1255
|
fact("Serial", record.robotSn),
|
|
1250
1256
|
fact("Last seen", agoText(live?.lastSeenAt ?? record.lastSeenAt, input.now)),
|
|
1251
1257
|
];
|
|
1258
|
+
// Hidden: a fact the brand cannot report, and a model nobody reported.
|
|
1259
|
+
return input.hideUnreported ? all.filter((f) => !(f.dim && f.value === "Not reported") && !((f.label === "Model" || f.label === "Robot reports") && f.value === "—")) : all;
|
|
1252
1260
|
}
|
|
1253
1261
|
|
|
1254
1262
|
/**
|
|
@@ -1417,6 +1425,129 @@ export function capabilityNote(capabilities: Record<string, unknown> | null | un
|
|
|
1417
1425
|
return `${label} — no ${list}`;
|
|
1418
1426
|
}
|
|
1419
1427
|
|
|
1428
|
+
/** Plain-words facts for one brand + model, from core's manifest (`TFleetRobot.profile`). */
|
|
1429
|
+
export type TRobotProfile = {
|
|
1430
|
+
model?: string;
|
|
1431
|
+
modelKnown?: boolean;
|
|
1432
|
+
alarms?: string[];
|
|
1433
|
+
makerApp?: string;
|
|
1434
|
+
makerAppOnly?: string[];
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
export type TRobotAbilityRow = { label: string; value: string };
|
|
1438
|
+
|
|
1439
|
+
const listText = (items: string[]) =>
|
|
1440
|
+
items.length > 1 ? `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}` : items[0] ?? "";
|
|
1441
|
+
const firstUp = (t: string) => (t ? t.charAt(0).toUpperCase() + t.slice(1) : t);
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* "What this robot can do" (owner 2026-09-26): a few short rows in our words,
|
|
1445
|
+
* built only from THIS robot's manifest (brand + model), so two models read
|
|
1446
|
+
* differently. Rows with nothing to say are left out; [] while the robot has
|
|
1447
|
+
* not been read, or when it has no interface (the no-monitoring panel says so).
|
|
1448
|
+
*
|
|
1449
|
+
* - Does: what it does when told (only what its maker's API lets us send);
|
|
1450
|
+
* - Reports: what it tells us (battery, floor, position, heat, past patrols);
|
|
1451
|
+
* - Alarms: the alarm kinds it raises;
|
|
1452
|
+
* - In <app>: what only the maker's own app does (so nobody hunts for it here);
|
|
1453
|
+
* - Not shared yet: the facts (battery, floor, position) its maker does not give us.
|
|
1454
|
+
*/
|
|
1455
|
+
export function robotAbilities(input: {
|
|
1456
|
+
capabilities?: Record<string, unknown> | null;
|
|
1457
|
+
profile?: TRobotProfile | null;
|
|
1458
|
+
type?: unknown;
|
|
1459
|
+
brand?: unknown;
|
|
1460
|
+
} | null | undefined): TRobotAbilityRow[] {
|
|
1461
|
+
const caps = input?.capabilities;
|
|
1462
|
+
if (!caps || typeof caps !== "object" || capabilityLabel(caps) === "No interface") return [];
|
|
1463
|
+
const on = (k: string) => caps[k] === true;
|
|
1464
|
+
const patrols = input?.type === "security" && on("patrolControl");
|
|
1465
|
+
const cleans = input?.type !== "security";
|
|
1466
|
+
const does = [
|
|
1467
|
+
patrols && "patrols its routes (start, stop, resume)",
|
|
1468
|
+
cleans && on("startProcess") && "cleans a whole floor or chosen areas",
|
|
1469
|
+
cleans && on("spotClean") && "cleans spots you pick on the plan",
|
|
1470
|
+
on("pauseResume") && "pauses and resumes",
|
|
1471
|
+
!patrols && on("pauseProcess") && "stops a job",
|
|
1472
|
+
on("navigateToPoint") && "goes to a point you pick",
|
|
1473
|
+
on("relocalize") && "can be told where it is",
|
|
1474
|
+
on("sendToDock") && "returns to its charger",
|
|
1475
|
+
on("liveVideo") && "streams live video",
|
|
1476
|
+
on("manualDrive") && "can be driven by hand",
|
|
1477
|
+
].filter(Boolean) as string[];
|
|
1478
|
+
const reports = [
|
|
1479
|
+
on("battery") && "battery",
|
|
1480
|
+
on("floorReporting") && "floor",
|
|
1481
|
+
on("readLocation") && "live position",
|
|
1482
|
+
on("thermalCamera") && "heat readings",
|
|
1483
|
+
on("patrolHistory") && (on("thermalCamera") ? "past patrols with photos and temperatures" : "past patrols with photos"),
|
|
1484
|
+
].filter(Boolean) as string[];
|
|
1485
|
+
const p = input?.profile ?? {};
|
|
1486
|
+
const alarms = (Array.isArray(p.alarms) ? p.alarms : []).filter((a) => typeof a === "string" && a);
|
|
1487
|
+
const makerOnly = (Array.isArray(p.makerAppOnly) ? p.makerAppOnly : []).filter((a) => typeof a === "string" && a);
|
|
1488
|
+
const rows: TRobotAbilityRow[] = [];
|
|
1489
|
+
if (does.length) rows.push({ label: "Does", value: `${firstUp(listText(does))}.` });
|
|
1490
|
+
if (reports.length) rows.push({ label: "Reports", value: `${firstUp(listText(reports))}.` });
|
|
1491
|
+
if (alarms.length) rows.push({ label: "Alarms", value: `${firstUp(listText(alarms))}${on("resolveAlerts") ? " - mark them resolved here." : "."}` });
|
|
1492
|
+
if (makerOnly.length && p.makerApp) rows.push({ label: `In ${p.makerApp}`, value: `${firstUp(listText(makerOnly))}.` });
|
|
1493
|
+
// Facts only: an action it doesn't take is simply absent from "Does", never listed as missing.
|
|
1494
|
+
const missing = [!on("battery") && "battery", !on("floorReporting") && "floor", !on("readLocation") && "live position"].filter(Boolean) as string[];
|
|
1495
|
+
if (missing.length) rows.push({ label: "Not shared yet", value: `${firstUp(listText(missing))}.` });
|
|
1496
|
+
return rows;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* The empty schedule for a robot whose maker keeps schedules in its own app:
|
|
1501
|
+
* "Patrol schedules are set in the PadBot app. PadBot doesn't share them with us yet."
|
|
1502
|
+
*/
|
|
1503
|
+
export function scheduleElsewhereText(type: unknown, brand: unknown, profile?: TRobotProfile | null): string {
|
|
1504
|
+
const what = type === "security" ? "Patrol schedules" : "Cleaning schedules";
|
|
1505
|
+
const name = (typeof brand === "string" && brand.trim()) || "The maker";
|
|
1506
|
+
const app = profile?.makerApp || `${name}'s own app`;
|
|
1507
|
+
return `${what} are set in ${app}. ${name} doesn't share them with us yet.`;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* The confirm step for a command (owner 2026-09-26: say what will happen
|
|
1512
|
+
* before it is sent). `point` names the saved point for Go to point / Set position.
|
|
1513
|
+
*/
|
|
1514
|
+
export function commandConfirm(
|
|
1515
|
+
kind: unknown,
|
|
1516
|
+
input: { type?: unknown; name?: unknown; point?: unknown } = {},
|
|
1517
|
+
): { title: string; body: string; action: string } {
|
|
1518
|
+
const who = (typeof input.name === "string" && input.name) || "The robot";
|
|
1519
|
+
const at = typeof input.point === "string" && input.point ? input.point : "";
|
|
1520
|
+
const label = kind === "full" && input.type === "security" ? "Start patrol" : commandLabel(kind);
|
|
1521
|
+
const body: Record<string, string> = {
|
|
1522
|
+
full: input.type === "security" ? `${who} starts walking its default patrol route once.` : `${who} starts cleaning the whole floor it is on.`,
|
|
1523
|
+
area: `${who} starts cleaning the chosen areas.`,
|
|
1524
|
+
return: `${who} stops what it is doing and drives back to its charger.`,
|
|
1525
|
+
pause: `${who} stops where it is and waits. Press Resume to carry on.`,
|
|
1526
|
+
resume: `${who} carries on with the job it paused.`,
|
|
1527
|
+
cancel: `${who} stops the current job and stays where it is. It will not carry on by itself.`,
|
|
1528
|
+
navigate: at ? `${who} drives to ${at}.` : `${who} drives to the point you pick.`,
|
|
1529
|
+
relocalize: at
|
|
1530
|
+
? `${who} is told it is standing at ${at}. Only do this if it really is there - a wrong position gets it lost.`
|
|
1531
|
+
: `${who} is told where it is standing. Only do this if the position is right.`,
|
|
1532
|
+
"patrol-resume": `${who} carries on with the patrol that was interrupted.`,
|
|
1533
|
+
};
|
|
1534
|
+
return { title: `${label}?`, body: body[String(kind)] ?? `${who}: ${label}.`, action: label };
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/** The Brand column: "PadBot S2", "Demo S1 (simulated)", or just "Ounitech" when no model is reported. */
|
|
1538
|
+
export function brandModelText(
|
|
1539
|
+
brand: unknown,
|
|
1540
|
+
brands: Array<{ brand?: unknown; displayName?: unknown }> | null | undefined,
|
|
1541
|
+
live?: TLiveRobotState | null,
|
|
1542
|
+
): string {
|
|
1543
|
+
const name = brandName(brand, brands);
|
|
1544
|
+
const model = live?.ok ? (live.profile?.model || live.model || "").trim() : "";
|
|
1545
|
+
if (!model) return name;
|
|
1546
|
+
const word = name.split(" ")[0]!.toLowerCase();
|
|
1547
|
+
const simulated = /\(simulated\)/i.test(name) ? " (simulated)" : "";
|
|
1548
|
+
return (model.toLowerCase().startsWith(word) ? model : `${name.replace(/\s*\(.*\)$/, "")} ${model}`) + simulated;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1420
1551
|
/**
|
|
1421
1552
|
* The note under the credentials. The handoff says "stored encrypted"; they
|
|
1422
1553
|
* are not encrypted at rest today (see HERO-PENDING_INTEGRATIONS), so the note
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
{
|
|
2
|
+
"s2": {
|
|
3
|
+
"capabilities": {
|
|
4
|
+
"readLocation": true,
|
|
5
|
+
"navigateToPoint": true,
|
|
6
|
+
"battery": true,
|
|
7
|
+
"sendToDock": true,
|
|
8
|
+
"relocalize": true,
|
|
9
|
+
"startProcess": true,
|
|
10
|
+
"pauseProcess": true,
|
|
11
|
+
"processCompletion": true,
|
|
12
|
+
"floorReporting": true,
|
|
13
|
+
"patrolControl": true,
|
|
14
|
+
"liveVideo": true,
|
|
15
|
+
"patrolHistory": true,
|
|
16
|
+
"resolveAlerts": true,
|
|
17
|
+
"thermalCamera": true
|
|
18
|
+
},
|
|
19
|
+
"profile": {
|
|
20
|
+
"model": "PadBot S2",
|
|
21
|
+
"modelKnown": true,
|
|
22
|
+
"alarms": [
|
|
23
|
+
"person",
|
|
24
|
+
"high temperature",
|
|
25
|
+
"smoke",
|
|
26
|
+
"debris",
|
|
27
|
+
"noise",
|
|
28
|
+
"water on the floor",
|
|
29
|
+
"person falling",
|
|
30
|
+
"door open",
|
|
31
|
+
"window open",
|
|
32
|
+
"fire-fighting equipment"
|
|
33
|
+
],
|
|
34
|
+
"makerApp": "the PadBot app",
|
|
35
|
+
"makerAppOnly": [
|
|
36
|
+
"patrol schedules",
|
|
37
|
+
"maps, points and routes",
|
|
38
|
+
"alarm thresholds and AI recognition",
|
|
39
|
+
"lights and voice announcements",
|
|
40
|
+
"recorded video",
|
|
41
|
+
"live thermal view"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"padbot-other": {
|
|
46
|
+
"capabilities": {
|
|
47
|
+
"readLocation": true,
|
|
48
|
+
"navigateToPoint": true,
|
|
49
|
+
"battery": true,
|
|
50
|
+
"sendToDock": true,
|
|
51
|
+
"relocalize": true,
|
|
52
|
+
"startProcess": true,
|
|
53
|
+
"pauseProcess": true,
|
|
54
|
+
"processCompletion": true,
|
|
55
|
+
"floorReporting": true,
|
|
56
|
+
"patrolControl": true,
|
|
57
|
+
"liveVideo": true,
|
|
58
|
+
"patrolHistory": true,
|
|
59
|
+
"resolveAlerts": true
|
|
60
|
+
},
|
|
61
|
+
"profile": {
|
|
62
|
+
"model": "PadBot P9",
|
|
63
|
+
"modelKnown": false,
|
|
64
|
+
"alarms": [
|
|
65
|
+
"person",
|
|
66
|
+
"high temperature",
|
|
67
|
+
"smoke",
|
|
68
|
+
"debris",
|
|
69
|
+
"noise",
|
|
70
|
+
"water on the floor",
|
|
71
|
+
"person falling",
|
|
72
|
+
"door open",
|
|
73
|
+
"window open",
|
|
74
|
+
"fire-fighting equipment"
|
|
75
|
+
],
|
|
76
|
+
"makerApp": "the PadBot app",
|
|
77
|
+
"makerAppOnly": [
|
|
78
|
+
"patrol schedules",
|
|
79
|
+
"maps, points and routes",
|
|
80
|
+
"alarm thresholds and AI recognition",
|
|
81
|
+
"lights and voice announcements",
|
|
82
|
+
"recorded video"
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"ounitech": {
|
|
87
|
+
"capabilities": {
|
|
88
|
+
"readLocation": true,
|
|
89
|
+
"sendToDock": true,
|
|
90
|
+
"startProcess": true
|
|
91
|
+
},
|
|
92
|
+
"profile": {
|
|
93
|
+
"model": "",
|
|
94
|
+
"modelKnown": false,
|
|
95
|
+
"alarms": [],
|
|
96
|
+
"makerApp": "the Ounitech app",
|
|
97
|
+
"makerAppOnly": [
|
|
98
|
+
"pausing or stopping a job",
|
|
99
|
+
"cleaning across floors"
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"demo-s1": {
|
|
104
|
+
"capabilities": {
|
|
105
|
+
"readLocation": true,
|
|
106
|
+
"navigateToPoint": true,
|
|
107
|
+
"pauseResume": true,
|
|
108
|
+
"pathAndEta": true,
|
|
109
|
+
"battery": true,
|
|
110
|
+
"sendToDock": true,
|
|
111
|
+
"relocalize": true,
|
|
112
|
+
"startProcess": true,
|
|
113
|
+
"pauseProcess": true,
|
|
114
|
+
"processCompletion": true,
|
|
115
|
+
"floorReporting": true,
|
|
116
|
+
"multiFloor": true,
|
|
117
|
+
"spotClean": true
|
|
118
|
+
},
|
|
119
|
+
"profile": {
|
|
120
|
+
"model": "Demo S1",
|
|
121
|
+
"modelKnown": false,
|
|
122
|
+
"alarms": [],
|
|
123
|
+
"makerApp": "",
|
|
124
|
+
"makerAppOnly": []
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"demo-cx1": {
|
|
128
|
+
"capabilities": {
|
|
129
|
+
"readLocation": true,
|
|
130
|
+
"pauseResume": true,
|
|
131
|
+
"pathAndEta": true,
|
|
132
|
+
"battery": true,
|
|
133
|
+
"sendToDock": true,
|
|
134
|
+
"startProcess": true,
|
|
135
|
+
"pauseProcess": true,
|
|
136
|
+
"processCompletion": true,
|
|
137
|
+
"floorReporting": true,
|
|
138
|
+
"multiFloor": true
|
|
139
|
+
},
|
|
140
|
+
"profile": {
|
|
141
|
+
"model": "Demo CX-1",
|
|
142
|
+
"modelKnown": true,
|
|
143
|
+
"alarms": [],
|
|
144
|
+
"makerApp": "",
|
|
145
|
+
"makerAppOnly": []
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
"demo-sp1": {
|
|
149
|
+
"capabilities": {
|
|
150
|
+
"readLocation": true,
|
|
151
|
+
"navigateToPoint": true,
|
|
152
|
+
"pauseResume": true,
|
|
153
|
+
"pathAndEta": true,
|
|
154
|
+
"battery": true,
|
|
155
|
+
"sendToDock": true,
|
|
156
|
+
"relocalize": true,
|
|
157
|
+
"startProcess": true,
|
|
158
|
+
"pauseProcess": true,
|
|
159
|
+
"processCompletion": true,
|
|
160
|
+
"floorReporting": true,
|
|
161
|
+
"multiFloor": true
|
|
162
|
+
},
|
|
163
|
+
"profile": {
|
|
164
|
+
"model": "Demo SP-1",
|
|
165
|
+
"modelKnown": true,
|
|
166
|
+
"alarms": [],
|
|
167
|
+
"makerApp": "",
|
|
168
|
+
"makerAppOnly": []
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"unknown": {
|
|
172
|
+
"capabilities": {},
|
|
173
|
+
"profile": {
|
|
174
|
+
"model": "Z9",
|
|
175
|
+
"modelKnown": false,
|
|
176
|
+
"alarms": [],
|
|
177
|
+
"makerApp": "",
|
|
178
|
+
"makerAppOnly": []
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|