@7365admin1/layer-common 4.122.0 → 4.122.2
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 -4
- package/assets/css/robot-hero.css +3 -0
- package/components/Robot/LiftQueue.vue +18 -6
- package/components/Robot/MapEditor.vue +18 -0
- package/package.json +1 -1
- package/utils/robot-building.ts +20 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.122.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ad65b64: Map editor: Add floor beside the floor list adds a floor no robot reports yet (saved as an empty level), so a lift shaft can stop on two floors at a site whose robots are all on one floor. The one-floor lift message now says how.
|
|
8
|
+
|
|
9
|
+
## 4.122.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 811b0e5: Lift queue: after Connect gateway / New code, the card shows the ready-to-paste gateway settings (this app's address and the code) with Copy settings, Done, and three numbered setup steps; the Waiting line says what to check.
|
|
14
|
+
|
|
3
15
|
## 4.122.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -3228,11 +3240,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
|
|
|
3228
3240
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
3229
3241
|
own rule from two endpoints the console already calls, unprojected:
|
|
3230
3242
|
|
|
3231
|
-
|
|
3232
|
-
|
|
3243
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
3244
|
+
GET /api/roles/id/:role that membership's role document
|
|
3233
3245
|
|
|
3234
|
-
|
|
3235
|
-
|
|
3246
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
3247
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
3236
3248
|
|
|
3237
3249
|
`role.default` is the marker because it is the only property of a platform
|
|
3238
3250
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -1272,6 +1272,9 @@ div.hero-editor__wp.hero-editor__sel, div.hero-editor__fid.hero-editor__sel { ou
|
|
|
1272
1272
|
.hero-lift__gwreg { display: inline-block; margin: 4px 12px 0 0; font-family: monospace; }
|
|
1273
1273
|
.hero-lift__gwact { margin-left: auto; display: flex; gap: 6px; }
|
|
1274
1274
|
.hero-lift__gwcode { flex-basis: 100%; display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; padding: 8px 10px; border: 1px solid rgb(var(--v-theme-warning)); }
|
|
1275
|
+
.hero-lift__gwini { flex-basis: 100%; margin: 0; padding: 8px 10px; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; user-select: all; background: rgba(var(--v-theme-on-surface), 0.06); }
|
|
1276
|
+
.hero-lift__gwsteps { flex-basis: 100%; margin: 0; padding-left: 20px; font-size: 13px; }
|
|
1277
|
+
.hero-lift__gwsteps li { margin: 2px 0; }
|
|
1275
1278
|
.hero-lift__gwcode code { font-family: monospace; word-break: break-all; user-select: all; }
|
|
1276
1279
|
@media (max-width: 600px) {
|
|
1277
1280
|
.hero-lift { grid-template-columns: 1fr; }
|
|
@@ -57,10 +57,18 @@
|
|
|
57
57
|
<v-btn v-if="l.gateway" size="small" variant="text" :disabled="busy === `gw:${l.name}`" @click="disconnectGateway(l.name)">Disconnect</v-btn>
|
|
58
58
|
</span>
|
|
59
59
|
<div v-if="newCode?.lift === l.name" class="hero-lift__gwcode" role="alert">
|
|
60
|
-
<b>Gateway
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
|
|
60
|
+
<b>Gateway settings for {{ l.name }} - shown once, copy them now:</b>
|
|
61
|
+
<pre class="hero-lift__gwini">{{ gatewayIni }}</pre>
|
|
62
|
+
<span class="hero-lift__gwact">
|
|
63
|
+
<v-btn size="small" variant="tonal" @click="copyCode">Copy settings</v-btn>
|
|
64
|
+
<v-btn size="small" variant="text" @click="newCode = null">Done</v-btn>
|
|
65
|
+
</span>
|
|
66
|
+
<ol class="hero-lift__gwsteps">
|
|
67
|
+
<li>On the gateway (Raspberry Pi / PC), paste these two lines into the <code>[server]</code> section of <code>lift-gateway.ini</code>.</li>
|
|
68
|
+
<li>Check the link without the lift: <code>python3 lift_gateway.py -c lift-gateway.ini --simulate</code>. It prints "iService365 OK" and this card turns <b>Connected</b>.</li>
|
|
69
|
+
<li>Wire the lift board's MOD1 port, then run it without <code>--simulate</code>. Floor and door show once the lift's register list is in the <code>[decode]</code> section.</li>
|
|
70
|
+
</ol>
|
|
71
|
+
<span class="hero-muted">Lost the code? Press New code - the old one stops working.</span>
|
|
64
72
|
</div>
|
|
65
73
|
</div>
|
|
66
74
|
|
|
@@ -290,10 +298,14 @@ async function disconnectGateway(lift: string) {
|
|
|
290
298
|
busy.value = "";
|
|
291
299
|
}
|
|
292
300
|
}
|
|
301
|
+
/** The gateway's [server] lines: this app's own address (it forwards /api to iService365) and the code. */
|
|
302
|
+
const gatewayIni = computed(() =>
|
|
303
|
+
newCode.value ? `url = ${typeof window !== "undefined" ? window.location.origin : ""}\ncode = ${newCode.value.code}` : "",
|
|
304
|
+
);
|
|
293
305
|
async function copyCode() {
|
|
294
306
|
try {
|
|
295
|
-
await navigator.clipboard.writeText(
|
|
296
|
-
toast.value = "
|
|
307
|
+
await navigator.clipboard.writeText(gatewayIni.value);
|
|
308
|
+
toast.value = "Settings copied";
|
|
297
309
|
} catch {
|
|
298
310
|
toast.value = "Select the code and copy it by hand.";
|
|
299
311
|
}
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
<select v-model.number="floor" class="hero-input hero-select hero-editor__floor" aria-label="Floor">
|
|
40
40
|
<option v-for="f in allFloors" :key="f" :value="f">{{ floorName(f) }}</option>
|
|
41
41
|
</select>
|
|
42
|
+
<div class="hero-seg" role="group" aria-label="Floors">
|
|
43
|
+
<button type="button" class="hero-seg__opt" title="Add a floor no robot is on yet (e.g. a lift's other stops)" @click="addFloor">Add floor</button>
|
|
44
|
+
</div>
|
|
42
45
|
<label class="hero-editor__graphpick">
|
|
43
46
|
<span>Route graph</span>
|
|
44
47
|
<select v-model.number="graph" class="hero-input hero-select hero-editor__floor" aria-label="Route graph">
|
|
@@ -408,6 +411,7 @@ import {
|
|
|
408
411
|
LIFT_MIN_M,
|
|
409
412
|
NUDGE_BIG_M,
|
|
410
413
|
NUDGE_M,
|
|
414
|
+
newFloorNo,
|
|
411
415
|
ROUTE_TOOLS,
|
|
412
416
|
routeProblems,
|
|
413
417
|
saveErrorText,
|
|
@@ -1062,6 +1066,20 @@ function setLevel(key: "name" | "elevationM", value: string | number) {
|
|
|
1062
1066
|
levels.value = { ...levels.value, [String(floor.value)]: { ...cur, [key]: value } };
|
|
1063
1067
|
state.value = { ...state.value, dirty: true };
|
|
1064
1068
|
}
|
|
1069
|
+
/** A floor with nothing on it yet (no robot reports it): saved as an empty level so a lift can stop there. */
|
|
1070
|
+
function addFloor() {
|
|
1071
|
+
const answer = window.prompt("Floor number to add (1 = L1, 0 = G, -1 = B1)");
|
|
1072
|
+
if (answer === null) return;
|
|
1073
|
+
const r = newFloorNo(answer, allFloors.value);
|
|
1074
|
+
if ("error" in r) return void dispatch({ type: "notice", text: r.error, ok: false });
|
|
1075
|
+
const fl = r.floorNo;
|
|
1076
|
+
levels.value = { ...levels.value, [String(fl)]: { name: floorName(fl), elevationM: fl * 4, floorPolys: [], holePolys: [] } };
|
|
1077
|
+
// A selected lift stays selected (its Stops table now lists the floor); otherwise go to the new floor.
|
|
1078
|
+
const liftPicked = single.value?.kind === "lift";
|
|
1079
|
+
if (!liftPicked) floor.value = fl;
|
|
1080
|
+
state.value = { ...state.value, dirty: true };
|
|
1081
|
+
dispatch({ type: "notice", text: `${floorName(fl)} added.${liftPicked ? ` Tick Stops for ${floorName(fl)} below,` : " Select the lift shaft, tick Stops for it,"} then Save.`, ok: true });
|
|
1082
|
+
}
|
|
1065
1083
|
function alignNow() {
|
|
1066
1084
|
const r = alignFloor(state.value.shapes, state.value.frame, floor.value, alignTo.value);
|
|
1067
1085
|
if ("error" in r) return void dispatch({ type: "notice", text: r.error!, ok: false });
|
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.122.
|
|
5
|
+
"version": "4.122.2",
|
|
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-building.ts
CHANGED
|
@@ -1190,6 +1190,24 @@ export function editorReduce(state: TEditorState, action: TEditorAction): TEdito
|
|
|
1190
1190
|
}
|
|
1191
1191
|
}
|
|
1192
1192
|
|
|
1193
|
+
/** Floor numbers the API accepts (robot-building-map floorNo). */
|
|
1194
|
+
export const FLOOR_MIN = -20;
|
|
1195
|
+
export const FLOOR_MAX = 200;
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* The editor's "Add floor" answer, checked: a whole number (1 = L1, 0 = G,
|
|
1199
|
+
* -1 = B1) in the API's range and not already a floor - or the reason not.
|
|
1200
|
+
*/
|
|
1201
|
+
export function newFloorNo(input: string | null | undefined, existing: number[]): { floorNo: number } | { error: string } {
|
|
1202
|
+
const t = String(input ?? "").trim().toUpperCase();
|
|
1203
|
+
const m = /^(?:([LB])(\d+)|(-?\d+))$/.exec(t);
|
|
1204
|
+
const n = t === "G" ? 0 : m ? (m[3] !== undefined ? Number(m[3]) : m[1] === "B" ? -Number(m[2]) : Number(m[2])) : NaN;
|
|
1205
|
+
if (!Number.isInteger(n)) return { error: `"${String(input ?? "").trim()}" is not a floor. Type a whole number: 1 for L1, 0 for G, -1 for B1.` };
|
|
1206
|
+
if (n < FLOOR_MIN || n > FLOOR_MAX) return { error: `Floors go from ${floorName(FLOOR_MIN)} to ${floorName(FLOOR_MAX)}.` };
|
|
1207
|
+
if (existing.includes(n)) return { error: `${floorName(n)} is already a floor - pick it in the floor list.` };
|
|
1208
|
+
return { floorNo: n };
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1193
1211
|
/**
|
|
1194
1212
|
* Why Save must wait, in plain words - or null. The handoff's sentence for
|
|
1195
1213
|
* an unnamed zone; then what the API would refuse (a lift on one floor,
|
|
@@ -1205,7 +1223,7 @@ export function editorProblem(state: TEditorState): string | null {
|
|
|
1205
1223
|
const lonely = state.shapes.find((s) => s.kind === "lift" && (s.floors?.length ?? 0) < 2);
|
|
1206
1224
|
if (lonely) {
|
|
1207
1225
|
const a = shapeAnchor(lonely);
|
|
1208
|
-
return `Can't save yet: the lift shaft at (${a.x}, ${a.y}) serves only one floor. A lift needs at least two floors -
|
|
1226
|
+
return `Can't save yet: the lift shaft at (${a.x}, ${a.y}) serves only one floor. A lift needs at least two floors - select it and tick Stops for another floor (use Add floor beside the floor list if that floor is not there yet).`;
|
|
1209
1227
|
}
|
|
1210
1228
|
const long = state.shapes.find((s) => (s.kind === "zone" || s.kind === "waypoint" || s.kind === "fiducial") && (s.name ?? "").length > 60);
|
|
1211
1229
|
if (long) return `Can't save yet: the name "${long.name!.slice(0, 20)}…" is too long. Use 60 characters or fewer.`;
|
|
@@ -1495,7 +1513,7 @@ export function liftGatewayLine(g: Record<string, any> | null) {
|
|
|
1495
1513
|
waiting: {
|
|
1496
1514
|
text: `Waiting for the gateway (code …${g.codeHint ?? ""})`,
|
|
1497
1515
|
tone: "neutral",
|
|
1498
|
-
help: "
|
|
1516
|
+
help: "The gateway has not sent anything yet. Check its lift-gateway.ini (url and code from Connect gateway), its network, and that the program is running.",
|
|
1499
1517
|
},
|
|
1500
1518
|
connected: { text: `Connected · last reading ${ago}`, tone: "success", help: "" },
|
|
1501
1519
|
"no-lift": {
|