@robotical/webapp-types 3.25.10 → 3.25.12

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "marty-web-app",
3
- "version": "3.25.10",
3
+ "version": "3.25.12",
4
4
  "private": true,
5
5
  "engines": {
6
6
  "node": ">=20"
@@ -1,12 +1,12 @@
1
1
  /// <reference types="node" />
2
2
  import EventEmitter from "events";
3
- import { ColourSignalProfile } from "../../../utils/colour-classifier";
3
+ import { classifyColour, ColourSignalProfile } from "../../../utils/colour-classifier";
4
4
  import RAFT from "../RAFT";
5
5
  import type { RICStateInfo } from "@robotical/roboticaljs";
6
6
  import { type ColourSensorValues } from "./published-data-helpers";
7
7
  interface MartyState {
8
8
  colourSensed: "blue" | "green" | "red" | "yellow" | "purple" | "unclear" | "air";
9
- /** Rule strength (0–100), not probability; null when no scored reading is available. */
9
+ /** Rule strength of the confirmed/held colour (0–100); null without a reading. */
10
10
  colourConfidence: number | null;
11
11
  lightSense: "none" | "high" | "mid" | "low";
12
12
  objectSense: "none" | "near";
@@ -16,6 +16,11 @@ declare class PublishedDataAnalyser extends EventEmitter {
16
16
  private marty;
17
17
  martyState: MartyState;
18
18
  private pubSub;
19
+ lastColourReading: ReturnType<typeof classifyColour> | null;
20
+ private colourFilter;
21
+ private colourContext;
22
+ private colourExpiry;
23
+ private connectionObserver;
19
24
  lightSenseDetection: LightSenseDetection;
20
25
  colourSensedDetection: ColourSensedDetection;
21
26
  objectSenseDetection: ObjectSenseDetection;
@@ -37,12 +42,15 @@ declare class PublishedDataAnalyser extends EventEmitter {
37
42
  unsubscribeFromPublishedData(): void;
38
43
  analyse(data: RICStateInfo): void;
39
44
  setColourSensed(colour: MartyState["colourSensed"], confidence?: number | null): void;
45
+ private resetColourDetection;
46
+ recordColourReading(reading: ReturnType<typeof classifyColour>, receivedAtMs: number): void;
47
+ private scheduleColourExpiry;
40
48
  setLightSense(lightSense: MartyState["lightSense"]): void;
41
49
  setObjectSense(objectSense: MartyState["objectSense"]): void;
42
50
  setNoiseSense(noiseSense: MartyState["noiseSense"]): void;
43
51
  }
44
52
  declare class ColourSensedDetection {
45
- detectColourSensed({ r, g, b, clear, air }: ColourSensorValues, analyser: PublishedDataAnalyser, profile?: ColourSignalProfile): void;
53
+ detectColourSensed({ r, g, b, clear, air }: ColourSensorValues, analyser: PublishedDataAnalyser, profile?: ColourSignalProfile, receivedAtMs?: number): void;
46
54
  }
47
55
  declare class LightSenseDetection {
48
56
  private lowLightThreshold;
@@ -14,15 +14,29 @@ var __extends = (this && this.__extends) || (function () {
14
14
  };
15
15
  })();
16
16
  import EventEmitter from "events";
17
- import { classifyColour } from "../../../utils/colour-classifier";
17
+ import { RaftConnEvent } from "@robdobsn/raftjs";
18
+ import { classifyColour, colourChannelsFromValues } from "../../../utils/colour-classifier";
19
+ import { ColourEventFilter } from "../../../utils/colour-event-filter";
18
20
  import { getAddonColourSensorProfile } from "../../../utils/colour-sensor-profile";
19
21
  import { createRaftPubSubscriptionHelper } from "../raft-subscription-helpers";
20
- import { getColourSensor, getLightSensor, getObjectSensor, getNoiseSensor, getRGBFromColourSensorVals, getLightSenseValues, getObjectIsTouchingFromVals, getNoiseSenseValue, } from "./published-data-helpers";
22
+ import { getColourSensor, getLightSensor, getObjectSensor, getNoiseSensor, getLightSenseValues, getObjectIsTouchingFromVals, getNoiseSenseValue, } from "./published-data-helpers";
21
23
  var PublishedDataAnalyser = /** @class */ (function (_super) {
22
24
  __extends(PublishedDataAnalyser, _super);
23
25
  function PublishedDataAnalyser(marty) {
24
26
  var _this = _super.call(this) || this;
25
27
  _this.marty = marty;
28
+ _this.lastColourReading = null;
29
+ _this.colourFilter = new ColourEventFilter();
30
+ _this.colourContext = "";
31
+ _this.colourExpiry = null;
32
+ _this.connectionObserver = {
33
+ notify: function (eventType, eventEnum) {
34
+ if (eventType === "conn" && [RaftConnEvent.CONN_DISCONNECTED, RaftConnEvent.CONN_REJECTED,
35
+ RaftConnEvent.CONN_VERIFYING_CORRECT, RaftConnEvent.CONN_VERIFIED_CORRECT].includes(eventEnum)) {
36
+ _this.resetColourDetection();
37
+ }
38
+ },
39
+ };
26
40
  _this.lightSenseDetection = new LightSenseDetection();
27
41
  _this.colourSensedDetection = new ColourSensedDetection();
28
42
  _this.objectSenseDetection = new ObjectSenseDetection();
@@ -72,6 +86,9 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
72
86
  }
73
87
  PublishedDataAnalyser.prototype.subscribeToPublishedData = function () {
74
88
  var _this = this;
89
+ var _a;
90
+ (_a = this.pubSub) === null || _a === void 0 ? void 0 : _a.unsubscribe();
91
+ this.marty.subscribe(this.connectionObserver, ["conn"]);
75
92
  this.pubSub = createRaftPubSubscriptionHelper(this.marty, this.marty.id);
76
93
  this.pubSub.subscribe(function (data) {
77
94
  _this.analyse(data.stateInfo);
@@ -80,6 +97,9 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
80
97
  PublishedDataAnalyser.prototype.unsubscribeFromPublishedData = function () {
81
98
  var _a;
82
99
  (_a = this.pubSub) === null || _a === void 0 ? void 0 : _a.unsubscribe();
100
+ this.pubSub = null;
101
+ this.marty.unsubscribe(this.connectionObserver);
102
+ this.resetColourDetection();
83
103
  };
84
104
  PublishedDataAnalyser.prototype.analyse = function (data) {
85
105
  var _a;
@@ -89,8 +109,24 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
89
109
  var lightSensor = getLightSensor(data);
90
110
  var objectSensor = getObjectSensor(data);
91
111
  var noiseSensor = getNoiseSensor(data);
92
- colourSensor &&
93
- this.colourSensedDetection.detectColourSensed(getRGBFromColourSensorVals(colourSensor), this, (_a = getAddonColourSensorProfile(this.marty, colourSensor)) === null || _a === void 0 ? void 0 : _a.signal);
112
+ var powerAge = Date.now() - data.powerValidMs;
113
+ if (!colourSensor || this.marty.isVerified === false || document.hidden ||
114
+ (colourSensor.status !== undefined && !(colourSensor.status & 0x80)) ||
115
+ (data.powerValidMs > 0 && powerAge >= 0 && powerAge < 3000 && ((_a = data.power) === null || _a === void 0 ? void 0 : _a.powerStatus.power5VIsOn) === false)) {
116
+ this.resetColourDetection();
117
+ }
118
+ else {
119
+ var profile = getAddonColourSensorProfile(this.marty, colourSensor);
120
+ var profileSignal = profile === null || profile === void 0 ? void 0 : profile.signal;
121
+ var context = JSON.stringify([colourSensor.id, colourSensor.name, profile === null || profile === void 0 ? void 0 : profile.serialNumber, profile === null || profile === void 0 ? void 0 : profile.offsetsHex, profile === null || profile === void 0 ? void 0 : profile.whiteCalibrationHex, profile === null || profile === void 0 ? void 0 : profile.capturedAtMs, profileSignal === null || profileSignal === void 0 ? void 0 : profileSignal.airMax.red, profileSignal === null || profileSignal === void 0 ? void 0 : profileSignal.airMax.green, profileSignal === null || profileSignal === void 0 ? void 0 : profileSignal.airMax.blue, profileSignal === null || profileSignal === void 0 ? void 0 : profileSignal.margin.red, profileSignal === null || profileSignal === void 0 ? void 0 : profileSignal.margin.green, profileSignal === null || profileSignal === void 0 ? void 0 : profileSignal.margin.blue]);
122
+ if (context !== this.colourContext) {
123
+ this.resetColourDetection();
124
+ this.colourContext = context;
125
+ }
126
+ var values = colourChannelsFromValues(colourSensor.vals);
127
+ this.colourSensedDetection.detectColourSensed({ r: values.red, g: values.green, b: values.blue,
128
+ clear: values.clear, air: values.air }, this, profile === null || profile === void 0 ? void 0 : profile.signal, data.addOnInfoValidMs);
129
+ }
94
130
  (objectSensor || colourSensor) &&
95
131
  this.objectSenseDetection.detectObjectSense(getObjectIsTouchingFromVals(objectSensor, colourSensor), this);
96
132
  lightSensor &&
@@ -104,9 +140,42 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
104
140
  };
105
141
  PublishedDataAnalyser.prototype.setColourSensed = function (colour, confidence) {
106
142
  if (confidence === void 0) { confidence = null; }
143
+ var changed = colour !== this.martyState.colourSensed;
107
144
  this.martyState.colourSensed = colour;
108
145
  this.martyState.colourConfidence = confidence;
109
- this.emit(this.eventsMap.colourSensed[colour]);
146
+ if (changed)
147
+ this.emit(this.eventsMap.colourSensed[colour]);
148
+ };
149
+ PublishedDataAnalyser.prototype.resetColourDetection = function () {
150
+ if (this.colourExpiry !== null)
151
+ clearTimeout(this.colourExpiry);
152
+ this.colourExpiry = null;
153
+ this.colourContext = "";
154
+ this.colourFilter.reset();
155
+ this.lastColourReading = null;
156
+ this.setColourSensed("unclear", null);
157
+ };
158
+ PublishedDataAnalyser.prototype.recordColourReading = function (reading, receivedAtMs) {
159
+ this.lastColourReading = reading;
160
+ var value = this.colourFilter.update(reading, receivedAtMs, Date.now());
161
+ this.setColourSensed(value.colour, value.confidence);
162
+ this.scheduleColourExpiry();
163
+ };
164
+ PublishedDataAnalyser.prototype.scheduleColourExpiry = function () {
165
+ var _this = this;
166
+ if (this.colourExpiry !== null)
167
+ clearTimeout(this.colourExpiry);
168
+ var expiresAt = this.colourFilter.nextExpiryAtMs;
169
+ this.colourExpiry = expiresAt === null ? null : setTimeout(function () {
170
+ _this.colourExpiry = null;
171
+ if (_this.marty.isVerified === false || document.hidden) {
172
+ _this.resetColourDetection();
173
+ return;
174
+ }
175
+ var value = _this.colourFilter.current(Date.now());
176
+ _this.setColourSensed(value.colour, value.confidence);
177
+ _this.scheduleColourExpiry();
178
+ }, Math.max(0, expiresAt - Date.now()));
110
179
  };
111
180
  PublishedDataAnalyser.prototype.setLightSense = function (lightSense) {
112
181
  this.martyState.lightSense = lightSense;
@@ -125,10 +194,11 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
125
194
  var ColourSensedDetection = /** @class */ (function () {
126
195
  function ColourSensedDetection() {
127
196
  }
128
- ColourSensedDetection.prototype.detectColourSensed = function (_a, analyser, profile) {
197
+ ColourSensedDetection.prototype.detectColourSensed = function (_a, analyser, profile, receivedAtMs) {
129
198
  var r = _a.r, g = _a.g, b = _a.b, clear = _a.clear, air = _a.air;
199
+ if (receivedAtMs === void 0) { receivedAtMs = Date.now(); }
130
200
  var reading = classifyColour({ red: r, green: g, blue: b, clear: clear, air: air }, profile);
131
- return analyser.setColourSensed(reading.classifiedColour, reading.confidence);
201
+ return analyser.recordColourReading(reading, receivedAtMs);
132
202
  };
133
203
  return ColourSensedDetection;
134
204
  }());
@@ -22,6 +22,19 @@ declare const colourDiagnosticsEnglish: {
22
22
  confidenceHelp: string;
23
23
  withoutAir: string;
24
24
  firmwareAir: string;
25
+ profileSource: string;
26
+ eepromStatus: string;
27
+ eeprom: string;
28
+ local: string;
29
+ firebase: string;
30
+ defaultRules: string;
31
+ verified: string;
32
+ empty: string;
33
+ invalid: string;
34
+ readFailed: string;
35
+ writeFailed: string;
36
+ firmwareNotAudited: string;
37
+ notChecked: string;
25
38
  waiting: string;
26
39
  capture: string;
27
40
  captureAir: string;
@@ -17,6 +17,12 @@ var colourDiagnosticsEnglish = {
17
17
  confidenceHelp: "Rule strength, not probability. Higher scores mean more room before a colour or air cutoff. Air needs a saved calibration to score.",
18
18
  withoutAir: "Firmware reports air; the RGB signal matches {{result}}.",
19
19
  firmwareAir: "Firmware Air",
20
+ profileSource: "Profile: {{source}}",
21
+ eepromStatus: "EEPROM: {{status}}",
22
+ eeprom: "sensor memory", local: "local backup", firebase: "database backup", defaultRules: "default rules",
23
+ verified: "verified", empty: "no profile saved", invalid: "invalid or outdated profile",
24
+ readFailed: "read failed", writeFailed: "save failed", firmwareNotAudited: "not enabled for this firmware",
25
+ notChecked: "not checked yet",
20
26
  waiting: "Select a sensor and wait for fresh readings.",
21
27
  capture: "Take {{colour}} snapshot",
22
28
  captureAir: "Take air snapshot",
@@ -4,7 +4,7 @@ export type RGB = {
4
4
  blue: number;
5
5
  };
6
6
  export type ColourSignalProfile = {
7
- airMin: RGB;
7
+ airMin?: RGB;
8
8
  airMax: RGB;
9
9
  margin: RGB;
10
10
  };
@@ -33,7 +33,7 @@ export declare const classifyColour: (values: RGB & {
33
33
  signalSource: string;
34
34
  classificationReason: string;
35
35
  };
36
- export declare const colourChannelsFromValues: (vals: Record<string, number | boolean>) => {
36
+ export declare const colourChannelsFromValues: (vals: Record<string, unknown>) => {
37
37
  red: number;
38
38
  green: number;
39
39
  blue: number;
@@ -86,5 +86,5 @@ export var colourChannelsFromValues = function (vals) {
86
86
  values[channel.toLowerCase()] = vals[key];
87
87
  }
88
88
  return { red: values.red, green: values.green, blue: values.blue,
89
- clear: values.clear, air: [true, false, 0, 1].includes(values.air) ? Boolean(values.air) : undefined };
89
+ clear: values.clear, air: [true, false, 0, 1].some(function (value) { return value === values.air; }) ? Boolean(values.air) : undefined };
90
90
  };
@@ -0,0 +1,24 @@
1
+ import type { classifyColour, ColourName } from "./colour-classifier";
2
+ export declare const COLOUR_EVENT_TIMING: {
3
+ readonly minimumReadings: 3;
4
+ readonly confirmationMs: 200;
5
+ readonly signalGapMs: 250;
6
+ readonly staleMs: 500;
7
+ };
8
+ type Reading = Pick<ReturnType<typeof classifyColour>, "classifiedColour" | "confidence" | "classificationReason">;
9
+ type Value = {
10
+ colour: ColourName;
11
+ confidence: number | null;
12
+ };
13
+ /** Per-sensor event confirmation. Instantaneous classification remains independent. */
14
+ export declare class ColourEventFilter {
15
+ private lastPacketAtMs;
16
+ private latest;
17
+ private confirmed;
18
+ private candidate;
19
+ reset(): void;
20
+ current(now: number): Value;
21
+ update(reading: Reading, receivedAtMs: number, now: number): Value;
22
+ get nextExpiryAtMs(): number | null;
23
+ }
24
+ export {};
@@ -0,0 +1,88 @@
1
+ export var COLOUR_EVENT_TIMING = {
2
+ minimumReadings: 3,
3
+ confirmationMs: 200,
4
+ signalGapMs: 250,
5
+ staleMs: 500,
6
+ };
7
+ var isCard = function (colour) { return colour !== "air" && colour !== "unclear"; };
8
+ /** Per-sensor event confirmation. Instantaneous classification remains independent. */
9
+ var ColourEventFilter = /** @class */ (function () {
10
+ function ColourEventFilter() {
11
+ this.lastPacketAtMs = null;
12
+ this.latest = null;
13
+ this.confirmed = null;
14
+ this.candidate = null;
15
+ }
16
+ ColourEventFilter.prototype.reset = function () {
17
+ this.lastPacketAtMs = null;
18
+ this.latest = this.confirmed = this.candidate = null;
19
+ };
20
+ ColourEventFilter.prototype.current = function (now) {
21
+ var _a, _b;
22
+ if (this.lastPacketAtMs !== null && now - this.lastPacketAtMs >= COLOUR_EVENT_TIMING.staleMs) {
23
+ // Retain the packet watermark: repeated cached packets are still duplicates.
24
+ this.latest = this.confirmed = this.candidate = null;
25
+ }
26
+ if (this.confirmed && now - this.confirmed.lastAtMs >= COLOUR_EVENT_TIMING.signalGapMs)
27
+ this.confirmed = null;
28
+ if (this.candidate && now - this.candidate.lastAtMs >= COLOUR_EVENT_TIMING.signalGapMs)
29
+ this.candidate = null;
30
+ return ((_a = this.confirmed) === null || _a === void 0 ? void 0 : _a.value) || (((_b = this.latest) === null || _b === void 0 ? void 0 : _b.colour) === "air" ? this.latest :
31
+ { colour: "unclear", confidence: this.latest ? 0 : null });
32
+ };
33
+ ColourEventFilter.prototype.update = function (reading, receivedAtMs, now) {
34
+ var _a, _b;
35
+ this.current(now);
36
+ if (!Number.isFinite(receivedAtMs) || receivedAtMs <= 0 || receivedAtMs > now ||
37
+ now - receivedAtMs >= COLOUR_EVENT_TIMING.staleMs) {
38
+ this.reset();
39
+ return this.current(now);
40
+ }
41
+ // A STATE_INFO event can reuse the same add-on packet when other telemetry changes.
42
+ if (this.lastPacketAtMs !== null && receivedAtMs <= this.lastPacketAtMs)
43
+ return this.current(now);
44
+ this.lastPacketAtMs = receivedAtMs;
45
+ var value = { colour: reading.classifiedColour, confidence: reading.confidence };
46
+ this.latest = value;
47
+ if (reading.classificationReason === "invalid") {
48
+ this.confirmed = this.candidate = null;
49
+ }
50
+ else if (isCard(value.colour)) {
51
+ if (((_a = this.confirmed) === null || _a === void 0 ? void 0 : _a.value.colour) === value.colour) {
52
+ this.confirmed.value = value;
53
+ this.confirmed.lastAtMs = receivedAtMs;
54
+ this.candidate = null;
55
+ }
56
+ else {
57
+ if (((_b = this.candidate) === null || _b === void 0 ? void 0 : _b.value.colour) === value.colour) {
58
+ this.candidate.count++;
59
+ this.candidate.lastAtMs = receivedAtMs;
60
+ this.candidate.value = value;
61
+ }
62
+ else {
63
+ this.candidate = { value: value, firstAtMs: receivedAtMs, lastAtMs: receivedAtMs, count: 1 };
64
+ }
65
+ if (this.candidate.count >= COLOUR_EVENT_TIMING.minimumReadings &&
66
+ receivedAtMs - this.candidate.firstAtMs >= COLOUR_EVENT_TIMING.confirmationMs) {
67
+ this.confirmed = this.candidate;
68
+ this.candidate = null;
69
+ }
70
+ }
71
+ }
72
+ return this.current(now);
73
+ };
74
+ Object.defineProperty(ColourEventFilter.prototype, "nextExpiryAtMs", {
75
+ get: function () {
76
+ var deadlines = [
77
+ this.confirmed && this.confirmed.lastAtMs + COLOUR_EVENT_TIMING.signalGapMs,
78
+ this.candidate && this.candidate.lastAtMs + COLOUR_EVENT_TIMING.signalGapMs,
79
+ this.latest && this.lastPacketAtMs + COLOUR_EVENT_TIMING.staleMs,
80
+ ].filter(function (value) { return value !== null; });
81
+ return deadlines.length ? Math.min.apply(Math, deadlines) : null;
82
+ },
83
+ enumerable: false,
84
+ configurable: true
85
+ });
86
+ return ColourEventFilter;
87
+ }());
88
+ export { ColourEventFilter };
@@ -0,0 +1,36 @@
1
+ import type { ColourSignalProfile } from "./colour-classifier";
2
+ export declare const COLOUR_PROFILE_EEPROM_ADDRESS: "401c";
3
+ export declare const COLOUR_PROFILE_EEPROM_FORMAT = 1;
4
+ export type ColourProfileIdentity = {
5
+ serialNumber: string;
6
+ typeCode: string;
7
+ firmwareVersion: string;
8
+ offsetsHex: string;
9
+ whiteCalibrationHex: string;
10
+ };
11
+ export type ColourProfileStorageEvidence = {
12
+ source: "eeprom" | "local" | "firebase" | "defaultRules";
13
+ eeprom: {
14
+ status: "verified" | "empty" | "invalid" | "readFailed" | "writeFailed" | "firmwareNotAudited" | "notChecked";
15
+ address: typeof COLOUR_PROFILE_EEPROM_ADDRESS;
16
+ formatVersion: number;
17
+ hex?: string;
18
+ reason?: string;
19
+ checkedAtMs?: number;
20
+ writeAttempted?: boolean;
21
+ readbackMatches?: boolean;
22
+ };
23
+ };
24
+ export declare const supportsColourProfileEEPROM: (hardware: {
25
+ typeCode: string | null;
26
+ firmwareVersion: string | null;
27
+ }) => boolean;
28
+ /** Format 1: version(2), R/G/B air excursions(6 each), coordinate checksum(12). Big endian. */
29
+ export declare const encodeColourProfileEEPROM: (identity: ColourProfileIdentity, signal: ColourSignalProfile) => string;
30
+ export declare const decodeColourProfileEEPROM: (hex: string, identity: ColourProfileIdentity) => {
31
+ status: "verified";
32
+ signal: ColourSignalProfile;
33
+ } | {
34
+ status: "empty" | "invalid";
35
+ reason: string;
36
+ };
@@ -0,0 +1,88 @@
1
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
2
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
3
+ if (ar || !(i in from)) {
4
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
5
+ ar[i] = from[i];
6
+ }
7
+ }
8
+ return to.concat(ar || Array.prototype.slice.call(from));
9
+ };
10
+ // The documented four-byte gap between air offsets and the white reference.
11
+ export var COLOUR_PROFILE_EEPROM_ADDRESS = "401c";
12
+ export var COLOUR_PROFILE_EEPROM_FORMAT = 1;
13
+ var RAW_NOISE_MARGIN = 3;
14
+ var CHANNELS = ["red", "green", "blue"];
15
+ // 1.4.1 retains its existing calibration path, but its source has not been audited for this new allocation.
16
+ export var supportsColourProfileEEPROM = function (hardware) {
17
+ return (hardware.typeCode === "97" && hardware.firmwareVersion === "1.3.1") ||
18
+ (hardware.typeCode === "98" && hardware.firmwareVersion === "1.4.0");
19
+ };
20
+ var coordinates = function (identity) {
21
+ if (!supportsColourProfileEEPROM(identity) || !/^[a-zA-Z0-9_-]{1,128}$/.test(identity.serialNumber) ||
22
+ !/^[a-f0-9]{8}$/i.test(identity.offsetsHex) || !/^[a-f0-9]{16}$/i.test(identity.whiteCalibrationHex)) {
23
+ throw new Error("unsupportedProfileCoordinates");
24
+ }
25
+ var offsets = CHANNELS.map(function (_, i) { return parseInt(identity.offsetsHex.slice((i + 1) * 2, (i + 2) * 2), 16); });
26
+ var white = CHANNELS.map(function (_, i) { return parseInt(identity.whiteCalibrationHex.slice((i + 1) * 4, (i + 2) * 4), 16); });
27
+ if (white.some(function (value) { return value < 10 || value > 255; }))
28
+ throw new Error("invalidWhiteReference");
29
+ return { offsets: offsets, white: white };
30
+ };
31
+ // CRC-12 polynomial 0x80f, MSB-first, initial value zero, no reflection or final XOR.
32
+ var checksum = function (payload, identity) {
33
+ var bytes = __spreadArray(__spreadArray(__spreadArray(__spreadArray([payload >>> 16, (payload >>> 8) & 255, payload & 255], Array.from((identity.offsetsHex + identity.whiteCalibrationHex).match(/../g), function (byte) { return parseInt(byte, 16); }), true), [Number(identity.typeCode)], false), identity.firmwareVersion.split(".").map(Number), true), Array.from(identity.serialNumber, function (character) { return character.charCodeAt(0); }), true);
34
+ var crc = 0;
35
+ for (var _i = 0, bytes_1 = bytes; _i < bytes_1.length; _i++) {
36
+ var byte = bytes_1[_i];
37
+ for (var bit = 7; bit >= 0; bit--) {
38
+ var feedback = ((crc >>> 11) ^ (byte >>> bit)) & 1;
39
+ crc = ((crc << 1) & 0xfff) ^ (feedback ? 0x80f : 0);
40
+ }
41
+ }
42
+ return crc;
43
+ };
44
+ /** Format 1: version(2), R/G/B air excursions(6 each), coordinate checksum(12). Big endian. */
45
+ export var encodeColourProfileEEPROM = function (identity, signal) {
46
+ var _a = coordinates(identity), offsets = _a.offsets, white = _a.white;
47
+ var excursions = CHANNELS.map(function (channel, i) {
48
+ var rawMax = signal.airMax[channel] * white[i] / 255;
49
+ var margin = signal.margin[channel] * white[i] / 255;
50
+ var excursion = Math.round(rawMax) - (identity.typeCode === "97" ? offsets[i] : 0);
51
+ // Calibration's steadiness check guarantees this range; never silently quantise arbitrary legacy profiles.
52
+ if (!Number.isFinite(rawMax) || Math.abs(rawMax - Math.round(rawMax)) > 1e-7 ||
53
+ !Number.isFinite(margin) || Math.abs(margin - RAW_NOISE_MARGIN) > 1e-7 ||
54
+ excursion < 0 || excursion > 63 || excursion + offsets[i] >= 255 || excursion > Math.max(4, offsets[i] * 0.15)) {
55
+ throw new Error("profileNotRepresentable");
56
+ }
57
+ return excursion;
58
+ });
59
+ var payload = COLOUR_PROFILE_EEPROM_FORMAT * Math.pow(2, 18) + excursions[0] * Math.pow(2, 12) + excursions[1] * Math.pow(2, 6) + excursions[2];
60
+ return (payload * Math.pow(2, 12) + checksum(payload, identity)).toString(16).padStart(8, "0");
61
+ };
62
+ export var decodeColourProfileEEPROM = function (hex, identity) {
63
+ if (!/^[a-f0-9]{8}$/i.test(hex))
64
+ return { status: "invalid", reason: "invalidLength" };
65
+ if (["00000000", "ffffffff"].includes(hex.toLowerCase()))
66
+ return { status: "empty", reason: "noProfile" };
67
+ try {
68
+ var _a = coordinates(identity), offsets_1 = _a.offsets, white_1 = _a.white;
69
+ var word = parseInt(hex, 16);
70
+ var payload = Math.floor(word / Math.pow(2, 12));
71
+ if (Math.floor(payload / Math.pow(2, 18)) !== COLOUR_PROFILE_EEPROM_FORMAT)
72
+ return { status: "invalid", reason: "unknownFormat" };
73
+ if (word % Math.pow(2, 12) !== checksum(payload, identity))
74
+ return { status: "invalid", reason: "checksumOrCalibrationMismatch" };
75
+ var excursions_1 = [Math.floor(payload / Math.pow(2, 12)) % 64, Math.floor(payload / Math.pow(2, 6)) % 64, payload % 64];
76
+ if (excursions_1.some(function (value, i) { return value + offsets_1[i] >= 255 || value > Math.max(4, offsets_1[i] * 0.15); }))
77
+ return { status: "invalid", reason: "invalidAirExcursion" };
78
+ var airMax = Object.fromEntries(CHANNELS.map(function (channel, i) {
79
+ return [channel, (excursions_1[i] + (identity.typeCode === "97" ? offsets_1[i] : 0)) * 255 / white_1[i]];
80
+ }));
81
+ var margin = Object.fromEntries(CHANNELS.map(function (channel, i) { return [channel, RAW_NOISE_MARGIN * 255 / white_1[i]]; }));
82
+ // Measured air minima and capture history remain in diagnostics; classification needs only max + margin.
83
+ return { status: "verified", signal: { airMax: airMax, margin: margin } };
84
+ }
85
+ catch (_b) {
86
+ return { status: "invalid", reason: "invalidCoordinates" };
87
+ }
88
+ };
@@ -1,6 +1,7 @@
1
1
  import colourPolicy from "./marty-colour-policy.json";
2
2
  import { ColourConfidenceFactors, ColourSignalProfile } from "./colour-classifier";
3
- import { ColourSensorProfile } from "./colour-sensor-profile";
3
+ import { ActiveColourSensorProfile } from "./colour-sensor-profile";
4
+ import { ColourProfileStorageEvidence } from "./colour-profile-eeprom";
4
5
  import type { RaftObserver } from "../application/RAFTs/RaftObserver";
5
6
  import type { RestMessageOptions } from "../types/communication-between-apps/wrapper-communication";
6
7
  import { SensorRead, RawColourFrame, ColourSensorSetup } from "./colour-sensor-registers";
@@ -94,7 +95,8 @@ export type ColourSnapshot = {
94
95
  airFlagTakesPrecedence: boolean;
95
96
  boundaries: typeof BLOCKS_COLOUR_BOUNDARIES;
96
97
  signal?: typeof colourPolicy.signal;
97
- profile?: ColourSensorProfile;
98
+ profile?: ActiveColourSensorProfile;
99
+ profileStorage?: ColourProfileStorageEvidence;
98
100
  clearBoundsUsedForClassification?: boolean;
99
101
  confidence?: typeof colourPolicy.confidence;
100
102
  };
@@ -72,7 +72,8 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
72
72
  import packageInfo from "../../package.json";
73
73
  import colourPolicy from "./marty-colour-policy.json";
74
74
  import { classifyColour, colourChannelsFromValues } from "./colour-classifier";
75
- import { getActiveColourSensorProfile } from "./colour-sensor-profile";
75
+ import { getActiveColourSensorProfile, hydrateColourSensorProfile, clearActiveColourSensorProfile } from "./colour-sensor-profile";
76
+ import { COLOUR_PROFILE_EEPROM_ADDRESS, COLOUR_PROFILE_EEPROM_FORMAT } from "./colour-profile-eeprom";
76
77
  import { lockColourSensorOperation, waitForColourProfileRead } from "./colour-sensor-operation";
77
78
  import { getPendingColourCalibration } from "./colour-calibration-reconnect";
78
79
  import { collectColourSensorSetup, readRawColourSensor } from "./colour-sensor-registers";
@@ -165,7 +166,7 @@ var delay = function (ms, signal) { return new Promise(function (resolve, reject
165
166
  abort();
166
167
  }); };
167
168
  var captureUnlocked = function (sensor, expectedColour, session, signal) { return __awaiter(void 0, void 0, void 0, function () {
168
- var startedAtMs, checkActive, sensorSetup, sampleWindowStartedAtMs, samples, rawSamples, rawController, abortRaw, rawFailure, collectRaw, rawCapture, index, reading;
169
+ var startedAtMs, checkActive, sensorSetup, profileStorage, hardware, sampleWindowStartedAtMs, samples, rawSamples, rawController, abortRaw, rawFailure, collectRaw, rawCapture, index, reading;
169
170
  var _a, _b, _c, _d, _e, _f;
170
171
  return __generator(this, function (_g) {
171
172
  switch (_g.label) {
@@ -187,6 +188,21 @@ var captureUnlocked = function (sensor, expectedColour, session, signal) { retur
187
188
  throw new ColourDiagnosticError("sensorPowerFailed");
188
189
  if (sensorSetup.hardware.status === "ok" && sensorSetup.hardware.value.commsOk === 0)
189
190
  throw new ColourDiagnosticError("sensorOffline");
191
+ profileStorage = { source: "defaultRules", eeprom: {
192
+ status: "notChecked", address: COLOUR_PROFILE_EEPROM_ADDRESS, formatVersion: COLOUR_PROFILE_EEPROM_FORMAT,
193
+ reason: "sensorDetailsUnavailable",
194
+ } };
195
+ clearActiveColourSensorProfile(sensor);
196
+ if (!(sensorSetup.hardware.status === "ok" && sensorSetup.offsets.status === "ok" && sensorSetup.whiteCalibration.status === "ok")) return [3 /*break*/, 3];
197
+ hardware = sensorSetup.hardware.value;
198
+ if (!(hardware.serialNumber && hardware.typeCode && hardware.firmwareVersion)) return [3 /*break*/, 3];
199
+ return [4 /*yield*/, hydrateColourSensorProfile(sensor, { serialNumber: hardware.serialNumber,
200
+ typeCode: hardware.typeCode, firmwareVersion: hardware.firmwareVersion,
201
+ offsetsHex: sensorSetup.offsets.value.hexRd, whiteCalibrationHex: sensorSetup.whiteCalibration.value.hexRd }, signal, checkActive)];
202
+ case 2:
203
+ profileStorage = _g.sent();
204
+ _g.label = 3;
205
+ case 3:
190
206
  readColourSensor(sensor);
191
207
  sampleWindowStartedAtMs = Date.now();
192
208
  samples = [];
@@ -228,42 +244,42 @@ var captureUnlocked = function (sensor, expectedColour, session, signal) { retur
228
244
  });
229
245
  }); };
230
246
  rawCapture = collectRaw();
231
- _g.label = 2;
232
- case 2:
233
- _g.trys.push([2, , 7, 9]);
247
+ _g.label = 4;
248
+ case 4:
249
+ _g.trys.push([4, , 9, 11]);
234
250
  index = 0;
235
- _g.label = 3;
236
- case 3:
237
- if (!(index <= CAPTURE_DURATION_MS / CAPTURE_INTERVAL_MS)) return [3 /*break*/, 6];
251
+ _g.label = 5;
252
+ case 5:
253
+ if (!(index <= CAPTURE_DURATION_MS / CAPTURE_INTERVAL_MS)) return [3 /*break*/, 8];
238
254
  checkActive();
239
255
  if (rawFailure)
240
256
  throw rawFailure;
241
257
  reading = readColourSensor(sensor);
242
258
  if (!samples.length || samples[samples.length - 1].receivedAtMs !== reading.receivedAtMs)
243
259
  samples.push(reading);
244
- if (!(index < CAPTURE_DURATION_MS / CAPTURE_INTERVAL_MS)) return [3 /*break*/, 5];
260
+ if (!(index < CAPTURE_DURATION_MS / CAPTURE_INTERVAL_MS)) return [3 /*break*/, 7];
245
261
  return [4 /*yield*/, delay(CAPTURE_INTERVAL_MS, signal)];
246
- case 4:
262
+ case 6:
247
263
  _g.sent();
248
- _g.label = 5;
249
- case 5:
250
- index++;
251
- return [3 /*break*/, 3];
252
- case 6: return [3 /*break*/, 9];
264
+ _g.label = 7;
253
265
  case 7:
266
+ index++;
267
+ return [3 /*break*/, 5];
268
+ case 8: return [3 /*break*/, 11];
269
+ case 9:
254
270
  rawController.abort();
255
271
  signal.removeEventListener("abort", abortRaw);
256
272
  return [4 /*yield*/, rawCapture];
257
- case 8:
273
+ case 10:
258
274
  _g.sent();
259
275
  return [7 /*endfinally*/];
260
- case 9:
276
+ case 11:
261
277
  if (rawFailure)
262
278
  throw rawFailure;
263
279
  if (samples.length < 2)
264
280
  throw new ColourDiagnosticError("staleReading");
265
281
  return [2 /*return*/, __assign(__assign({ schemaVersion: 2, sessionId: session.sessionId, sessionStartedAtMs: session.startedAtMs, captureId: createId(), expectedColour: expectedColour, startedAtMs: startedAtMs, finishedAtMs: Date.now(), appVersion: packageInfo.version, classifier: { source: colourPolicy.source, airFlagTakesPrecedence: false, boundaries: BLOCKS_COLOUR_BOUNDARIES,
266
- signal: colourPolicy.signal, profile: getActiveColourSensorProfile(sensor), clearBoundsUsedForClassification: false,
282
+ signal: colourPolicy.signal, profile: getActiveColourSensorProfile(sensor), profileStorage: profileStorage, clearBoundsUsedForClassification: false,
267
283
  confidence: colourPolicy.confidence }, device: {
268
284
  id: sensor.raftId, type: sensor.raft.type, name: ((_c = (_b = sensor.raft).getFriendlyName) === null || _c === void 0 ? void 0 : _c.call(_b)) || sensor.raftId,
269
285
  firmwareVersion: ((_d = sensor.raft.systemInfo) === null || _d === void 0 ? void 0 : _d.SystemVersion) || null, connectionMethod: sensor.raft.connectionMethod || null,
@@ -1,5 +1,6 @@
1
1
  import type { ColourSensor } from "./colour-sensor-diagnostics";
2
2
  import { ColourSignalProfile } from "./colour-classifier";
3
+ import { ColourProfileIdentity, ColourProfileStorageEvidence } from "./colour-profile-eeprom";
3
4
  export type ColourSensorProfile = {
4
5
  schemaVersion: 1;
5
6
  serialNumber: string;
@@ -12,21 +13,26 @@ export type ColourSensorProfile = {
12
13
  purpleIncluded: boolean;
13
14
  groundDetection: "clearThreshold" | "rgbFallback" | "purpleSkipped";
14
15
  };
16
+ export type ActiveColourSensorProfile = Omit<ColourSensorProfile, "capturedAtMs" | "purpleIncluded" | "groundDetection"> & Partial<Pick<ColourSensorProfile, "capturedAtMs" | "purpleIncluded" | "groundDetection">>;
15
17
  export declare const validColourSensorProfile: (value: unknown) => value is ColourSensorProfile;
16
18
  export declare const cacheColourSensorProfile: (profile: ColourSensorProfile) => void;
17
19
  export declare const colourSensorProfilePath: (profile: ColourSensorProfile) => string;
18
- export declare const bindColourSensorProfile: (sensor: ColourSensor, profile: ColourSensorProfile) => void;
19
- export declare const getActiveColourSensorProfile: (sensor: Pick<ColourSensor, "raft" | "key">) => ColourSensorProfile | undefined;
20
+ export declare const bindColourSensorProfile: (sensor: ColourSensor, profile: ActiveColourSensorProfile) => void;
21
+ export declare const getActiveColourSensorProfile: (sensor: Pick<ColourSensor, "raft" | "key">) => ActiveColourSensorProfile | undefined;
20
22
  export declare const getAddonColourSensorProfile: (raft: object, addon: {
21
23
  id: number;
22
24
  name: string;
23
- }) => ColourSensorProfile | undefined;
24
- export declare const clearActiveColourSensorProfile: (sensor: Pick<ColourSensor, "raft" | "key">) => boolean | undefined;
25
+ }) => ActiveColourSensorProfile | undefined;
26
+ export declare const getColourProfileStorage: (sensor: Pick<ColourSensor, "raft" | "key">) => ColourProfileStorageEvidence | undefined;
27
+ export declare const setColourProfileStorage: (sensor: Pick<ColourSensor, "raft" | "key">, evidence: ColourProfileStorageEvidence) => void;
28
+ export declare const clearActiveColourSensorProfile: (sensor: Pick<ColourSensor, "raft" | "key">) => void;
25
29
  export declare const classifyColourAddon: (addon: {
26
30
  id: number;
27
31
  name: string;
28
32
  vals: Record<string, number | boolean>;
29
33
  }) => import("./colour-classifier").ColourName;
34
+ /** Caller owns the raw-command lock and has freshly read hardware, offsets and white references. */
35
+ export declare const hydrateColourSensorProfile: (sensor: ColourSensor, identity: ColourProfileIdentity, signal: AbortSignal, check: () => void) => Promise<ColourProfileStorageEvidence>;
30
36
  export declare const loadColourSensorProfile: (sensor: ColourSensor, signal: AbortSignal) => Promise<void>;
31
37
  /** Read-only browser/WebView hydration. Never wakes sleeping sensors or modifies their registers. */
32
38
  export declare const startColourSensorProfileSync: () => () => void;
@@ -1,3 +1,14 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
1
12
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
13
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
14
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -39,9 +50,11 @@ import { classifyColour, colourChannelsFromValues } from "./colour-classifier";
39
50
  import { lockColourSensorOperation } from "./colour-sensor-operation";
40
51
  import { readColourSensorHardware, readColourSensorReadiness, readColourSensorRegister } from "./colour-sensor-registers";
41
52
  import { getPendingColourCalibration } from "./colour-calibration-reconnect";
53
+ import { COLOUR_PROFILE_EEPROM_ADDRESS, COLOUR_PROFILE_EEPROM_FORMAT, decodeColourProfileEEPROM, supportsColourProfileEEPROM } from "./colour-profile-eeprom";
42
54
  var PREFIX = "marty-colour-signal-profile:";
43
55
  var memory = new Map();
44
56
  var active = new WeakMap();
57
+ var storage = new WeakMap();
45
58
  var retries = new WeakMap();
46
59
  var profileKey = function (profile) {
47
60
  return "".concat(profile.serialNumber, "/").concat(profile.offsetsHex).concat(profile.whiteCalibrationHex);
@@ -57,7 +70,7 @@ export var validColourSensorProfile = function (value) {
57
70
  var _a, _b, _c, _d, _e, _f;
58
71
  var c = channel;
59
72
  return [(_b = (_a = profile.signal) === null || _a === void 0 ? void 0 : _a.airMin) === null || _b === void 0 ? void 0 : _b[c], (_d = (_c = profile.signal) === null || _c === void 0 ? void 0 : _c.airMax) === null || _d === void 0 ? void 0 : _d[c], (_f = (_e = profile.signal) === null || _e === void 0 ? void 0 : _e.margin) === null || _f === void 0 ? void 0 : _f[c]]
60
- .every(function (v) { return Number.isFinite(v) && v >= 0; }) && profile.signal.airMin[c] <= profile.signal.airMax[c] && profile.signal.margin[c] > 0;
73
+ .every(function (v) { return typeof v === "number" && Number.isFinite(v) && v >= 0; }) && profile.signal.airMin[c] <= profile.signal.airMax[c] && profile.signal.margin[c] > 0;
61
74
  });
62
75
  };
63
76
  export var cacheColourSensorProfile = function (profile) {
@@ -82,7 +95,20 @@ export var bindColourSensorProfile = function (sensor, profile) {
82
95
  };
83
96
  export var getActiveColourSensorProfile = function (sensor) { var _a; return (_a = active.get(sensor.raft)) === null || _a === void 0 ? void 0 : _a.get(sensor.key); };
84
97
  export var getAddonColourSensorProfile = function (raft, addon) { var _a; return (_a = active.get(raft)) === null || _a === void 0 ? void 0 : _a.get(JSON.stringify([addon.id, addon.name])); };
85
- export var clearActiveColourSensorProfile = function (sensor) { var _a; return (_a = active.get(sensor.raft)) === null || _a === void 0 ? void 0 : _a.delete(sensor.key); };
98
+ export var getColourProfileStorage = function (sensor) { var _a; return (_a = storage.get(sensor.raft)) === null || _a === void 0 ? void 0 : _a.get(sensor.key); };
99
+ export var setColourProfileStorage = function (sensor, evidence) {
100
+ var entries = storage.get(sensor.raft);
101
+ if (!entries) {
102
+ entries = new Map();
103
+ storage.set(sensor.raft, entries);
104
+ }
105
+ entries.set(sensor.key, evidence);
106
+ };
107
+ export var clearActiveColourSensorProfile = function (sensor) {
108
+ var _a, _b;
109
+ (_a = active.get(sensor.raft)) === null || _a === void 0 ? void 0 : _a.delete(sensor.key);
110
+ (_b = storage.get(sensor.raft)) === null || _b === void 0 ? void 0 : _b.delete(sensor.key);
111
+ };
86
112
  export var classifyColourAddon = function (addon) {
87
113
  var _a;
88
114
  var raft = Object.values(window.applicationManager.connectedRafts).find(function (candidate) {
@@ -91,96 +117,156 @@ export var classifyColourAddon = function (addon) {
91
117
  });
92
118
  return classifyColour(colourChannelsFromValues(addon.vals), raft ? (_a = getAddonColourSensorProfile(raft, addon)) === null || _a === void 0 ? void 0 : _a.signal : undefined).classifiedColour;
93
119
  };
94
- // Check physical identity and the complete saved coordinate system before using a persisted profile.
95
- export var loadColourSensorProfile = function (sensor, signal) { return __awaiter(void 0, void 0, void 0, function () {
96
- var release, check, readiness, hardware, white, offsets, identity, key_1, profile, stored, controller_1, abort, timeout, response, records, _a, state, current;
120
+ /** Caller owns the raw-command lock and has freshly read hardware, offsets and white references. */
121
+ export var hydrateColourSensorProfile = function (sensor, identity, signal, check) { return __awaiter(void 0, void 0, void 0, function () {
122
+ var evidence, profile, result, decoded, key_1, cached, stored, controller_1, abort, timeout, response, records, _a, hardware, unavailable, state, current;
97
123
  var _b;
98
124
  return __generator(this, function (_c) {
99
125
  switch (_c.label) {
100
126
  case 0:
101
- release = lockColourSensorOperation(sensor.raft, true);
102
- if (!release)
103
- return [2 /*return*/];
104
- check = function () { return assertColourSensorActive(sensor, signal); };
105
- _c.label = 1;
106
- case 1:
107
- _c.trys.push([1, , 13, 14]);
108
127
  check();
109
128
  clearActiveColourSensorProfile(sensor);
110
- return [4 /*yield*/, readColourSensorReadiness(sensor, signal, check)];
129
+ evidence = { source: "defaultRules", eeprom: {
130
+ status: "firmwareNotAudited", address: COLOUR_PROFILE_EEPROM_ADDRESS, formatVersion: COLOUR_PROFILE_EEPROM_FORMAT,
131
+ } };
132
+ if (!supportsColourProfileEEPROM(identity)) return [3 /*break*/, 2];
133
+ return [4 /*yield*/, readColourSensorRegister(sensor, COLOUR_PROFILE_EEPROM_ADDRESS, signal, check)];
134
+ case 1:
135
+ result = _c.sent();
136
+ evidence.eeprom.checkedAtMs = result.receivedAtMs;
137
+ if (result.status === "ok") {
138
+ decoded = decodeColourProfileEEPROM(result.value, identity);
139
+ evidence.eeprom = __assign(__assign(__assign({}, evidence.eeprom), { status: decoded.status, hex: result.value }), (decoded.status === "verified" ? { readbackMatches: true } : { reason: decoded.reason }));
140
+ if (decoded.status === "verified") {
141
+ profile = __assign(__assign({ schemaVersion: 1 }, identity), { signal: decoded.signal });
142
+ evidence.source = "eeprom";
143
+ }
144
+ }
145
+ else {
146
+ evidence.eeprom.status = "readFailed";
147
+ evidence.eeprom.reason = result.reason;
148
+ }
149
+ _c.label = 2;
111
150
  case 2:
112
- readiness = _c.sent();
113
- if (readiness.status !== "ok" || !readiness.value)
114
- return [2 /*return*/];
115
- return [4 /*yield*/, readColourSensorHardware(sensor, signal, check)];
116
- case 3:
117
- hardware = _c.sent();
118
- if (hardware.status !== "ok" || hardware.value.commsOk === 0 || !hardware.value.serialNumber ||
119
- !/^[a-zA-Z0-9_-]{1,128}$/.test(hardware.value.serialNumber))
120
- return [2 /*return*/];
121
- return [4 /*yield*/, readColourSensorRegister(sensor, "4020", signal, check)];
122
- case 4:
123
- white = _c.sent();
124
- if (white.status !== "ok")
125
- return [2 /*return*/];
126
- return [4 /*yield*/, readColourSensorRegister(sensor, "4018", signal, check)];
127
- case 5:
128
- offsets = _c.sent();
129
- if (offsets.status !== "ok")
130
- return [2 /*return*/];
131
- identity = { serialNumber: hardware.value.serialNumber, offsetsHex: offsets.value, whiteCalibrationHex: white.value };
151
+ if (!!profile) return [3 /*break*/, 10];
132
152
  key_1 = profileKey(identity);
133
- profile = memory.get(key_1);
153
+ cached = memory.get(key_1);
134
154
  try {
135
155
  stored = JSON.parse(localStorage.getItem(PREFIX + key_1) || "null");
136
- if (validColourSensorProfile(stored))
137
- profile = stored;
156
+ if (validColourSensorProfile(stored) && profileKey(stored) === key_1)
157
+ cached = stored;
138
158
  }
139
159
  catch ( /* Native WebViews may have temporary or unavailable storage. */_d) { /* Native WebViews may have temporary or unavailable storage. */ }
140
- if (!!profile) return [3 /*break*/, 12];
160
+ if (!(cached && profileKey(cached) === key_1 && cached.typeCode === identity.typeCode && cached.firmwareVersion === identity.firmwareVersion)) return [3 /*break*/, 3];
161
+ profile = cached;
162
+ evidence.source = "local";
163
+ return [3 /*break*/, 10];
164
+ case 3:
141
165
  controller_1 = new AbortController();
142
166
  abort = function () { return controller_1.abort(); };
143
167
  signal.addEventListener("abort", abort, { once: true });
168
+ if (signal.aborted)
169
+ abort();
144
170
  timeout = setTimeout(abort, 5000);
145
- _c.label = 6;
146
- case 6:
147
- _c.trys.push([6, 10, 11, 12]);
171
+ _c.label = 4;
172
+ case 4:
173
+ _c.trys.push([4, 8, 9, 10]);
148
174
  return [4 /*yield*/, fetch("".concat(COLOUR_DATABASE_URL, "/colourSensorProfiles/").concat(key_1, ".json"), { signal: controller_1.signal })];
149
- case 7:
175
+ case 5:
150
176
  response = _c.sent();
151
- if (!response.ok) return [3 /*break*/, 9];
177
+ if (!response.ok) return [3 /*break*/, 7];
152
178
  return [4 /*yield*/, response.json()];
153
- case 8:
179
+ case 6:
154
180
  records = _c.sent();
155
181
  if (records && typeof records === "object")
156
182
  profile = Object.values(records).filter(validColourSensorProfile)
157
- .filter(function (value) { return profileKey(value) === key_1; }).sort(function (a, b) { return b.capturedAtMs - a.capturedAtMs; })[0];
158
- _c.label = 9;
159
- case 9: return [3 /*break*/, 12];
160
- case 10:
183
+ .filter(function (value) { return profileKey(value) === key_1 && value.typeCode === identity.typeCode && value.firmwareVersion === identity.firmwareVersion; })
184
+ .sort(function (a, b) { return b.capturedAtMs - a.capturedAtMs; })[0];
185
+ if (profile) {
186
+ evidence.source = "firebase";
187
+ cacheColourSensorProfile(profile);
188
+ }
189
+ _c.label = 7;
190
+ case 7: return [3 /*break*/, 10];
191
+ case 8:
161
192
  _a = _c.sent();
162
- return [3 /*break*/, 12];
163
- case 11:
193
+ return [3 /*break*/, 10];
194
+ case 9:
164
195
  clearTimeout(timeout);
165
196
  signal.removeEventListener("abort", abort);
166
197
  return [7 /*endfinally*/];
167
- case 12:
198
+ case 10:
168
199
  check();
200
+ return [4 /*yield*/, readColourSensorHardware(sensor, signal, check)];
201
+ case 11:
202
+ hardware = _c.sent();
203
+ if (hardware.status !== "ok" || hardware.value.commsOk === 0 || hardware.value.serialNumber !== identity.serialNumber ||
204
+ hardware.value.typeCode !== identity.typeCode || hardware.value.firmwareVersion !== identity.firmwareVersion) {
205
+ unavailable = __assign(__assign({}, evidence), { source: "defaultRules", eeprom: __assign(__assign({}, evidence.eeprom), { status: "notChecked", reason: "sensorIdentityChangedOrUnavailable", readbackMatches: false }) });
206
+ setColourProfileStorage(sensor, unavailable);
207
+ return [2 /*return*/, unavailable];
208
+ }
169
209
  state = sensor.raft.raftStateInfo;
170
210
  current = state.addOnInfo.addons.find(function (addon) { return addon.id === sensor.addon.id && addon.name === sensor.addon.name; });
171
211
  if (!current || (current.status !== undefined && !(current.status & 128)) ||
172
- Date.now() - state.addOnInfoValidMs > 1500 || state.addOnInfoValidMs > Date.now() || ((_b = state.power) === null || _b === void 0 ? void 0 : _b.powerStatus.power5VIsOn) === false)
173
- return [2 /*return*/];
174
- if (profile && profileKey(profile) === key_1 && profile.typeCode === hardware.value.typeCode &&
175
- profile.firmwareVersion === hardware.value.firmwareVersion) {
176
- cacheColourSensorProfile(profile);
177
- bindColourSensorProfile(sensor, profile);
212
+ Date.now() - state.addOnInfoValidMs > 1500 || state.addOnInfoValidMs > Date.now() || ((_b = state.power) === null || _b === void 0 ? void 0 : _b.powerStatus.power5VIsOn) === false) {
213
+ return [2 /*return*/, __assign(__assign({}, evidence), { source: "defaultRules", eeprom: __assign(__assign({}, evidence.eeprom), { status: "notChecked", reason: "sensorNotFresh" }) })];
178
214
  }
179
- return [3 /*break*/, 14];
180
- case 13:
215
+ if (profile)
216
+ bindColourSensorProfile(sensor, profile);
217
+ setColourProfileStorage(sensor, evidence);
218
+ return [2 /*return*/, evidence];
219
+ }
220
+ });
221
+ }); };
222
+ // Check physical identity and the complete saved coordinate system before using a persisted profile.
223
+ export var loadColourSensorProfile = function (sensor, signal) { return __awaiter(void 0, void 0, void 0, function () {
224
+ var release, check, readiness, hardware, white, offsets;
225
+ return __generator(this, function (_a) {
226
+ switch (_a.label) {
227
+ case 0:
228
+ release = lockColourSensorOperation(sensor.raft, true);
229
+ if (!release)
230
+ return [2 /*return*/];
231
+ check = function () { return assertColourSensorActive(sensor, signal); };
232
+ _a.label = 1;
233
+ case 1:
234
+ _a.trys.push([1, , 7, 8]);
235
+ check();
236
+ clearActiveColourSensorProfile(sensor);
237
+ return [4 /*yield*/, readColourSensorReadiness(sensor, signal, check)];
238
+ case 2:
239
+ readiness = _a.sent();
240
+ if (readiness.status !== "ok" || !readiness.value)
241
+ return [2 /*return*/];
242
+ return [4 /*yield*/, readColourSensorHardware(sensor, signal, check)];
243
+ case 3:
244
+ hardware = _a.sent();
245
+ if (hardware.status !== "ok" || hardware.value.commsOk === 0 || !hardware.value.serialNumber ||
246
+ !/^[a-zA-Z0-9_-]{1,128}$/.test(hardware.value.serialNumber))
247
+ return [2 /*return*/];
248
+ return [4 /*yield*/, readColourSensorRegister(sensor, "4020", signal, check)];
249
+ case 4:
250
+ white = _a.sent();
251
+ if (white.status !== "ok")
252
+ return [2 /*return*/];
253
+ return [4 /*yield*/, readColourSensorRegister(sensor, "4018", signal, check)];
254
+ case 5:
255
+ offsets = _a.sent();
256
+ if (offsets.status !== "ok")
257
+ return [2 /*return*/];
258
+ if (!hardware.value.typeCode || !hardware.value.firmwareVersion)
259
+ return [2 /*return*/];
260
+ return [4 /*yield*/, hydrateColourSensorProfile(sensor, { serialNumber: hardware.value.serialNumber,
261
+ typeCode: hardware.value.typeCode, firmwareVersion: hardware.value.firmwareVersion,
262
+ offsetsHex: offsets.value, whiteCalibrationHex: white.value }, signal, check)];
263
+ case 6:
264
+ _a.sent();
265
+ return [3 /*break*/, 8];
266
+ case 7:
181
267
  release();
182
268
  return [7 /*endfinally*/];
183
- case 14: return [2 /*return*/];
269
+ case 8: return [2 /*return*/];
184
270
  }
185
271
  });
186
272
  }); };
@@ -43,7 +43,7 @@ export type ColourSensorSetup = {
43
43
  offsets: SensorRead<Channels>;
44
44
  whiteCalibration: SensorRead<Channels>;
45
45
  };
46
- export type ColourSensorRegister = "4014" | "4018" | "4020";
46
+ export type ColourSensorRegister = "4014" | "4018" | "401c" | "4020";
47
47
  export declare const readColourSensorRegister: (sensor: ColourSensor, address: ColourSensorRegister, signal: AbortSignal, checkActive: () => void) => Promise<SensorRead<string>>;
48
48
  export declare const readColourSensorHardware: (sensor: ColourSensor, signal: AbortSignal, checkActive: () => void) => Promise<SensorRead<SensorHardware>>;
49
49
  export declare const wakeColourSensor: (sensor: ColourSensor, signal: AbortSignal, checkActive: () => void) => Promise<SensorRead<boolean>>;
@@ -138,7 +138,7 @@ var readHex = function (sensor, hexWr, bytes, signal, checkActive) {
138
138
  });
139
139
  };
140
140
  export var readColourSensorRegister = function (sensor, address, signal, checkActive) {
141
- return readHex(sensor, "10".concat(address), { "4014": 2, "4018": 4, "4020": 8 }[address], signal, checkActive);
141
+ return readHex(sensor, "10".concat(address), { "4014": 2, "4018": 4, "401c": 4, "4020": 8 }[address], signal, checkActive);
142
142
  };
143
143
  var mapRead = function (result, convert) {
144
144
  return result.status === "ok" ? __assign(__assign({}, result), { value: convert(result.value) }) : result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robotical/webapp-types",
3
- "version": "3.25.10",
3
+ "version": "3.25.12",
4
4
  "description": "Type definitions for the Application Manager",
5
5
  "main": "dist/application-manager.d.ts",
6
6
  "types": "dist/application-manager.d.ts",