@robotical/webapp-types 3.25.5 → 3.25.6

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.
Files changed (23) hide show
  1. package/dist-types/package.json +1 -1
  2. package/dist-types/src/application/RAFTs/Marty/PublishedDataAnalyser.d.ts +2 -2
  3. package/dist-types/src/application/RAFTs/Marty/PublishedDataAnalyser.js +7 -23
  4. package/dist-types/src/components/oneoffs/calibration-screens/ColourSensorCalibration/messages.d.ts +14 -2
  5. package/dist-types/src/components/oneoffs/calibration-screens/ColourSensorCalibration/messages.js +14 -2
  6. package/dist-types/src/screens/configuration/marty/Diagnostics/ColourSensorDiagnostics/messages.d.ts +2 -0
  7. package/dist-types/src/screens/configuration/marty/Diagnostics/ColourSensorDiagnostics/messages.js +3 -1
  8. package/dist-types/src/utils/colour-calibration-reconnect.d.ts +19 -0
  9. package/dist-types/src/utils/colour-calibration-reconnect.js +261 -0
  10. package/dist-types/src/utils/colour-calibration-records.d.ts +21 -0
  11. package/dist-types/src/utils/colour-calibration-records.js +253 -0
  12. package/dist-types/src/utils/colour-classifier.d.ts +32 -0
  13. package/dist-types/src/utils/colour-classifier.js +37 -0
  14. package/dist-types/src/utils/colour-sensor-diagnostics.d.ts +162 -0
  15. package/dist-types/src/utils/colour-sensor-diagnostics.js +364 -0
  16. package/dist-types/src/utils/colour-sensor-operation.d.ts +2 -0
  17. package/dist-types/src/utils/colour-sensor-operation.js +62 -0
  18. package/dist-types/src/utils/colour-sensor-profile.d.ts +32 -0
  19. package/dist-types/src/utils/colour-sensor-profile.js +229 -0
  20. package/dist-types/src/utils/colour-sensor-registers.d.ts +53 -0
  21. package/dist-types/src/utils/colour-sensor-registers.js +310 -0
  22. package/dist-types/src/utils/marty-colour-policy.json +5 -3
  23. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "marty-web-app",
3
- "version": "3.25.5",
3
+ "version": "3.25.6",
4
4
  "private": true,
5
5
  "engines": {
6
6
  "node": ">=20"
@@ -1,5 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  import EventEmitter from "events";
3
+ import { ColourSignalProfile } from "../../../utils/colour-classifier";
3
4
  import RAFT from "../RAFT";
4
5
  import type { RICStateInfo } from "@robotical/roboticaljs";
5
6
  import { type ColourSensorValues } from "./published-data-helpers";
@@ -39,8 +40,7 @@ declare class PublishedDataAnalyser extends EventEmitter {
39
40
  setNoiseSense(noiseSense: MartyState["noiseSense"]): void;
40
41
  }
41
42
  declare class ColourSensedDetection {
42
- private thresholds;
43
- detectColourSensed({ r, g, b, clear, air }: ColourSensorValues, analyser: PublishedDataAnalyser): void;
43
+ detectColourSensed({ r, g, b, clear, air }: ColourSensorValues, analyser: PublishedDataAnalyser, profile?: ColourSignalProfile): void;
44
44
  }
45
45
  declare class LightSenseDetection {
46
46
  private lowLightThreshold;
@@ -14,9 +14,10 @@ var __extends = (this && this.__extends) || (function () {
14
14
  };
15
15
  })();
16
16
  import EventEmitter from "events";
17
- import colourPolicy from "../../../utils/marty-colour-policy.json";
17
+ import { classifyColour } from "../../../utils/colour-classifier";
18
+ import { getAddonColourSensorProfile } from "../../../utils/colour-sensor-profile";
18
19
  import { createRaftPubSubscriptionHelper } from "../raft-subscription-helpers";
19
- import { getColourSensor, getLightSensor, getObjectSensor, getNoiseSensor, getRGBFromColourSensorVals, getHueChroma, getLightSenseValues, getObjectIsTouchingFromVals, getNoiseSenseValue, } from "./published-data-helpers";
20
+ import { getColourSensor, getLightSensor, getObjectSensor, getNoiseSensor, getRGBFromColourSensorVals, getLightSenseValues, getObjectIsTouchingFromVals, getNoiseSenseValue, } from "./published-data-helpers";
20
21
  var PublishedDataAnalyser = /** @class */ (function (_super) {
21
22
  __extends(PublishedDataAnalyser, _super);
22
23
  function PublishedDataAnalyser(marty) {
@@ -80,6 +81,7 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
80
81
  (_a = this.pubSub) === null || _a === void 0 ? void 0 : _a.unsubscribe();
81
82
  };
82
83
  PublishedDataAnalyser.prototype.analyse = function (data) {
84
+ var _a;
83
85
  if (!data)
84
86
  return;
85
87
  var colourSensor = getColourSensor(data);
@@ -87,7 +89,7 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
87
89
  var objectSensor = getObjectSensor(data);
88
90
  var noiseSensor = getNoiseSensor(data);
89
91
  colourSensor &&
90
- this.colourSensedDetection.detectColourSensed(getRGBFromColourSensorVals(colourSensor), this);
92
+ this.colourSensedDetection.detectColourSensed(getRGBFromColourSensorVals(colourSensor), this, (_a = getAddonColourSensorProfile(this.marty, colourSensor)) === null || _a === void 0 ? void 0 : _a.signal);
91
93
  (objectSensor || colourSensor) &&
92
94
  this.objectSenseDetection.detectObjectSense(getObjectIsTouchingFromVals(objectSensor, colourSensor), this);
93
95
  lightSensor &&
@@ -119,28 +121,10 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
119
121
  }(EventEmitter));
120
122
  var ColourSensedDetection = /** @class */ (function () {
121
123
  function ColourSensedDetection() {
122
- this.thresholds = colourPolicy.boundaries;
123
124
  }
124
- ColourSensedDetection.prototype.detectColourSensed = function (_a, analyser) {
125
+ ColourSensedDetection.prototype.detectColourSensed = function (_a, analyser, profile) {
125
126
  var r = _a.r, g = _a.g, b = _a.b, clear = _a.clear, air = _a.air;
126
- if (air) {
127
- return analyser.setColourSensed("air");
128
- }
129
- else {
130
- var _b = getHueChroma(r, g, b), hue = _b[0], chroma = _b[1];
131
- for (var _i = 0, _c = this.thresholds; _i < _c.length; _i++) {
132
- var colour = _c[_i];
133
- if (colour.hue[0] <= hue &&
134
- hue <= colour.hue[1] &&
135
- colour.chroma[0] <= chroma &&
136
- chroma <= colour.chroma[1] &&
137
- colour.clear[0] <= clear &&
138
- clear <= colour.clear[1]) {
139
- return analyser.setColourSensed(colour.name);
140
- }
141
- }
142
- return analyser.setColourSensed("unclear");
143
- }
127
+ return analyser.setColourSensed(classifyColour({ red: r, green: g, blue: b, clear: clear, air: air }, profile).classifiedColour);
144
128
  };
145
129
  return ColourSensedDetection;
146
130
  }());
@@ -1,5 +1,12 @@
1
1
  declare const english: {
2
2
  readonly title: "Colour sensor calibration";
3
+ readonly sessionId: "Diagnostics session ID";
4
+ readonly uploading: "Saving calibration readings and results to the database…";
5
+ readonly uploadsSaved: "Calibration readings and results are saved automatically to the database under this session ID.";
6
+ readonly uploadPending: "Some calibration data is waiting to upload. It is saved on this device and will retry automatically when the connection is available.";
7
+ readonly uploadStorageWarning: "Some calibration data could not be saved on this device yet. Keep the app open and retry uploading before closing it.";
8
+ readonly phoneUploadPending: "Calibration data is waiting to upload. Keep the app open until uploading finishes so these readings are not lost.";
9
+ readonly retryUpload: "Retry calibration upload";
3
10
  readonly introduction: "Keep Marty connected to USB power. We will collect {{count}} readings each for air and white. If you have a purple colour card, you can also use it for an optional step. Keep the lighting the same throughout.";
4
11
  readonly sensor: "Colour sensor";
5
12
  readonly selectSensor: "Select a colour sensor";
@@ -22,16 +29,21 @@ declare const english: {
22
29
  readonly capturePurple: "Capture purple and save calibration";
23
30
  readonly skipPurple: "Skip and save calibration";
24
31
  readonly startAgain: "Start again";
32
+ readonly restartCalibration: "Calibrate this sensor again";
25
33
  readonly saved: "Calibration saved on Marty: {{count}} readings for each position. This sensor's ground threshold is {{threshold}}.";
26
34
  readonly savedWithoutPurple: "Calibration saved on Marty: {{count}} readings each for air and white. Purple was skipped, so the current ground threshold was kept.";
35
+ readonly savedWithRGB: "Calibration saved: {{count}} readings for air, white and purple. Purple's colour signal is usable, but its brightness overlaps air. The app will use the calibrated colour signal and keep Marty's current ground threshold.";
27
36
  readonly reconnectStep: "Final step: reconnect Marty";
28
37
  readonly reconnect: "Disconnect Marty below, then use Connect Marty to connect the same robot again. Your calibration is saved. Colour diagnostics will unlock once Marty has loaded the new sensor settings.";
29
38
  readonly disconnectToFinish: "Disconnect Marty to finish calibration";
30
39
  readonly disconnecting: "Disconnecting Marty…";
31
40
  readonly loadingCalibration: "Marty has reconnected. Waiting for the new sensor settings and fresh readings…";
32
- readonly calibrationReloadFailed: "The saved calibration could not be confirmed for this sensor. Keep the same sensor attached, disconnect Marty and reconnect it again.";
41
+ readonly calibrationCheckDelayed: "Marty is connected, but the sensor check is taking longer than expected. The app is retrying automatically. Keep the sensor attached and Marty on USB power. If this continues, disconnect and reconnect Marty.";
42
+ readonly calibrationSensorChanged: "The attached colour sensor is different from the one that was calibrated. Reattach the original sensor to finish, or use the calibration screen to calibrate this sensor again.";
43
+ readonly calibrationSettingsChanged: "The sensor's stored white reference differs from the calibration that was saved. Reconnect Marty once; if this message remains, use the calibration screen to calibrate this sensor again.";
44
+ readonly disconnectFailed: "Marty could not be disconnected. Try the disconnect button again.";
33
45
  readonly reconnected: "Calibration complete. Marty has reconnected and loaded the saved sensor settings. Use Settings → Diagnostics to check all five cards and air.";
34
- readonly download: "Download calibration results";
46
+ readonly download: "Download a copy (optional)";
35
47
  readonly back: "Back to calibration";
36
48
  readonly usbRequired: "Connect Marty to USB power and keep it connected until calibration finishes.";
37
49
  readonly notInAir: "Lift Marty so both feet are clear of the surface, then try again.";
@@ -1,5 +1,12 @@
1
1
  var english = {
2
2
  title: "Colour sensor calibration",
3
+ sessionId: "Diagnostics session ID",
4
+ uploading: "Saving calibration readings and results to the database…",
5
+ uploadsSaved: "Calibration readings and results are saved automatically to the database under this session ID.",
6
+ uploadPending: "Some calibration data is waiting to upload. It is saved on this device and will retry automatically when the connection is available.",
7
+ uploadStorageWarning: "Some calibration data could not be saved on this device yet. Keep the app open and retry uploading before closing it.",
8
+ phoneUploadPending: "Calibration data is waiting to upload. Keep the app open until uploading finishes so these readings are not lost.",
9
+ retryUpload: "Retry calibration upload",
3
10
  introduction: "Keep Marty connected to USB power. We will collect {{count}} readings each for air and white. If you have a purple colour card, you can also use it for an optional step. Keep the lighting the same throughout.",
4
11
  sensor: "Colour sensor",
5
12
  selectSensor: "Select a colour sensor",
@@ -22,16 +29,21 @@ var english = {
22
29
  capturePurple: "Capture purple and save calibration",
23
30
  skipPurple: "Skip and save calibration",
24
31
  startAgain: "Start again",
32
+ restartCalibration: "Calibrate this sensor again",
25
33
  saved: "Calibration saved on Marty: {{count}} readings for each position. This sensor's ground threshold is {{threshold}}.",
26
34
  savedWithoutPurple: "Calibration saved on Marty: {{count}} readings each for air and white. Purple was skipped, so the current ground threshold was kept.",
35
+ savedWithRGB: "Calibration saved: {{count}} readings for air, white and purple. Purple's colour signal is usable, but its brightness overlaps air. The app will use the calibrated colour signal and keep Marty's current ground threshold.",
27
36
  reconnectStep: "Final step: reconnect Marty",
28
37
  reconnect: "Disconnect Marty below, then use Connect Marty to connect the same robot again. Your calibration is saved. Colour diagnostics will unlock once Marty has loaded the new sensor settings.",
29
38
  disconnectToFinish: "Disconnect Marty to finish calibration",
30
39
  disconnecting: "Disconnecting Marty…",
31
40
  loadingCalibration: "Marty has reconnected. Waiting for the new sensor settings and fresh readings…",
32
- calibrationReloadFailed: "The saved calibration could not be confirmed for this sensor. Keep the same sensor attached, disconnect Marty and reconnect it again.",
41
+ calibrationCheckDelayed: "Marty is connected, but the sensor check is taking longer than expected. The app is retrying automatically. Keep the sensor attached and Marty on USB power. If this continues, disconnect and reconnect Marty.",
42
+ calibrationSensorChanged: "The attached colour sensor is different from the one that was calibrated. Reattach the original sensor to finish, or use the calibration screen to calibrate this sensor again.",
43
+ calibrationSettingsChanged: "The sensor's stored white reference differs from the calibration that was saved. Reconnect Marty once; if this message remains, use the calibration screen to calibrate this sensor again.",
44
+ disconnectFailed: "Marty could not be disconnected. Try the disconnect button again.",
33
45
  reconnected: "Calibration complete. Marty has reconnected and loaded the saved sensor settings. Use Settings → Diagnostics to check all five cards and air.",
34
- download: "Download calibration results",
46
+ download: "Download a copy (optional)",
35
47
  back: "Back to calibration",
36
48
  usbRequired: "Connect Marty to USB power and keep it connected until calibration finishes.",
37
49
  notInAir: "Lift Marty so both feet are clear of the surface, then try again.",
@@ -17,6 +17,7 @@ declare const colourDiagnosticsEnglish: {
17
17
  airPrompt: string;
18
18
  result: string;
19
19
  withoutAir: string;
20
+ firmwareAir: string;
20
21
  waiting: string;
21
22
  capture: string;
22
23
  captureAir: string;
@@ -33,6 +34,7 @@ declare const colourDiagnosticsEnglish: {
33
34
  storageWarning: string;
34
35
  sensorDisconnected: string;
35
36
  sensorInitialising: string;
37
+ sensorBusy: string;
36
38
  calibrationReconnectRequired: string;
37
39
  sensorAsleep: string;
38
40
  sensorOffline: string;
@@ -11,7 +11,8 @@ var colourDiagnosticsEnglish = {
11
11
  prompt: "Place the selected sensor on the {{colour}} card.",
12
12
  airPrompt: "Lift Marty so both feet are at least 10 cm above any card or surface, with the sensors facing down and uncovered. Keep the lighting the same and hold still.",
13
13
  result: "Blocks interpretation: {{result}}",
14
- withoutAir: "Colour match before the air flag is applied: {{result}}",
14
+ withoutAir: "Firmware reports air; the RGB signal matches {{result}}.",
15
+ firmwareAir: "Firmware Air",
15
16
  waiting: "Select a sensor and wait for fresh readings.",
16
17
  capture: "Take {{colour}} snapshot",
17
18
  captureAir: "Take air snapshot",
@@ -28,6 +29,7 @@ var colourDiagnosticsEnglish = {
28
29
  storageWarning: "This browser cannot keep a local backup. Download the data before closing this page.",
29
30
  sensorDisconnected: "The selected sensor disconnected. Reconnect Marty and select the sensor again.",
30
31
  sensorInitialising: "Marty is still loading the sensor settings. Wait a few seconds and try again.",
32
+ sensorBusy: "The sensor is completing another check. Wait a moment and take the snapshot again.",
31
33
  calibrationReconnectRequired: "Reconnect Marty to finish calibration before taking more colour snapshots.",
32
34
  sensorAsleep: "Marty's sensor power is asleep. Taking a snapshot wakes the sensors before collecting readings.",
33
35
  sensorOffline: "The colour sensor is offline. Take a snapshot to wake it. If it stays offline, check the sensor connection.",
@@ -0,0 +1,19 @@
1
+ import type { ColourSensor } from "./colour-sensor-diagnostics";
2
+ import { CalibrationRecordRef } from "./colour-calibration-records";
3
+ type PendingCalibration = {
4
+ connectionInstanceId: string;
5
+ serialNumber: string;
6
+ whiteCalibrationHex: string;
7
+ calibrationRecord?: CalibrationRecordRef;
8
+ };
9
+ export declare class ColourCalibrationReconnectError extends Error {
10
+ reason: "calibrationSensorChanged" | "calibrationSettingsChanged";
11
+ constructor(reason: "calibrationSensorChanged" | "calibrationSettingsChanged");
12
+ }
13
+ export declare const getPendingColourCalibration: (sensor: ColourSensor) => PendingCalibration | null;
14
+ export declare const markColourCalibrationPending: (sensor: ColourSensor, serialNumber: string, whiteCalibrationHex: string, calibrationRecord?: CalibrationRecordRef) => void;
15
+ export declare const restorePendingColourCalibration: (sensor: ColourSensor, pending: PendingCalibration) => void;
16
+ export declare const clearPendingColourCalibration: (sensor: ColourSensor) => void;
17
+ /** Read-only completion check, shared by browsers and native WebViews through the existing REST bridge. */
18
+ export declare const finishColourCalibrationReconnect: (sensor: ColourSensor, signal: AbortSignal) => Promise<boolean>;
19
+ export {};
@@ -0,0 +1,261 @@
1
+ var __extends = (this && this.__extends) || (function () {
2
+ var extendStatics = function (d, b) {
3
+ extendStatics = Object.setPrototypeOf ||
4
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
5
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
6
+ return extendStatics(d, b);
7
+ };
8
+ return function (d, b) {
9
+ if (typeof b !== "function" && b !== null)
10
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
11
+ extendStatics(d, b);
12
+ function __() { this.constructor = d; }
13
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
14
+ };
15
+ })();
16
+ var __assign = (this && this.__assign) || function () {
17
+ __assign = Object.assign || function(t) {
18
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
19
+ s = arguments[i];
20
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
21
+ t[p] = s[p];
22
+ }
23
+ return t;
24
+ };
25
+ return __assign.apply(this, arguments);
26
+ };
27
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
28
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
29
+ return new (P || (P = Promise))(function (resolve, reject) {
30
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
31
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
32
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
33
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
34
+ });
35
+ };
36
+ var __generator = (this && this.__generator) || function (thisArg, body) {
37
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
38
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
39
+ function verb(n) { return function (v) { return step([n, v]); }; }
40
+ function step(op) {
41
+ if (f) throw new TypeError("Generator is already executing.");
42
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
43
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
44
+ if (y = 0, t) op = [op[0] & 2, t.value];
45
+ switch (op[0]) {
46
+ case 0: case 1: t = op; break;
47
+ case 4: _.label++; return { value: op[1], done: false };
48
+ case 5: _.label++; y = op[1]; op = [0]; continue;
49
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
50
+ default:
51
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
52
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
53
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
54
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
55
+ if (t[2]) _.ops.pop();
56
+ _.trys.pop(); continue;
57
+ }
58
+ op = body.call(thisArg, _);
59
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
60
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
61
+ }
62
+ };
63
+ import { loadColourSession, persistColourSession } from "./colour-sensor-diagnostics";
64
+ import { readColourSensorHardware, readColourSensorReadiness, readColourSensorRegister, wakeColourSensor } from "./colour-sensor-registers";
65
+ import { createColourCalibrationRecorder, recordColourCalibrationEvent } from "./colour-calibration-records";
66
+ import { lockColourSensorOperation } from "./colour-sensor-operation";
67
+ var keyFor = function (sensor) { return "marty-colour-calibration-pending:".concat(JSON.stringify([sensor.raftId, sensor.key])); };
68
+ var cached = new Map();
69
+ var checking = new WeakSet();
70
+ var checks = new WeakMap();
71
+ var ColourCalibrationReconnectError = /** @class */ (function (_super) {
72
+ __extends(ColourCalibrationReconnectError, _super);
73
+ function ColourCalibrationReconnectError(reason) {
74
+ var _this = _super.call(this, "calibrationReloadFailed") || this;
75
+ _this.reason = reason;
76
+ return _this;
77
+ }
78
+ return ColourCalibrationReconnectError;
79
+ }(Error));
80
+ export { ColourCalibrationReconnectError };
81
+ export var getPendingColourCalibration = function (sensor) {
82
+ var key = keyFor(sensor);
83
+ try {
84
+ var value = localStorage.getItem(key);
85
+ if (!value) {
86
+ cached.delete(key);
87
+ return null;
88
+ }
89
+ var parsed = JSON.parse(value);
90
+ if (typeof parsed.connectionInstanceId !== "string" || typeof parsed.serialNumber !== "string" ||
91
+ !/^[a-f0-9]{16}$/.test(parsed.whiteCalibrationHex))
92
+ throw new Error("Invalid pending calibration");
93
+ cached.set(key, parsed);
94
+ return parsed;
95
+ }
96
+ catch (_a) {
97
+ // Never unlock a known pending calibration just because storage becomes unavailable.
98
+ return cached.get(key) || null;
99
+ }
100
+ };
101
+ export var markColourCalibrationPending = function (sensor, serialNumber, whiteCalibrationHex, calibrationRecord) {
102
+ var pending = { connectionInstanceId: sensor.raft.connectionInstanceId || "", serialNumber: serialNumber, whiteCalibrationHex: whiteCalibrationHex, calibrationRecord: calibrationRecord };
103
+ restorePendingColourCalibration(sensor, pending);
104
+ };
105
+ export var restorePendingColourCalibration = function (sensor, pending) {
106
+ localStorage.setItem(keyFor(sensor), JSON.stringify(pending));
107
+ cached.set(keyFor(sensor), pending);
108
+ };
109
+ export var clearPendingColourCalibration = function (sensor) {
110
+ localStorage.removeItem(keyFor(sensor));
111
+ cached.delete(keyFor(sensor));
112
+ };
113
+ /** Read-only completion check, shared by browsers and native WebViews through the existing REST bridge. */
114
+ export var finishColourCalibrationReconnect = function (sensor, signal) { return __awaiter(void 0, void 0, void 0, function () {
115
+ var pending, instanceId, release, pendingKey, connectionChecks, previousCheck, check, record, checkActive, session, recorder, ref, linked, readiness, power, hardware, white, latest, state, addon;
116
+ return __generator(this, function (_a) {
117
+ switch (_a.label) {
118
+ case 0:
119
+ pending = getPendingColourCalibration(sensor);
120
+ if (!pending)
121
+ return [2 /*return*/, true];
122
+ instanceId = sensor.raft.connectionInstanceId;
123
+ // A fresh packet, repeated verification, or a different selected robot is not a reconnect.
124
+ if (!instanceId || instanceId === pending.connectionInstanceId)
125
+ return [2 /*return*/, false];
126
+ // Add-ons have one raw response slot. Independent mounted panels must not overwrite each other's commands.
127
+ if (checking.has(sensor.raft))
128
+ return [2 /*return*/, false];
129
+ release = lockColourSensorOperation(sensor.raft);
130
+ if (!release)
131
+ return [2 /*return*/, false];
132
+ checking.add(sensor.raft);
133
+ pendingKey = JSON.stringify(pending);
134
+ connectionChecks = checks.get(sensor.raft);
135
+ if (!connectionChecks) {
136
+ connectionChecks = new Map();
137
+ checks.set(sensor.raft, connectionChecks);
138
+ }
139
+ previousCheck = connectionChecks.get(sensor.key);
140
+ check = (previousCheck === null || previousCheck === void 0 ? void 0 : previousCheck.pending) === pendingKey ? previousCheck :
141
+ { pending: pendingKey, calibrationRecord: pending.calibrationRecord };
142
+ connectionChecks.set(sensor.key, check);
143
+ record = function (reason, data) {
144
+ if (!check.calibrationRecord || (check.loggedReason === reason && Date.now() - check.loggedAtMs < 30000))
145
+ return;
146
+ check.loggedReason = reason;
147
+ check.loggedAtMs = Date.now();
148
+ // Upload problems must never turn a successful hardware check into a failed calibration.
149
+ try {
150
+ recordColourCalibrationEvent(check.calibrationRecord, "reconnectCheck", __assign({ reason: reason, connectionInstanceId: instanceId, expectedSerialNumber: pending.serialNumber, expectedWhiteCalibrationHex: pending.whiteCalibrationHex, readyAtMs: check.readyAtMs }, data));
151
+ }
152
+ catch ( /* The upload queue reports its own failures. */_a) { /* The upload queue reports its own failures. */ }
153
+ };
154
+ checkActive = function () {
155
+ var _a;
156
+ var current = window.applicationManager.connectedRafts[sensor.raftId];
157
+ if (signal.aborted || document.hidden || current !== sensor.raft || !current.isVerified ||
158
+ !((_a = sensor.raft.raftStateInfo) === null || _a === void 0 ? void 0 : _a.addOnInfo.addons.some(function (addon) { return addon.id === sensor.addon.id && addon.name === sensor.addon.name; }))) {
159
+ throw new Error("cancelled");
160
+ }
161
+ };
162
+ _a.label = 1;
163
+ case 1:
164
+ _a.trys.push([1, , 6, 7]);
165
+ checkActive();
166
+ // Older app versions saved pending settings without an upload reference. Log their reconnect too.
167
+ if (!check.calibrationRecord) {
168
+ try {
169
+ session = loadColourSession();
170
+ persistColourSession(session);
171
+ recorder = createColourCalibrationRecorder(sensor, session);
172
+ ref = { sessionId: recorder.sessionId, attemptId: recorder.attemptId };
173
+ check.calibrationRecord = ref;
174
+ linked = __assign(__assign({}, pending), { calibrationRecord: ref });
175
+ try {
176
+ restorePendingColourCalibration(sensor, linked);
177
+ Object.assign(pending, linked);
178
+ pendingKey = check.pending = JSON.stringify(linked);
179
+ }
180
+ catch ( /* The in-memory check can still upload when pending-record storage is unavailable. */_b) { /* The in-memory check can still upload when pending-record storage is unavailable. */ }
181
+ recorder.record("reconnectStarted", { reconnectOnly: true, expectedSerialNumber: pending.serialNumber,
182
+ expectedWhiteCalibrationHex: pending.whiteCalibrationHex, connectionInstanceId: instanceId });
183
+ }
184
+ catch ( /* Logging must not prevent read-only hardware confirmation. */_c) { /* Logging must not prevent read-only hardware confirmation. */ }
185
+ }
186
+ return [4 /*yield*/, readColourSensorReadiness(sensor, signal, checkActive)];
187
+ case 2:
188
+ readiness = _a.sent();
189
+ if (readiness.status !== "ok" || !readiness.value) {
190
+ // A new initialization cycle on this connection invalidates the previous freshness reference.
191
+ if (readiness.status === "ok")
192
+ check.readyAtMs = undefined;
193
+ record("initialising", { readiness: readiness });
194
+ return [2 /*return*/, false];
195
+ }
196
+ // Keep the first confirmed initialization time. Polling must not keep moving the freshness target.
197
+ if (check.readyAtMs === undefined)
198
+ check.readyAtMs = readiness.receivedAtMs;
199
+ return [4 /*yield*/, wakeColourSensor(sensor, signal, checkActive)];
200
+ case 3:
201
+ power = _a.sent();
202
+ if (power.status !== "ok") {
203
+ record("sensorWakeUnavailable", { readiness: readiness, power: power });
204
+ return [2 /*return*/, false];
205
+ }
206
+ return [4 /*yield*/, readColourSensorHardware(sensor, signal, checkActive)];
207
+ case 4:
208
+ hardware = _a.sent();
209
+ if (hardware.status !== "ok" || !hardware.value.serialNumber || hardware.value.commsOk === 0) {
210
+ record("hardwareReadUnavailable", { readiness: readiness, power: power, hardware: hardware });
211
+ return [2 /*return*/, false];
212
+ }
213
+ if (JSON.stringify(getPendingColourCalibration(sensor)) !== pendingKey)
214
+ return [2 /*return*/, false];
215
+ if (hardware.value.serialNumber !== pending.serialNumber) {
216
+ record("sensorChanged", { hardware: hardware });
217
+ throw new ColourCalibrationReconnectError("calibrationSensorChanged");
218
+ }
219
+ return [4 /*yield*/, readColourSensorRegister(sensor, "4020", signal, checkActive)];
220
+ case 5:
221
+ white = _a.sent();
222
+ checkActive();
223
+ latest = getPendingColourCalibration(sensor);
224
+ if (!latest || JSON.stringify(latest) !== pendingKey)
225
+ return [2 /*return*/, false];
226
+ if (white.status !== "ok") {
227
+ record("whiteReadUnavailable", { hardware: hardware, whiteCalibration: white });
228
+ return [2 /*return*/, false];
229
+ }
230
+ if (white.value !== pending.whiteCalibrationHex) {
231
+ record("settingsChanged", { hardware: hardware, whiteCalibration: white });
232
+ throw new ColourCalibrationReconnectError("calibrationSettingsChanged");
233
+ }
234
+ state = sensor.raft.raftStateInfo;
235
+ addon = state.addOnInfo.addons.find(function (candidate) { return candidate.id === sensor.addon.id && candidate.name === sensor.addon.name; });
236
+ if (state.addOnInfoValidMs < check.readyAtMs || Date.now() - state.addOnInfoValidMs > 1500 ||
237
+ state.addOnInfoValidMs > Date.now() || (addon.status !== undefined && !(addon.status & 0x80))) {
238
+ record("waitingForTelemetry", { hardware: hardware, whiteCalibration: white, receivedAtMs: state.addOnInfoValidMs, sensor: addon });
239
+ return [2 /*return*/, false];
240
+ }
241
+ clearPendingColourCalibration(sensor);
242
+ if (check.calibrationRecord) {
243
+ try {
244
+ recordColourCalibrationEvent(check.calibrationRecord, "reconnectCompleted", {
245
+ connectionInstanceId: instanceId,
246
+ hardware: hardware,
247
+ whiteCalibration: white, readyAtMs: check.readyAtMs,
248
+ receivedAtMs: state.addOnInfoValidMs, sensor: addon,
249
+ });
250
+ }
251
+ catch ( /* Hardware confirmation remains valid if logging fails. */_d) { /* Hardware confirmation remains valid if logging fails. */ }
252
+ }
253
+ return [2 /*return*/, true];
254
+ case 6:
255
+ checking.delete(sensor.raft);
256
+ release();
257
+ return [7 /*endfinally*/];
258
+ case 7: return [2 /*return*/];
259
+ }
260
+ });
261
+ }); };
@@ -0,0 +1,21 @@
1
+ import type { ColourSensor, ColourSession } from "./colour-sensor-diagnostics";
2
+ export type CalibrationRecordRef = {
3
+ sessionId: string;
4
+ attemptId: string;
5
+ };
6
+ export type CalibrationRecorder = CalibrationRecordRef & {
7
+ record: (type: string, data: Record<string, unknown>) => void;
8
+ };
9
+ /** Immutable event IDs allow safe retries after an unknown HTTP outcome or a reload. */
10
+ export declare const recordColourCalibrationEvent: (ref: CalibrationRecordRef, type: string, data: Record<string, unknown>, metadata?: Record<string, unknown>, sequence?: number) => void;
11
+ export declare const getColourCalibrationUploadState: (sessionId: string) => {
12
+ pendingCount: number;
13
+ inMemoryOnly: boolean;
14
+ temporaryStorage: boolean;
15
+ uploading: boolean;
16
+ error: string;
17
+ };
18
+ export declare const subscribeColourCalibrationRecords: (listener: () => void) => () => void;
19
+ export declare const flushColourCalibrationRecords: () => Promise<void>;
20
+ export declare const startColourCalibrationRecordSync: () => () => void;
21
+ export declare const createColourCalibrationRecorder: (sensor: ColourSensor, session: ColourSession) => CalibrationRecorder;