@7365admin1/core 3.85.0 → 3.86.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 +25 -0
- package/dist/index.d.ts +166 -87
- package/dist/index.js +478 -168
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +468 -165
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/demo-adapter.test.mjs +164 -0
- package/test/robot-adapter.test.mjs +83 -32
package/dist/index.mjs
CHANGED
|
@@ -19668,8 +19668,8 @@ function useDahuaService() {
|
|
|
19668
19668
|
}
|
|
19669
19669
|
}
|
|
19670
19670
|
}
|
|
19671
|
-
async function handleBuffer(
|
|
19672
|
-
partialBuffer = Buffer.concat([partialBuffer,
|
|
19671
|
+
async function handleBuffer(chunk2, onDetected2) {
|
|
19672
|
+
partialBuffer = Buffer.concat([partialBuffer, chunk2]);
|
|
19673
19673
|
while (true) {
|
|
19674
19674
|
const boundaryIndex = partialBuffer.indexOf(Buffer.from(boundary));
|
|
19675
19675
|
if (boundaryIndex === -1)
|
|
@@ -19920,12 +19920,12 @@ function useDahuaService() {
|
|
|
19920
19920
|
resolve("aborted");
|
|
19921
19921
|
};
|
|
19922
19922
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
19923
|
-
response.res.on("data", (
|
|
19923
|
+
response.res.on("data", (chunk2) => {
|
|
19924
19924
|
if (signal.aborted) {
|
|
19925
19925
|
response.res.destroy();
|
|
19926
19926
|
return;
|
|
19927
19927
|
}
|
|
19928
|
-
bufferQueue.enqueue(
|
|
19928
|
+
bufferQueue.enqueue(chunk2);
|
|
19929
19929
|
});
|
|
19930
19930
|
response.res.on("end", () => {
|
|
19931
19931
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -22529,10 +22529,10 @@ function buildAdapter(params) {
|
|
|
22529
22529
|
return;
|
|
22530
22530
|
let buffered = "";
|
|
22531
22531
|
const decoder = new TextDecoder();
|
|
22532
|
-
for await (const
|
|
22532
|
+
for await (const chunk2 of body) {
|
|
22533
22533
|
if (params2.signal.aborted)
|
|
22534
22534
|
return;
|
|
22535
|
-
buffered += decoder.decode(
|
|
22535
|
+
buffered += decoder.decode(chunk2, { stream: true });
|
|
22536
22536
|
let at2 = buffered.indexOf(marker);
|
|
22537
22537
|
while (at2 >= 0) {
|
|
22538
22538
|
const next = buffered.indexOf(marker, at2 + marker.length);
|
|
@@ -22844,16 +22844,16 @@ function useCameraViewService() {
|
|
|
22844
22844
|
() => finish(new Error("camera-timeout")),
|
|
22845
22845
|
CAMERA_RTSP_TIMEOUT_MS
|
|
22846
22846
|
);
|
|
22847
|
-
child.stdout.on("data", (
|
|
22848
|
-
bytes +=
|
|
22847
|
+
child.stdout.on("data", (chunk2) => {
|
|
22848
|
+
bytes += chunk2.length;
|
|
22849
22849
|
if (bytes > CAMERA_SNAPSHOT_MAX_BYTES) {
|
|
22850
22850
|
finish(new Error("camera-picture-too-large"));
|
|
22851
22851
|
return;
|
|
22852
22852
|
}
|
|
22853
|
-
chunks.push(
|
|
22853
|
+
chunks.push(chunk2);
|
|
22854
22854
|
});
|
|
22855
|
-
child.stderr?.on("data", (
|
|
22856
|
-
const text2 = carry +
|
|
22855
|
+
child.stderr?.on("data", (chunk2) => {
|
|
22856
|
+
const text2 = carry + chunk2.toString("latin1");
|
|
22857
22857
|
if (/401\s*unauthorized|\bunauthorized\b/i.test(text2))
|
|
22858
22858
|
rejected = true;
|
|
22859
22859
|
carry = text2.slice(-32);
|
|
@@ -32276,8 +32276,8 @@ function useOccurrenceBookRepo() {
|
|
|
32276
32276
|
const chunkSize = 500;
|
|
32277
32277
|
let totalInserted = 0;
|
|
32278
32278
|
for (let i = 0; i < payloads.length; i += chunkSize) {
|
|
32279
|
-
const
|
|
32280
|
-
const operations =
|
|
32279
|
+
const chunk2 = payloads.slice(i, i + chunkSize);
|
|
32280
|
+
const operations = chunk2.map((item) => ({
|
|
32281
32281
|
insertOne: {
|
|
32282
32282
|
document: {
|
|
32283
32283
|
...item,
|
|
@@ -51702,9 +51702,9 @@ function useVisitorTransactionService() {
|
|
|
51702
51702
|
expectedCheckIn
|
|
51703
51703
|
} = value;
|
|
51704
51704
|
for (let i = 0; i < value.members.length; i += chunkSize) {
|
|
51705
|
-
const
|
|
51705
|
+
const chunk2 = value.members.slice(i, i + chunkSize);
|
|
51706
51706
|
await Promise.all(
|
|
51707
|
-
|
|
51707
|
+
chunk2.map(async (member) => {
|
|
51708
51708
|
const clonedMember = structuredClone(member);
|
|
51709
51709
|
const { visitorPass, passKeys, nric: nric2, name: name2, contact: contact2 } = clonedMember;
|
|
51710
51710
|
if (/^(guest|walk-in|contractor|delivery)$/.test(type2 ?? "invalid") && site2) {
|
|
@@ -51946,9 +51946,9 @@ function useVisitorTransactionService() {
|
|
|
51946
51946
|
unitName
|
|
51947
51947
|
} = value;
|
|
51948
51948
|
for (let i = 0; i < value.members.length; i += chunkSize) {
|
|
51949
|
-
const
|
|
51949
|
+
const chunk2 = value.members.slice(i, i + chunkSize);
|
|
51950
51950
|
await Promise.all(
|
|
51951
|
-
|
|
51951
|
+
chunk2.map(async (member) => {
|
|
51952
51952
|
const clonedMember = structuredClone(member);
|
|
51953
51953
|
const { visitorPass, passKeys, nric: nric2, name: name2, contact: contact2 } = clonedMember;
|
|
51954
51954
|
if (/^(guest|walk-in|contractor|delivery)$/.test(type2 ?? "invalid") && site2) {
|
|
@@ -56409,8 +56409,8 @@ function useAccessManagementSvc() {
|
|
|
56409
56409
|
return new Promise((resolve, reject) => {
|
|
56410
56410
|
const fileStream = Readable.from(bufferFile);
|
|
56411
56411
|
let fileBuffer = Buffer.alloc(0);
|
|
56412
|
-
fileStream.on("data", (
|
|
56413
|
-
fileBuffer = Buffer.concat([fileBuffer,
|
|
56412
|
+
fileStream.on("data", (chunk2) => {
|
|
56413
|
+
fileBuffer = Buffer.concat([fileBuffer, chunk2]);
|
|
56414
56414
|
});
|
|
56415
56415
|
fileStream.on("end", () => {
|
|
56416
56416
|
try {
|
|
@@ -59266,10 +59266,10 @@ function parseRobotCommand(body) {
|
|
|
59266
59266
|
function createSseParser() {
|
|
59267
59267
|
let buffer = "";
|
|
59268
59268
|
let dataLines = [];
|
|
59269
|
-
function push(
|
|
59270
|
-
if (!
|
|
59269
|
+
function push(chunk2) {
|
|
59270
|
+
if (!chunk2)
|
|
59271
59271
|
return { events: [], sawActivity: false };
|
|
59272
|
-
buffer +=
|
|
59272
|
+
buffer += chunk2;
|
|
59273
59273
|
const events = [];
|
|
59274
59274
|
buffer = buffer.replace(/\r\n|\r/g, "\n");
|
|
59275
59275
|
const lines = buffer.split("\n");
|
|
@@ -59535,26 +59535,44 @@ function commandSupported(capabilities, kind) {
|
|
|
59535
59535
|
return false;
|
|
59536
59536
|
return supports(capabilities, needed);
|
|
59537
59537
|
}
|
|
59538
|
-
var
|
|
59539
|
-
function
|
|
59540
|
-
if (!
|
|
59541
|
-
throw new Error("A robot
|
|
59542
|
-
|
|
59538
|
+
var brands = /* @__PURE__ */ new Map();
|
|
59539
|
+
function registerRobotBrand(brand) {
|
|
59540
|
+
if (!brand?.brand)
|
|
59541
|
+
throw new Error("A robot brand must name itself.");
|
|
59542
|
+
if (typeof brand.create !== "function") {
|
|
59543
|
+
throw new Error(`Robot brand "${brand.brand}" has no way to build an adapter.`);
|
|
59544
|
+
}
|
|
59545
|
+
brands.set(brand.brand, brand);
|
|
59543
59546
|
}
|
|
59544
|
-
function
|
|
59547
|
+
function robotBrandFor(brand) {
|
|
59545
59548
|
if (typeof brand !== "string" || !brand)
|
|
59546
59549
|
return null;
|
|
59547
|
-
return
|
|
59550
|
+
return brands.get(brand) ?? null;
|
|
59551
|
+
}
|
|
59552
|
+
function adapterForRobot(robot) {
|
|
59553
|
+
const brandName = (robot?.brand ?? "").trim();
|
|
59554
|
+
const brand = robotBrandFor(brandName);
|
|
59555
|
+
if (!brand) {
|
|
59556
|
+
throw new Error(
|
|
59557
|
+
brandName ? `No robot adapter is registered for the brand "${brandName}".` : `Robot "${robot?.name ?? "unknown"}" does not say which brand it is.`
|
|
59558
|
+
);
|
|
59559
|
+
}
|
|
59560
|
+
const appid = (robot.appKey ?? "").trim();
|
|
59561
|
+
const secret = (robot.appToken ?? "").trim();
|
|
59562
|
+
if (!appid || !secret) {
|
|
59563
|
+
throw new Error(`Robot "${robot?.name ?? brandName}" has no cloud credentials saved.`);
|
|
59564
|
+
}
|
|
59565
|
+
return brand.create({ appid, secret });
|
|
59548
59566
|
}
|
|
59549
59567
|
function registeredRobotBrands() {
|
|
59550
|
-
return [...
|
|
59551
|
-
brand:
|
|
59552
|
-
displayName:
|
|
59553
|
-
tier: robotTier(
|
|
59568
|
+
return [...brands.values()].map((b) => ({
|
|
59569
|
+
brand: b.brand,
|
|
59570
|
+
displayName: b.displayName,
|
|
59571
|
+
tier: robotTier(b.capabilities)
|
|
59554
59572
|
}));
|
|
59555
59573
|
}
|
|
59556
|
-
function
|
|
59557
|
-
|
|
59574
|
+
function clearRobotBrands() {
|
|
59575
|
+
brands.clear();
|
|
59558
59576
|
}
|
|
59559
59577
|
|
|
59560
59578
|
// src/utils/lift-contract.util.ts
|
|
@@ -60010,122 +60028,6 @@ function useRobotCloudService(credentials, options = {}) {
|
|
|
60010
60028
|
};
|
|
60011
60029
|
}
|
|
60012
60030
|
|
|
60013
|
-
// src/services/robot-fleet.service.ts
|
|
60014
|
-
function credentialsOf(robot) {
|
|
60015
|
-
const appid = (robot.appKey ?? "").trim();
|
|
60016
|
-
const secret = (robot.appToken ?? "").trim();
|
|
60017
|
-
if (!appid || !secret) {
|
|
60018
|
-
throw new BadRequestError134(
|
|
60019
|
-
`Robot "${robot.name}" has no cloud credentials saved.`
|
|
60020
|
-
);
|
|
60021
|
-
}
|
|
60022
|
-
return { appid, secret };
|
|
60023
|
-
}
|
|
60024
|
-
function useRobotFleetService(deps) {
|
|
60025
|
-
const cloudFor = deps?.cloudFor ?? useRobotCloudService;
|
|
60026
|
-
function clientFor(robot) {
|
|
60027
|
-
return cloudFor(credentialsOf(robot));
|
|
60028
|
-
}
|
|
60029
|
-
async function resolveRobotSn(robot) {
|
|
60030
|
-
const stored = (robot.robotSn ?? "").trim();
|
|
60031
|
-
if (stored)
|
|
60032
|
-
return stored;
|
|
60033
|
-
const fleet = await clientFor(robot).getRobots();
|
|
60034
|
-
if (fleet.length === 0) {
|
|
60035
|
-
throw new NotFoundError43(
|
|
60036
|
-
`Robot "${robot.name}": the cloud reports no robots for these credentials.`
|
|
60037
|
-
);
|
|
60038
|
-
}
|
|
60039
|
-
if (fleet.length > 1) {
|
|
60040
|
-
throw new BadRequestError134(
|
|
60041
|
-
`Robot "${robot.name}": these credentials cover ${fleet.length} robots, so the serial cannot be determined automatically. Set it explicitly.`
|
|
60042
|
-
);
|
|
60043
|
-
}
|
|
60044
|
-
const robotSn = fleet[0].robotSn;
|
|
60045
|
-
if (!robotSn) {
|
|
60046
|
-
throw new BadRequestError134(
|
|
60047
|
-
`Robot "${robot.name}": the cloud returned a robot with no serial.`
|
|
60048
|
-
);
|
|
60049
|
-
}
|
|
60050
|
-
const id = robot._id ? String(robot._id) : "";
|
|
60051
|
-
if (id && deps?.saveRobotSn) {
|
|
60052
|
-
await deps.saveRobotSn(id, robotSn).catch(() => void 0);
|
|
60053
|
-
}
|
|
60054
|
-
return robotSn;
|
|
60055
|
-
}
|
|
60056
|
-
async function getFleetRobot(robot) {
|
|
60057
|
-
const robotSn = await resolveRobotSn(robot);
|
|
60058
|
-
const fleet = await clientFor(robot).getRobots();
|
|
60059
|
-
const match = fleet.find(
|
|
60060
|
-
(r) => r.robotSn === robotSn
|
|
60061
|
-
);
|
|
60062
|
-
if (!match) {
|
|
60063
|
-
return {
|
|
60064
|
-
id: robot._id ? String(robot._id) : "",
|
|
60065
|
-
name: robot.name,
|
|
60066
|
-
robotSn,
|
|
60067
|
-
statusFlag: null,
|
|
60068
|
-
statusName: "Not found in the robot cloud",
|
|
60069
|
-
mode: "UNAVAILABLE",
|
|
60070
|
-
online: false,
|
|
60071
|
-
dispatchable: false
|
|
60072
|
-
};
|
|
60073
|
-
}
|
|
60074
|
-
const mode = robotModeFromList(match);
|
|
60075
|
-
return {
|
|
60076
|
-
id: robot._id ? String(robot._id) : "",
|
|
60077
|
-
name: robot.name,
|
|
60078
|
-
robotSn,
|
|
60079
|
-
statusFlag: typeof match.statusFlag === "number" ? match.statusFlag : null,
|
|
60080
|
-
statusName: match.statusName ?? "",
|
|
60081
|
-
mode,
|
|
60082
|
-
online: match.onlineFlag !== false,
|
|
60083
|
-
dispatchable: mode === "IDLE" || mode === "CHARGED"
|
|
60084
|
-
};
|
|
60085
|
-
}
|
|
60086
|
-
async function getMaps(robot) {
|
|
60087
|
-
const robotSn = await resolveRobotSn(robot);
|
|
60088
|
-
return clientFor(robot).getMaps(robotSn);
|
|
60089
|
-
}
|
|
60090
|
-
async function getMapImages(robot, fileKeys) {
|
|
60091
|
-
const robotSn = await resolveRobotSn(robot);
|
|
60092
|
-
return clientFor(robot).getFileUrls(robotSn, fileKeys);
|
|
60093
|
-
}
|
|
60094
|
-
async function dispatch(robot, command) {
|
|
60095
|
-
const robotSn = await resolveRobotSn(robot);
|
|
60096
|
-
const cloud = clientFor(robot);
|
|
60097
|
-
if (command.kind === "return") {
|
|
60098
|
-
return cloud.returnToBase(robotSn);
|
|
60099
|
-
}
|
|
60100
|
-
const state = await getFleetRobot(robot);
|
|
60101
|
-
if (!state.dispatchable) {
|
|
60102
|
-
throw new BadRequestError134(
|
|
60103
|
-
`Robot "${robot.name}" is ${state.statusName || state.mode} and cannot take a new job.`
|
|
60104
|
-
);
|
|
60105
|
-
}
|
|
60106
|
-
switch (command.kind) {
|
|
60107
|
-
case "full":
|
|
60108
|
-
return cloud.startFullCleaning(robotSn, command.options);
|
|
60109
|
-
case "area":
|
|
60110
|
-
return cloud.startAreaCleaning(robotSn, command.areas, command.options);
|
|
60111
|
-
case "spot":
|
|
60112
|
-
return cloud.startSpotCleaning(robotSn, command.points, command.options);
|
|
60113
|
-
}
|
|
60114
|
-
}
|
|
60115
|
-
async function getPlans(robot) {
|
|
60116
|
-
const robotSn = await resolveRobotSn(robot);
|
|
60117
|
-
return clientFor(robot).getPlans(robotSn);
|
|
60118
|
-
}
|
|
60119
|
-
return {
|
|
60120
|
-
resolveRobotSn,
|
|
60121
|
-
getFleetRobot,
|
|
60122
|
-
getMaps,
|
|
60123
|
-
getMapImages,
|
|
60124
|
-
dispatch,
|
|
60125
|
-
getPlans
|
|
60126
|
-
};
|
|
60127
|
-
}
|
|
60128
|
-
|
|
60129
60031
|
// src/services/ounitech.adapter.ts
|
|
60130
60032
|
var OUNITECH_BRAND = "ounitech";
|
|
60131
60033
|
var OUNITECH_CAPABILITIES = {
|
|
@@ -60229,25 +60131,412 @@ function createOunitechAdapter(credentials, options) {
|
|
|
60229
60131
|
async getMaps(robotSn) {
|
|
60230
60132
|
return cloud.getMaps(robotSn);
|
|
60231
60133
|
},
|
|
60134
|
+
async getMapImages(robotSn, fileKeys) {
|
|
60135
|
+
return cloud.getFileUrls(robotSn, fileKeys);
|
|
60136
|
+
},
|
|
60232
60137
|
async getPlans(robotSn) {
|
|
60233
60138
|
return cloud.getPlans(robotSn);
|
|
60234
60139
|
}
|
|
60235
60140
|
};
|
|
60236
60141
|
}
|
|
60237
60142
|
function registerOunitechBrand() {
|
|
60238
|
-
|
|
60239
|
-
throw new Error(
|
|
60240
|
-
"No Ounitech credentials for this robot. Add the app key and token on the robot's record."
|
|
60241
|
-
);
|
|
60242
|
-
};
|
|
60243
|
-
registerRobotAdapter({
|
|
60143
|
+
registerRobotBrand({
|
|
60244
60144
|
brand: OUNITECH_BRAND,
|
|
60245
60145
|
displayName: "Ounitech (Q3-G / Q3-W)",
|
|
60246
60146
|
capabilities: OUNITECH_CAPABILITIES,
|
|
60247
|
-
|
|
60248
|
-
getRobot: async () => unconfigured()
|
|
60147
|
+
create: (credentials) => createOunitechAdapter(credentials)
|
|
60249
60148
|
});
|
|
60250
60149
|
}
|
|
60150
|
+
registerOunitechBrand();
|
|
60151
|
+
|
|
60152
|
+
// src/utils/png-encoder.util.ts
|
|
60153
|
+
import { deflateSync } from "zlib";
|
|
60154
|
+
function crc32(buffer) {
|
|
60155
|
+
let crc = ~0;
|
|
60156
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
60157
|
+
crc ^= buffer[i];
|
|
60158
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
60159
|
+
crc = crc & 1 ? crc >>> 1 ^ 3988292384 : crc >>> 1;
|
|
60160
|
+
}
|
|
60161
|
+
}
|
|
60162
|
+
return ~crc >>> 0;
|
|
60163
|
+
}
|
|
60164
|
+
function chunk(type, data) {
|
|
60165
|
+
const length = Buffer.alloc(4);
|
|
60166
|
+
length.writeUInt32BE(data.length, 0);
|
|
60167
|
+
const typed = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
60168
|
+
const crc = Buffer.alloc(4);
|
|
60169
|
+
crc.writeUInt32BE(crc32(typed), 0);
|
|
60170
|
+
return Buffer.concat([length, typed, crc]);
|
|
60171
|
+
}
|
|
60172
|
+
function encodeGreyscalePng(pixels, width, height) {
|
|
60173
|
+
if (width <= 0 || height <= 0)
|
|
60174
|
+
throw new Error("A PNG needs a positive size.");
|
|
60175
|
+
if (pixels.length < width * height) {
|
|
60176
|
+
throw new Error("A PNG needs one byte per pixel.");
|
|
60177
|
+
}
|
|
60178
|
+
const raw = Buffer.alloc((width + 1) * height);
|
|
60179
|
+
for (let y = 0; y < height; y++) {
|
|
60180
|
+
raw[y * (width + 1)] = 0;
|
|
60181
|
+
for (let x = 0; x < width; x++) {
|
|
60182
|
+
raw[y * (width + 1) + 1 + x] = pixels[y * width + x];
|
|
60183
|
+
}
|
|
60184
|
+
}
|
|
60185
|
+
const ihdr = Buffer.alloc(13);
|
|
60186
|
+
ihdr.writeUInt32BE(width, 0);
|
|
60187
|
+
ihdr.writeUInt32BE(height, 4);
|
|
60188
|
+
ihdr[8] = 8;
|
|
60189
|
+
ihdr[9] = 0;
|
|
60190
|
+
ihdr[10] = 0;
|
|
60191
|
+
ihdr[11] = 0;
|
|
60192
|
+
ihdr[12] = 0;
|
|
60193
|
+
return Buffer.concat([
|
|
60194
|
+
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
60195
|
+
chunk("IHDR", ihdr),
|
|
60196
|
+
chunk("IDAT", deflateSync(raw)),
|
|
60197
|
+
chunk("IEND", Buffer.alloc(0))
|
|
60198
|
+
]);
|
|
60199
|
+
}
|
|
60200
|
+
function greyscalePngDataUri(pixels, width, height) {
|
|
60201
|
+
return `data:image/png;base64,${encodeGreyscalePng(pixels, width, height).toString("base64")}`;
|
|
60202
|
+
}
|
|
60203
|
+
|
|
60204
|
+
// src/services/demo.adapter.ts
|
|
60205
|
+
var DEMO_BRAND = "demo";
|
|
60206
|
+
var DEMO_CAPABILITIES = {
|
|
60207
|
+
readLocation: true,
|
|
60208
|
+
navigateToPoint: true,
|
|
60209
|
+
pauseResume: true,
|
|
60210
|
+
pathAndEta: true,
|
|
60211
|
+
battery: true,
|
|
60212
|
+
sendToDock: true,
|
|
60213
|
+
relocalize: true,
|
|
60214
|
+
startProcess: true,
|
|
60215
|
+
pauseProcess: true,
|
|
60216
|
+
processCompletion: true,
|
|
60217
|
+
floorReporting: true,
|
|
60218
|
+
multiFloor: true
|
|
60219
|
+
};
|
|
60220
|
+
var SCALE = 0.05;
|
|
60221
|
+
var PLAN_W = 260;
|
|
60222
|
+
var PLAN_H = 180;
|
|
60223
|
+
var DEMO_ROBOTS = [
|
|
60224
|
+
{ robotSn: "DEMO-0001", name: "Demo Cleaner 01", model: "Demo S1", floorNo: 1, phase: 0 },
|
|
60225
|
+
{ robotSn: "DEMO-0002", name: "Demo Cleaner 02", model: "Demo S1", floorNo: 1, phase: 0.37 },
|
|
60226
|
+
{ robotSn: "DEMO-0003", name: "Demo Scrubber 03", model: "Demo X2", floorNo: 2, phase: 0.68 }
|
|
60227
|
+
];
|
|
60228
|
+
var commanded = /* @__PURE__ */ new Map();
|
|
60229
|
+
function demoPlanPixels(floorNo) {
|
|
60230
|
+
const px = new Uint8Array(PLAN_W * PLAN_H).fill(255);
|
|
60231
|
+
const set = (x, y) => {
|
|
60232
|
+
if (x >= 0 && x < PLAN_W && y >= 0 && y < PLAN_H)
|
|
60233
|
+
px[y * PLAN_W + x] = 0;
|
|
60234
|
+
};
|
|
60235
|
+
for (let x = 0; x < PLAN_W; x++) {
|
|
60236
|
+
for (const t of [0, 1]) {
|
|
60237
|
+
set(x, t);
|
|
60238
|
+
set(x, PLAN_H - 1 - t);
|
|
60239
|
+
}
|
|
60240
|
+
}
|
|
60241
|
+
for (let y = 0; y < PLAN_H; y++) {
|
|
60242
|
+
for (const t of [0, 1]) {
|
|
60243
|
+
set(t, y);
|
|
60244
|
+
set(PLAN_W - 1 - t, y);
|
|
60245
|
+
}
|
|
60246
|
+
}
|
|
60247
|
+
const wallX = Math.floor(PLAN_W * (floorNo === 1 ? 0.45 : 0.6));
|
|
60248
|
+
for (let y = 2; y < PLAN_H - 2; y++) {
|
|
60249
|
+
if (y > PLAN_H * 0.45 && y < PLAN_H * 0.6)
|
|
60250
|
+
continue;
|
|
60251
|
+
set(wallX, y);
|
|
60252
|
+
set(wallX + 1, y);
|
|
60253
|
+
}
|
|
60254
|
+
if (floorNo === 2) {
|
|
60255
|
+
const wallY = Math.floor(PLAN_H * 0.5);
|
|
60256
|
+
for (let x = 2; x < wallX; x++) {
|
|
60257
|
+
if (x > PLAN_W * 0.15 && x < PLAN_W * 0.25)
|
|
60258
|
+
continue;
|
|
60259
|
+
set(x, wallY);
|
|
60260
|
+
set(x, wallY + 1);
|
|
60261
|
+
}
|
|
60262
|
+
}
|
|
60263
|
+
return px;
|
|
60264
|
+
}
|
|
60265
|
+
function demoPosition(robot, now) {
|
|
60266
|
+
const t = (now / 9e4 + robot.phase) % 1;
|
|
60267
|
+
const angle = t * Math.PI * 2;
|
|
60268
|
+
const cx = PLAN_W * 0.5 * SCALE;
|
|
60269
|
+
const cy = PLAN_H * 0.5 * SCALE;
|
|
60270
|
+
const rx = PLAN_W * 0.28 * SCALE;
|
|
60271
|
+
const ry = PLAN_H * 0.28 * SCALE;
|
|
60272
|
+
return {
|
|
60273
|
+
x: cx + Math.cos(angle) * rx,
|
|
60274
|
+
y: cy + Math.sin(angle) * ry,
|
|
60275
|
+
// Facing along the direction of travel, which is the tangent.
|
|
60276
|
+
theta: angle + Math.PI / 2
|
|
60277
|
+
};
|
|
60278
|
+
}
|
|
60279
|
+
function demoBattery(robot, now) {
|
|
60280
|
+
const t = (now / 36e5 + robot.phase) % 1;
|
|
60281
|
+
return Math.round(t < 0.8 ? 100 - t / 0.8 * 75 : 25 + (t - 0.8) / 0.2 * 75);
|
|
60282
|
+
}
|
|
60283
|
+
function demoState(robot, now) {
|
|
60284
|
+
const order = commanded.get(robot.robotSn);
|
|
60285
|
+
const active = order && order.until > now ? order.mode : null;
|
|
60286
|
+
const battery = demoBattery(robot, now);
|
|
60287
|
+
const mode = battery <= 25 ? "CHARGING_CYCLE" : active ?? "IDLE";
|
|
60288
|
+
return {
|
|
60289
|
+
robotSn: robot.robotSn,
|
|
60290
|
+
name: robot.name,
|
|
60291
|
+
model: robot.model,
|
|
60292
|
+
online: true,
|
|
60293
|
+
mode,
|
|
60294
|
+
statusName: mode === "WORKING" ? "Cleaning" : mode === "CHARGING_CYCLE" ? "Charging" : "Idle",
|
|
60295
|
+
batteryPercent: battery,
|
|
60296
|
+
floorNo: robot.floorNo,
|
|
60297
|
+
// The demo DOES know its floor - unlike Ounitech today - so the map can
|
|
60298
|
+
// draw it. That contrast is part of what the demo shows.
|
|
60299
|
+
floorKnown: true,
|
|
60300
|
+
position: demoPosition(robot, now)
|
|
60301
|
+
};
|
|
60302
|
+
}
|
|
60303
|
+
function createDemoAdapter(options) {
|
|
60304
|
+
const now = options?.now ?? Date.now;
|
|
60305
|
+
return {
|
|
60306
|
+
brand: DEMO_BRAND,
|
|
60307
|
+
displayName: "Demo robots (simulated)",
|
|
60308
|
+
capabilities: DEMO_CAPABILITIES,
|
|
60309
|
+
async listRobots() {
|
|
60310
|
+
const at2 = now();
|
|
60311
|
+
return DEMO_ROBOTS.map((r) => demoState(r, at2));
|
|
60312
|
+
},
|
|
60313
|
+
async getRobot(robotSn) {
|
|
60314
|
+
const robot = DEMO_ROBOTS.find((r) => r.robotSn === robotSn);
|
|
60315
|
+
return robot ? demoState(robot, now()) : null;
|
|
60316
|
+
},
|
|
60317
|
+
async sendCommand(robotSn, command) {
|
|
60318
|
+
const robot = DEMO_ROBOTS.find((r) => r.robotSn === robotSn);
|
|
60319
|
+
if (!robot)
|
|
60320
|
+
throw new Error(`No demo robot with serial ${robotSn}.`);
|
|
60321
|
+
const at2 = now();
|
|
60322
|
+
switch (command.kind) {
|
|
60323
|
+
case "full":
|
|
60324
|
+
case "area":
|
|
60325
|
+
case "spot":
|
|
60326
|
+
commanded.set(robotSn, { mode: "WORKING", until: at2 + 10 * 6e4 });
|
|
60327
|
+
return;
|
|
60328
|
+
case "return":
|
|
60329
|
+
commanded.set(robotSn, { mode: "CHARGING_CYCLE", until: at2 + 5 * 6e4 });
|
|
60330
|
+
return;
|
|
60331
|
+
case "pause":
|
|
60332
|
+
commanded.set(robotSn, { mode: "PAUSED", until: at2 + 10 * 6e4 });
|
|
60333
|
+
return;
|
|
60334
|
+
case "resume":
|
|
60335
|
+
commanded.set(robotSn, { mode: "WORKING", until: at2 + 10 * 6e4 });
|
|
60336
|
+
return;
|
|
60337
|
+
case "cancel":
|
|
60338
|
+
commanded.delete(robotSn);
|
|
60339
|
+
return;
|
|
60340
|
+
case "navigate":
|
|
60341
|
+
case "relocalize":
|
|
60342
|
+
commanded.set(robotSn, { mode: "WORKING", until: at2 + 6e4 });
|
|
60343
|
+
return;
|
|
60344
|
+
}
|
|
60345
|
+
},
|
|
60346
|
+
async getMaps() {
|
|
60347
|
+
return DEMO_ROBOTS.map((r) => r.floorNo).filter((floorNo, i, all) => all.indexOf(floorNo) === i).sort((a, b) => a - b).map((floorNo) => ({
|
|
60348
|
+
floorNo,
|
|
60349
|
+
mapName: `Demo Building L${floorNo}`,
|
|
60350
|
+
backgroundFileKey: `demo/L${floorNo}.png`,
|
|
60351
|
+
partitionFileKey: `demo/L${floorNo}.png`,
|
|
60352
|
+
originX: 0,
|
|
60353
|
+
originY: 0,
|
|
60354
|
+
resolution: SCALE,
|
|
60355
|
+
totalArea: Math.round(PLAN_W * PLAN_H * SCALE * SCALE),
|
|
60356
|
+
baseX: 12,
|
|
60357
|
+
baseY: 3,
|
|
60358
|
+
baseTheta: 0,
|
|
60359
|
+
createTime: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
60360
|
+
areaList: [
|
|
60361
|
+
{ areaName: `L${floorNo}-LOBBY`, rgba: "FF0B0B83", x: 60, y: 60 },
|
|
60362
|
+
{ areaName: `L${floorNo}-OFFICE`, rgba: "B7CAEFFF", x: 190, y: 60 },
|
|
60363
|
+
{ areaName: `L${floorNo}-CORRIDOR`, rgba: "BA144BFF", x: 120, y: 140 }
|
|
60364
|
+
]
|
|
60365
|
+
}));
|
|
60366
|
+
},
|
|
60367
|
+
/**
|
|
60368
|
+
* Data URIs rather than signed links.
|
|
60369
|
+
*
|
|
60370
|
+
* A demo must work with no storage account and no network, and a `data:`
|
|
60371
|
+
* URI is same-origin so the canvas is not tainted — which matters, because
|
|
60372
|
+
* the layout is built by reading these pixels back.
|
|
60373
|
+
*/
|
|
60374
|
+
async getMapImages(_robotSn, fileKeys) {
|
|
60375
|
+
return fileKeys.map((fileKey) => {
|
|
60376
|
+
const floorNo = fileKey.includes("L2") ? 2 : 1;
|
|
60377
|
+
return {
|
|
60378
|
+
fileKey,
|
|
60379
|
+
url: greyscalePngDataUri(demoPlanPixels(floorNo), PLAN_W, PLAN_H)
|
|
60380
|
+
};
|
|
60381
|
+
});
|
|
60382
|
+
},
|
|
60383
|
+
async getPlans() {
|
|
60384
|
+
return [
|
|
60385
|
+
{
|
|
60386
|
+
planId: 1,
|
|
60387
|
+
dateList: [1, 2, 3, 4, 5],
|
|
60388
|
+
startTime: "08:00",
|
|
60389
|
+
endTime: "10:00",
|
|
60390
|
+
enableFlag: true,
|
|
60391
|
+
jobList: [
|
|
60392
|
+
{
|
|
60393
|
+
floorNo: 1,
|
|
60394
|
+
rangeFlag: 3,
|
|
60395
|
+
cleanTimes: 1,
|
|
60396
|
+
pointList: [],
|
|
60397
|
+
colorList: [{ r: 255, g: 11, b: 11, a: 131 }],
|
|
60398
|
+
modeFlag: 1,
|
|
60399
|
+
subModeFlag: 0,
|
|
60400
|
+
levelFlag: 1
|
|
60401
|
+
}
|
|
60402
|
+
]
|
|
60403
|
+
}
|
|
60404
|
+
];
|
|
60405
|
+
}
|
|
60406
|
+
};
|
|
60407
|
+
}
|
|
60408
|
+
function registerDemoBrand() {
|
|
60409
|
+
registerRobotBrand({
|
|
60410
|
+
brand: DEMO_BRAND,
|
|
60411
|
+
displayName: "Demo robots (simulated)",
|
|
60412
|
+
capabilities: DEMO_CAPABILITIES,
|
|
60413
|
+
create: () => createDemoAdapter()
|
|
60414
|
+
});
|
|
60415
|
+
}
|
|
60416
|
+
registerDemoBrand();
|
|
60417
|
+
|
|
60418
|
+
// src/services/robot-fleet.service.ts
|
|
60419
|
+
function useRobotFleetService(deps) {
|
|
60420
|
+
const adapterFor = deps?.adapterFor ?? ((robot) => adapterForRobot(robot));
|
|
60421
|
+
function capabilitiesOf(robot) {
|
|
60422
|
+
return robotBrandFor(robot.brand ?? "")?.capabilities ?? {};
|
|
60423
|
+
}
|
|
60424
|
+
async function resolveRobotSn(robot) {
|
|
60425
|
+
const stored = (robot.robotSn ?? "").trim();
|
|
60426
|
+
if (stored)
|
|
60427
|
+
return stored;
|
|
60428
|
+
const fleet = await adapterFor(robot).listRobots();
|
|
60429
|
+
if (fleet.length === 0) {
|
|
60430
|
+
throw new NotFoundError43(
|
|
60431
|
+
`Robot "${robot.name}": the robot cloud reports no robots for these credentials.`
|
|
60432
|
+
);
|
|
60433
|
+
}
|
|
60434
|
+
if (fleet.length > 1) {
|
|
60435
|
+
throw new BadRequestError134(
|
|
60436
|
+
`Robot "${robot.name}": these credentials cover ${fleet.length} robots, so the serial cannot be determined automatically. Set it explicitly.`
|
|
60437
|
+
);
|
|
60438
|
+
}
|
|
60439
|
+
const robotSn = fleet[0].robotSn;
|
|
60440
|
+
if (!robotSn) {
|
|
60441
|
+
throw new BadRequestError134(`Robot "${robot.name}": the cloud returned a robot with no serial.`);
|
|
60442
|
+
}
|
|
60443
|
+
const id = robot._id ? String(robot._id) : "";
|
|
60444
|
+
if (id && deps?.saveRobotSn) {
|
|
60445
|
+
await deps.saveRobotSn(id, robotSn).catch(() => void 0);
|
|
60446
|
+
}
|
|
60447
|
+
return robotSn;
|
|
60448
|
+
}
|
|
60449
|
+
async function getFleetRobot(robot) {
|
|
60450
|
+
const adapter = adapterFor(robot);
|
|
60451
|
+
const robotSn = await resolveRobotSn(robot);
|
|
60452
|
+
const found = await adapter.getRobot(robotSn);
|
|
60453
|
+
const id = robot._id ? String(robot._id) : "";
|
|
60454
|
+
const brand = adapter.brand;
|
|
60455
|
+
if (!found) {
|
|
60456
|
+
return {
|
|
60457
|
+
id,
|
|
60458
|
+
name: robot.name,
|
|
60459
|
+
brand,
|
|
60460
|
+
robotSn,
|
|
60461
|
+
statusName: "Not found in the robot cloud",
|
|
60462
|
+
mode: "UNAVAILABLE",
|
|
60463
|
+
online: false,
|
|
60464
|
+
dispatchable: false,
|
|
60465
|
+
batteryPercent: null,
|
|
60466
|
+
floorNo: null,
|
|
60467
|
+
floorKnown: false,
|
|
60468
|
+
position: null
|
|
60469
|
+
};
|
|
60470
|
+
}
|
|
60471
|
+
const free = found.mode === "IDLE" || found.mode === "CHARGED";
|
|
60472
|
+
return {
|
|
60473
|
+
id,
|
|
60474
|
+
name: robot.name,
|
|
60475
|
+
brand,
|
|
60476
|
+
robotSn,
|
|
60477
|
+
statusName: found.statusName ?? "",
|
|
60478
|
+
mode: found.mode,
|
|
60479
|
+
online: found.online,
|
|
60480
|
+
dispatchable: found.online && free && commandSupported(adapter.capabilities, "full"),
|
|
60481
|
+
batteryPercent: found.batteryPercent ?? null,
|
|
60482
|
+
floorNo: found.floorNo ?? null,
|
|
60483
|
+
floorKnown: found.floorKnown === true,
|
|
60484
|
+
position: found.position ?? null
|
|
60485
|
+
};
|
|
60486
|
+
}
|
|
60487
|
+
async function getMaps(robot) {
|
|
60488
|
+
const adapter = adapterFor(robot);
|
|
60489
|
+
if (!adapter.getMaps)
|
|
60490
|
+
return [];
|
|
60491
|
+
return adapter.getMaps(await resolveRobotSn(robot));
|
|
60492
|
+
}
|
|
60493
|
+
async function getMapImages(robot, fileKeys) {
|
|
60494
|
+
const adapter = adapterFor(robot);
|
|
60495
|
+
if (!adapter.getMapImages || !fileKeys.length)
|
|
60496
|
+
return [];
|
|
60497
|
+
return adapter.getMapImages(await resolveRobotSn(robot), fileKeys);
|
|
60498
|
+
}
|
|
60499
|
+
async function getPlans(robot) {
|
|
60500
|
+
const adapter = adapterFor(robot);
|
|
60501
|
+
if (!adapter.getPlans)
|
|
60502
|
+
return [];
|
|
60503
|
+
return adapter.getPlans(await resolveRobotSn(robot));
|
|
60504
|
+
}
|
|
60505
|
+
async function dispatch(robot, command) {
|
|
60506
|
+
const adapter = adapterFor(robot);
|
|
60507
|
+
if (!adapter.sendCommand) {
|
|
60508
|
+
throw new BadRequestError134(
|
|
60509
|
+
`${adapter.displayName} robots cannot be commanded through their API.`
|
|
60510
|
+
);
|
|
60511
|
+
}
|
|
60512
|
+
if (!commandSupported(adapter.capabilities, command.kind)) {
|
|
60513
|
+
throw new BadRequestError134(
|
|
60514
|
+
`${adapter.displayName} does not support "${command.kind}" through its API yet.`
|
|
60515
|
+
);
|
|
60516
|
+
}
|
|
60517
|
+
const robotSn = await resolveRobotSn(robot);
|
|
60518
|
+
if (command.kind === "return") {
|
|
60519
|
+
return adapter.sendCommand(robotSn, command);
|
|
60520
|
+
}
|
|
60521
|
+
const state = await getFleetRobot(robot);
|
|
60522
|
+
if (!state.dispatchable) {
|
|
60523
|
+
throw new BadRequestError134(
|
|
60524
|
+
`Robot "${robot.name}" is ${state.statusName || state.mode} and cannot take a new job.`
|
|
60525
|
+
);
|
|
60526
|
+
}
|
|
60527
|
+
return adapter.sendCommand(robotSn, command);
|
|
60528
|
+
}
|
|
60529
|
+
return {
|
|
60530
|
+
adapterFor,
|
|
60531
|
+
capabilitiesOf,
|
|
60532
|
+
resolveRobotSn,
|
|
60533
|
+
getFleetRobot,
|
|
60534
|
+
getMaps,
|
|
60535
|
+
getMapImages,
|
|
60536
|
+
getPlans,
|
|
60537
|
+
dispatch
|
|
60538
|
+
};
|
|
60539
|
+
}
|
|
60251
60540
|
|
|
60252
60541
|
// src/services/robot-tracking.service.ts
|
|
60253
60542
|
import axios4 from "axios";
|
|
@@ -60349,10 +60638,10 @@ function startRobotTracking(options) {
|
|
|
60349
60638
|
onEvent({ type: "reconnected", at: now() });
|
|
60350
60639
|
everConnected = true;
|
|
60351
60640
|
attempt = 0;
|
|
60352
|
-
stream.on("data", (
|
|
60641
|
+
stream.on("data", (chunk2) => {
|
|
60353
60642
|
if (stopped)
|
|
60354
60643
|
return;
|
|
60355
|
-
const { events, sawActivity } = parser.push(
|
|
60644
|
+
const { events, sawActivity } = parser.push(chunk2.toString("utf8"));
|
|
60356
60645
|
if (sawActivity)
|
|
60357
60646
|
lastActivityAt = now();
|
|
60358
60647
|
for (const event of events) {
|
|
@@ -60514,7 +60803,14 @@ function useRobotFleetController(deps) {
|
|
|
60514
60803
|
req.on("aborted", close);
|
|
60515
60804
|
res.on("error", close);
|
|
60516
60805
|
}
|
|
60517
|
-
|
|
60806
|
+
async function getBrands(_req, res, next) {
|
|
60807
|
+
try {
|
|
60808
|
+
res.json({ items: registeredRobotBrands() });
|
|
60809
|
+
} catch (error) {
|
|
60810
|
+
fail(next, error);
|
|
60811
|
+
}
|
|
60812
|
+
}
|
|
60813
|
+
return { getState, getMaps, getMapImages, getPlans, sendCommand: sendCommand2, trackRobot, getBrands };
|
|
60518
60814
|
}
|
|
60519
60815
|
|
|
60520
60816
|
// src/models/patrol-question.model.ts
|
|
@@ -94352,6 +94648,8 @@ export {
|
|
|
94352
94648
|
DEFAULT_RING_SECONDS,
|
|
94353
94649
|
DEFAULT_SCALE_M_PER_PX,
|
|
94354
94650
|
DEFAULT_SITE_TIMEZONE,
|
|
94651
|
+
DEMO_BRAND,
|
|
94652
|
+
DEMO_CAPABILITIES,
|
|
94355
94653
|
DEVICE_STATUS,
|
|
94356
94654
|
DOBStatus,
|
|
94357
94655
|
DUPLICATE_TERMS_VERSION_MESSAGE,
|
|
@@ -94573,6 +94871,7 @@ export {
|
|
|
94573
94871
|
VisitorStatus,
|
|
94574
94872
|
WALL_HEIGHT_M,
|
|
94575
94873
|
WALL_THICKNESS_M,
|
|
94874
|
+
adapterForRobot,
|
|
94576
94875
|
addressSchema,
|
|
94577
94876
|
allowedFieldsSite,
|
|
94578
94877
|
allowedNatures,
|
|
@@ -94612,13 +94911,14 @@ export {
|
|
|
94612
94911
|
chatSchema,
|
|
94613
94912
|
claimReadsAsStale,
|
|
94614
94913
|
clampPtzSpeed,
|
|
94615
|
-
|
|
94914
|
+
clearRobotBrands,
|
|
94616
94915
|
clockDriftSeconds,
|
|
94617
94916
|
commandSupported,
|
|
94618
94917
|
consoleRoleAllows,
|
|
94619
94918
|
consoleRoleAllowsAll,
|
|
94620
94919
|
consoleSpellings,
|
|
94621
94920
|
console_audit_namespace_collection,
|
|
94921
|
+
createDemoAdapter,
|
|
94622
94922
|
createLiftSimulator,
|
|
94623
94923
|
createManpowerRemarksDaily,
|
|
94624
94924
|
createOunitechAdapter,
|
|
@@ -94642,6 +94942,7 @@ export {
|
|
|
94642
94942
|
disconnectedState,
|
|
94643
94943
|
emitNotificationCreated,
|
|
94644
94944
|
emptyPersonalEmergencyChain,
|
|
94945
|
+
encodeGreyscalePng,
|
|
94645
94946
|
encodeHidPacsCard,
|
|
94646
94947
|
entitledSiteScope,
|
|
94647
94948
|
events_namespace_collection,
|
|
@@ -94660,6 +94961,7 @@ export {
|
|
|
94660
94961
|
getIO,
|
|
94661
94962
|
getSessionIdFromRequest,
|
|
94662
94963
|
grabWithSubStreamFallback,
|
|
94964
|
+
greyscalePngDataUri,
|
|
94663
94965
|
guests_namespace_collection,
|
|
94664
94966
|
hasAnyCapability,
|
|
94665
94967
|
hasAnyPermission,
|
|
@@ -94761,8 +95063,9 @@ export {
|
|
|
94761
95063
|
recordConsoleAction,
|
|
94762
95064
|
refuseServiceProviderInviteAction,
|
|
94763
95065
|
registerCameraTransport,
|
|
95066
|
+
registerDemoBrand,
|
|
94764
95067
|
registerOunitechBrand,
|
|
94765
|
-
|
|
95068
|
+
registerRobotBrand,
|
|
94766
95069
|
registeredRobotBrands,
|
|
94767
95070
|
relayForRecorder,
|
|
94768
95071
|
remarksSchema,
|
|
@@ -94781,7 +95084,7 @@ export {
|
|
|
94781
95084
|
resolveSiteTimezone,
|
|
94782
95085
|
resolveStaffRole,
|
|
94783
95086
|
robotActionsFor,
|
|
94784
|
-
|
|
95087
|
+
robotBrandFor,
|
|
94785
95088
|
robotCloudBody,
|
|
94786
95089
|
robotCloudSign,
|
|
94787
95090
|
robotCloudSigningString,
|