@7365admin1/core 3.82.0 → 3.83.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/dist/index.mjs CHANGED
@@ -58577,6 +58577,7 @@ var robotSchema = Joi74.object({
58577
58577
  // Optional, so every caller written before this field existed still validates.
58578
58578
  type: Joi74.string().valid(...ROBOT_TYPES).optional().allow("", null),
58579
58579
  robotSn: Joi74.string().optional().allow("", null),
58580
+ brand: Joi74.string().optional().allow("", null),
58580
58581
  appKey: Joi74.string().required(),
58581
58582
  appToken: Joi74.string().required(),
58582
58583
  createdBy: Joi74.string().hex().required(),
@@ -58638,6 +58639,7 @@ function MRobot(value) {
58638
58639
  */
58639
58640
  type: value.type || "cleaning",
58640
58641
  robotSn: value.robotSn ?? "",
58642
+ brand: value.brand || "ounitech",
58641
58643
  appKey: value.appKey ?? "",
58642
58644
  appToken: value.appToken ?? "",
58643
58645
  createdBy: value.createdBy ?? "",
@@ -58801,6 +58803,9 @@ function useRobotRepo() {
58801
58803
  // Not a secret - it is the vendor's device serial, and the apps
58802
58804
  // need it to address a robot. The CREDENTIALS stay out.
58803
58805
  robotSn: 1,
58806
+ // Not a secret - it names which protocol the robot speaks, and a
58807
+ // screen needs it to know which controls to draw.
58808
+ brand: 1,
58804
58809
  createdByName: 1,
58805
58810
  createdAt: 1,
58806
58811
  status: 1
@@ -59194,30 +59199,30 @@ function parseRobotCommand(body) {
59194
59199
  if (kind !== "full" && kind !== "area" && kind !== "spot" && kind !== "return") {
59195
59200
  return { ok: false, message: 'kind must be one of "full", "area", "spot", "return".' };
59196
59201
  }
59197
- const has = (k) => Object.prototype.hasOwnProperty.call(b, k);
59202
+ const has2 = (k) => Object.prototype.hasOwnProperty.call(b, k);
59198
59203
  const optionKeys = ["cleanTimes", "modeFlag", "subModeFlag", "levelFlag"];
59199
59204
  if (kind === "return") {
59200
59205
  for (const k of [...optionKeys, "areas", "points"]) {
59201
- if (has(k))
59206
+ if (has2(k))
59202
59207
  return { ok: false, message: `${k} is not valid for a return command.` };
59203
59208
  }
59204
59209
  return { ok: true, command: { kind: "return" } };
59205
59210
  }
59206
- if (kind !== "area" && has("areas")) {
59211
+ if (kind !== "area" && has2("areas")) {
59207
59212
  return { ok: false, message: "areas is only valid for an area command." };
59208
59213
  }
59209
- if (kind !== "spot" && has("points")) {
59214
+ if (kind !== "spot" && has2("points")) {
59210
59215
  return { ok: false, message: "points is only valid for a spot command." };
59211
59216
  }
59212
59217
  const int = (k, allowed, fallback) => {
59213
- if (!has(k))
59218
+ if (!has2(k))
59214
59219
  return fallback;
59215
59220
  const v = b[k];
59216
59221
  if (typeof v !== "number" || !Number.isInteger(v) || !allowed.includes(v))
59217
59222
  return null;
59218
59223
  return v;
59219
59224
  };
59220
- const cleanTimes = has("cleanTimes") ? typeof b.cleanTimes === "number" && Number.isInteger(b.cleanTimes) && b.cleanTimes >= 0 && b.cleanTimes <= 99 ? b.cleanTimes : null : DEFAULTS.cleanTimes;
59225
+ const cleanTimes = has2("cleanTimes") ? typeof b.cleanTimes === "number" && Number.isInteger(b.cleanTimes) && b.cleanTimes >= 0 && b.cleanTimes <= 99 ? b.cleanTimes : null : DEFAULTS.cleanTimes;
59221
59226
  if (cleanTimes === null)
59222
59227
  return { ok: false, message: "cleanTimes must be an integer 0-99." };
59223
59228
  const modeFlag = int("modeFlag", [1, 2, 3], DEFAULTS.modeFlag);
@@ -59433,6 +59438,125 @@ function robotActionsFor(actor) {
59433
59438
  );
59434
59439
  }
59435
59440
 
59441
+ // src/utils/robot-adapter.util.ts
59442
+ var ROBOT_CAPABILITIES = [
59443
+ /** Read the current location of the robot [x, y, yaw]. */
59444
+ "readLocation",
59445
+ /** Request the robot move to an [x, y, yaw] coordinate. */
59446
+ "navigateToPoint",
59447
+ /** Pause a robot while it is navigating, and resume it. */
59448
+ "pauseResume",
59449
+ /** Read the route/path taken, or the ETA to the destination. */
59450
+ "pathAndEta",
59451
+ /** Read the battery level. */
59452
+ "battery",
59453
+ /** Send the robot to its docking/charging station. */
59454
+ "sendToDock",
59455
+ /** Switch the on-board map and re-localise the robot. */
59456
+ "relocalize",
59457
+ /** Start a process — "clean zone A". */
59458
+ "startProcess",
59459
+ /** Pause, resume or stop a running process. */
59460
+ "pauseProcess",
59461
+ /** Know, rather than infer, when a process has finished. */
59462
+ "processCompletion",
59463
+ /** Report which floor the robot is on. */
59464
+ "floorReporting",
59465
+ /** Ride a lift between floors under our coordination. */
59466
+ "multiFloor"
59467
+ ];
59468
+ var TIER_REQUIREMENTS = {
59469
+ "full-control": [
59470
+ "readLocation",
59471
+ "navigateToPoint",
59472
+ "pauseResume",
59473
+ "pathAndEta",
59474
+ "battery",
59475
+ "sendToDock",
59476
+ "relocalize",
59477
+ "startProcess",
59478
+ "pauseProcess",
59479
+ "processCompletion"
59480
+ ],
59481
+ "traffic-light": [
59482
+ "readLocation",
59483
+ "pauseResume",
59484
+ "battery",
59485
+ "sendToDock",
59486
+ "startProcess",
59487
+ "pauseProcess",
59488
+ "processCompletion"
59489
+ ],
59490
+ "read-only": ["readLocation", "pathAndEta", "battery", "processCompletion"]
59491
+ };
59492
+ var has = (caps, c) => Object.prototype.hasOwnProperty.call(caps, c) && caps[c] === true;
59493
+ function robotTier(capabilities) {
59494
+ const caps = capabilities ?? {};
59495
+ for (const tier of ["full-control", "traffic-light", "read-only"]) {
59496
+ if (TIER_REQUIREMENTS[tier].every((c) => has(caps, c)))
59497
+ return tier;
59498
+ }
59499
+ return "no-interface";
59500
+ }
59501
+ function missingForTier(capabilities, tier) {
59502
+ const caps = capabilities ?? {};
59503
+ return TIER_REQUIREMENTS[tier].filter((c) => !has(caps, c));
59504
+ }
59505
+ function supports(capabilities, capability) {
59506
+ return has(capabilities ?? {}, capability);
59507
+ }
59508
+ function uncontrollableFleetWarning(tiers) {
59509
+ const count = tiers.filter(
59510
+ (t) => t === "read-only" || t === "no-interface"
59511
+ ).length;
59512
+ if (count <= 1) {
59513
+ return { ok: true, count, message: "" };
59514
+ }
59515
+ return {
59516
+ ok: false,
59517
+ count,
59518
+ message: `This site has ${count} robot fleets that cannot be controlled (read-only or no interface). A shared space supports at most one: beyond that, robots cannot reliably be kept out of each other's way.`
59519
+ };
59520
+ }
59521
+ var COMMAND_NEEDS = {
59522
+ full: "startProcess",
59523
+ area: "startProcess",
59524
+ spot: "startProcess",
59525
+ return: "sendToDock",
59526
+ pause: "pauseProcess",
59527
+ resume: "pauseProcess",
59528
+ cancel: "pauseProcess",
59529
+ navigate: "navigateToPoint",
59530
+ relocalize: "relocalize"
59531
+ };
59532
+ function commandSupported(capabilities, kind) {
59533
+ const needed = Object.prototype.hasOwnProperty.call(COMMAND_NEEDS, kind) ? COMMAND_NEEDS[kind] : void 0;
59534
+ if (!needed)
59535
+ return false;
59536
+ return supports(capabilities, needed);
59537
+ }
59538
+ var adapters = /* @__PURE__ */ new Map();
59539
+ function registerRobotAdapter(adapter) {
59540
+ if (!adapter?.brand)
59541
+ throw new Error("A robot adapter must name its brand.");
59542
+ adapters.set(adapter.brand, adapter);
59543
+ }
59544
+ function robotAdapterFor(brand) {
59545
+ if (typeof brand !== "string" || !brand)
59546
+ return null;
59547
+ return adapters.get(brand) ?? null;
59548
+ }
59549
+ function registeredRobotBrands() {
59550
+ return [...adapters.values()].map((a) => ({
59551
+ brand: a.brand,
59552
+ displayName: a.displayName,
59553
+ tier: robotTier(a.capabilities)
59554
+ }));
59555
+ }
59556
+ function clearRobotAdapters() {
59557
+ adapters.clear();
59558
+ }
59559
+
59436
59560
  // src/utils/lift-contract.util.ts
59437
59561
  var LIFT_DOOR = {
59438
59562
  CLOSED: 0,
@@ -59829,6 +59953,129 @@ function useRobotFleetService(deps) {
59829
59953
  };
59830
59954
  }
59831
59955
 
59956
+ // src/services/ounitech.adapter.ts
59957
+ var OUNITECH_BRAND = "ounitech";
59958
+ var OUNITECH_CAPABILITIES = {
59959
+ // The tracking stream carries x, y, theta.
59960
+ readLocation: true,
59961
+ // `/portal/robots/return-to-base`.
59962
+ sendToDock: true,
59963
+ // `start-full-cleaning`, `start-area-cleaning`, `start-spot-cleaning` — RMF's
59964
+ // "start a process (such as clean Zone_A)", and their areas are RMF's zones.
59965
+ startProcess: true,
59966
+ // Everything below is FALSE on purpose, and each one is a question already put
59967
+ // to the vendor. Their app can do most of it; the API cannot.
59968
+ navigateToPoint: false,
59969
+ // app: "Move to Target Point in standby mode"
59970
+ relocalize: false,
59971
+ // app: "Set Origin"
59972
+ pauseResume: false,
59973
+ // handlebar + app: Pause/Resume, long-press cancel
59974
+ pauseProcess: false,
59975
+ // app: "End work"
59976
+ battery: false,
59977
+ // nothing in the API carries it
59978
+ pathAndEta: false,
59979
+ // nothing in the API carries it
59980
+ processCompletion: false,
59981
+ // no task id is returned, so completion is inferred
59982
+ floorReporting: false,
59983
+ // the tracking stream has no floorNo
59984
+ multiFloor: false
59985
+ // their app does cross-floor; the API does not expose it
59986
+ };
59987
+ function createOunitechAdapter(credentials, options) {
59988
+ const cloud = useRobotCloudService(credentials, options);
59989
+ function toAdapterRobot(raw) {
59990
+ return {
59991
+ robotSn: raw.robotSn,
59992
+ name: raw.robotName,
59993
+ online: raw.onlineFlag !== false,
59994
+ mode: robotModeFromList(raw),
59995
+ statusName: raw.statusName,
59996
+ // Null, not 0 or a guess. A screen must be able to tell "we do not know"
59997
+ // from "empty", and `floorKnown: false` is what stops a robot being drawn
59998
+ // on a storey nobody confirmed.
59999
+ batteryPercent: null,
60000
+ floorNo: null,
60001
+ floorKnown: false,
60002
+ position: null
60003
+ };
60004
+ }
60005
+ return {
60006
+ brand: OUNITECH_BRAND,
60007
+ displayName: "Ounitech (Q3-G / Q3-W)",
60008
+ capabilities: OUNITECH_CAPABILITIES,
60009
+ async listRobots() {
60010
+ const fleet = await cloud.getRobots();
60011
+ return fleet.map(toAdapterRobot);
60012
+ },
60013
+ async getRobot(robotSn) {
60014
+ const fleet = await cloud.getRobots();
60015
+ const match = fleet.find((r) => r.robotSn === robotSn);
60016
+ if (!match) {
60017
+ return {
60018
+ robotSn,
60019
+ online: false,
60020
+ mode: "UNAVAILABLE",
60021
+ statusName: "Not found in the robot cloud",
60022
+ batteryPercent: null,
60023
+ floorNo: null,
60024
+ floorKnown: false,
60025
+ position: null
60026
+ };
60027
+ }
60028
+ return toAdapterRobot(match);
60029
+ },
60030
+ async sendCommand(robotSn, command) {
60031
+ const options2 = {
60032
+ cleanTimes: 1,
60033
+ modeFlag: 1,
60034
+ subModeFlag: 0,
60035
+ levelFlag: 1
60036
+ };
60037
+ switch (command.kind) {
60038
+ case "full":
60039
+ await cloud.startFullCleaning(robotSn, options2);
60040
+ return;
60041
+ case "area":
60042
+ await cloud.startAreaCleaning(robotSn, command.areas, options2);
60043
+ return;
60044
+ case "spot":
60045
+ await cloud.startSpotCleaning(robotSn, command.points, options2);
60046
+ return;
60047
+ case "return":
60048
+ await cloud.returnToBase(robotSn);
60049
+ return;
60050
+ default:
60051
+ throw new Error(
60052
+ `Ounitech robots do not support "${command.kind}" through the API yet.`
60053
+ );
60054
+ }
60055
+ },
60056
+ async getMaps(robotSn) {
60057
+ return cloud.getMaps(robotSn);
60058
+ },
60059
+ async getPlans(robotSn) {
60060
+ return cloud.getPlans(robotSn);
60061
+ }
60062
+ };
60063
+ }
60064
+ function registerOunitechBrand() {
60065
+ const unconfigured = () => {
60066
+ throw new Error(
60067
+ "No Ounitech credentials for this robot. Add the app key and token on the robot's record."
60068
+ );
60069
+ };
60070
+ registerRobotAdapter({
60071
+ brand: OUNITECH_BRAND,
60072
+ displayName: "Ounitech (Q3-G / Q3-W)",
60073
+ capabilities: OUNITECH_CAPABILITIES,
60074
+ listRobots: async () => unconfigured(),
60075
+ getRobot: async () => unconfigured()
60076
+ });
60077
+ }
60078
+
59832
60079
  // src/controllers/robot-fleet.controller.ts
59833
60080
  import Joi76 from "joi";
59834
60081
  import { BadRequestError as BadRequestError135, logger as logger106 } from "@7365admin1/node-server-utils";
@@ -93881,6 +94128,8 @@ export {
93881
94128
  ORG_OWNER_REPAIR_CLAIM_TAKEN_REASON,
93882
94129
  ORG_OWNER_REPAIR_DEFAULT,
93883
94130
  ORG_OWNER_REPAIR_OFF_REASON,
94131
+ OUNITECH_BRAND,
94132
+ OUNITECH_CAPABILITIES,
93884
94133
  OrgNature,
93885
94134
  OvernightParkingRequestSort,
93886
94135
  OvernightParkingRequestStatus,
@@ -93908,6 +94157,7 @@ export {
93908
94157
  REALTIME_MAX_FANOUT,
93909
94158
  REGISTERED_VEHICLE_CATEGORIES,
93910
94159
  ROBOT_APP_SCOPES,
94160
+ ROBOT_CAPABILITIES,
93911
94161
  ROBOT_CLOUD_DOC_CONFIRMATIONS,
93912
94162
  ROBOT_CLOUD_PATHS,
93913
94163
  ROBOT_MODULE,
@@ -93996,13 +94246,16 @@ export {
93996
94246
  chatSchema,
93997
94247
  claimReadsAsStale,
93998
94248
  clampPtzSpeed,
94249
+ clearRobotAdapters,
93999
94250
  clockDriftSeconds,
94251
+ commandSupported,
94000
94252
  consoleRoleAllows,
94001
94253
  consoleRoleAllowsAll,
94002
94254
  consoleSpellings,
94003
94255
  console_audit_namespace_collection,
94004
94256
  createLiftSimulator,
94005
94257
  createManpowerRemarksDaily,
94258
+ createOunitechAdapter,
94006
94259
  createSseParser,
94007
94260
  customerSchema,
94008
94261
  customerSitePropertyFields,
@@ -94084,6 +94337,7 @@ export {
94084
94337
  maskNric,
94085
94338
  memberModulesHandlers,
94086
94339
  metresToPixel,
94340
+ missingForTier,
94087
94341
  moduleDeniedForCaller,
94088
94342
  moduleGateEnforceKeys,
94089
94343
  moduleGateMode,
@@ -94138,6 +94392,9 @@ export {
94138
94392
  recordConsoleAction,
94139
94393
  refuseServiceProviderInviteAction,
94140
94394
  registerCameraTransport,
94395
+ registerOunitechBrand,
94396
+ registerRobotAdapter,
94397
+ registeredRobotBrands,
94141
94398
  relayForRecorder,
94142
94399
  remarksSchema,
94143
94400
  renderPagePdf,
@@ -94155,6 +94412,7 @@ export {
94155
94412
  resolveSiteTimezone,
94156
94413
  resolveStaffRole,
94157
94414
  robotActionsFor,
94415
+ robotAdapterFor,
94158
94416
  robotCloudBody,
94159
94417
  robotCloudSign,
94160
94418
  robotCloudSigningString,
@@ -94163,6 +94421,7 @@ export {
94163
94421
  robotModeFromList,
94164
94422
  robotSchema,
94165
94423
  robotScopeForApp,
94424
+ robotTier,
94166
94425
  robotTypesVisibleTo,
94167
94426
  rtspUrl,
94168
94427
  schema,
@@ -94336,7 +94595,9 @@ export {
94336
94595
  stripFacialImageMetadata,
94337
94596
  subscriptionPlanSchema,
94338
94597
  summariseBulkCameraPlan,
94598
+ supports,
94339
94599
  trackingBackoffMs,
94600
+ uncontrollableFleetWarning,
94340
94601
  updateRemarksStatusEod,
94341
94602
  updateRemarksisAcknowledged,
94342
94603
  updateSiteSchema,