@robotical/webapp-types 3.25.9 → 3.25.11
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-types/package.json +1 -1
- package/dist-types/src/application/RAFTs/Marty/PublishedDataAnalyser.d.ts +13 -3
- package/dist-types/src/application/RAFTs/Marty/PublishedDataAnalyser.js +81 -8
- package/dist-types/src/screens/configuration/marty/Diagnostics/ColourSensorDiagnostics/messages.d.ts +4 -0
- package/dist-types/src/screens/configuration/marty/Diagnostics/ColourSensorDiagnostics/messages.js +4 -0
- package/dist-types/src/utils/colour-classifier.d.ts +11 -1
- package/dist-types/src/utils/colour-classifier.js +55 -2
- package/dist-types/src/utils/colour-event-filter.d.ts +24 -0
- package/dist-types/src/utils/colour-event-filter.js +88 -0
- package/dist-types/src/utils/colour-sensor-diagnostics.d.ts +8 -1
- package/dist-types/src/utils/colour-sensor-diagnostics.js +2 -1
- package/dist-types/src/utils/marty-colour-policy.json +1 -0
- package/package.json +1 -1
package/dist-types/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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 of the confirmed/held colour (0–100); null without a reading. */
|
|
10
|
+
colourConfidence: number | null;
|
|
9
11
|
lightSense: "none" | "high" | "mid" | "low";
|
|
10
12
|
objectSense: "none" | "near";
|
|
11
13
|
noiseSense: "none" | "detected";
|
|
@@ -14,6 +16,11 @@ declare class PublishedDataAnalyser extends EventEmitter {
|
|
|
14
16
|
private marty;
|
|
15
17
|
martyState: MartyState;
|
|
16
18
|
private pubSub;
|
|
19
|
+
lastColourReading: ReturnType<typeof classifyColour> | null;
|
|
20
|
+
private colourFilter;
|
|
21
|
+
private colourContext;
|
|
22
|
+
private colourExpiry;
|
|
23
|
+
private connectionObserver;
|
|
17
24
|
lightSenseDetection: LightSenseDetection;
|
|
18
25
|
colourSensedDetection: ColourSensedDetection;
|
|
19
26
|
objectSenseDetection: ObjectSenseDetection;
|
|
@@ -34,13 +41,16 @@ declare class PublishedDataAnalyser extends EventEmitter {
|
|
|
34
41
|
subscribeToPublishedData(): void;
|
|
35
42
|
unsubscribeFromPublishedData(): void;
|
|
36
43
|
analyse(data: RICStateInfo): void;
|
|
37
|
-
setColourSensed(colour: MartyState["colourSensed"]): void;
|
|
44
|
+
setColourSensed(colour: MartyState["colourSensed"], confidence?: number | null): void;
|
|
45
|
+
private resetColourDetection;
|
|
46
|
+
recordColourReading(reading: ReturnType<typeof classifyColour>, receivedAtMs: number): void;
|
|
47
|
+
private scheduleColourExpiry;
|
|
38
48
|
setLightSense(lightSense: MartyState["lightSense"]): void;
|
|
39
49
|
setObjectSense(objectSense: MartyState["objectSense"]): void;
|
|
40
50
|
setNoiseSense(noiseSense: MartyState["noiseSense"]): void;
|
|
41
51
|
}
|
|
42
52
|
declare class ColourSensedDetection {
|
|
43
|
-
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;
|
|
44
54
|
}
|
|
45
55
|
declare class LightSenseDetection {
|
|
46
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 {
|
|
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,
|
|
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();
|
|
@@ -61,6 +75,7 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
|
|
|
61
75
|
_this.marty = marty;
|
|
62
76
|
_this.martyState = {
|
|
63
77
|
colourSensed: "unclear",
|
|
78
|
+
colourConfidence: null,
|
|
64
79
|
lightSense: "none",
|
|
65
80
|
objectSense: "none",
|
|
66
81
|
noiseSense: "none",
|
|
@@ -71,6 +86,9 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
|
|
|
71
86
|
}
|
|
72
87
|
PublishedDataAnalyser.prototype.subscribeToPublishedData = function () {
|
|
73
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"]);
|
|
74
92
|
this.pubSub = createRaftPubSubscriptionHelper(this.marty, this.marty.id);
|
|
75
93
|
this.pubSub.subscribe(function (data) {
|
|
76
94
|
_this.analyse(data.stateInfo);
|
|
@@ -79,6 +97,9 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
|
|
|
79
97
|
PublishedDataAnalyser.prototype.unsubscribeFromPublishedData = function () {
|
|
80
98
|
var _a;
|
|
81
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();
|
|
82
103
|
};
|
|
83
104
|
PublishedDataAnalyser.prototype.analyse = function (data) {
|
|
84
105
|
var _a;
|
|
@@ -88,8 +109,23 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
|
|
|
88
109
|
var lightSensor = getLightSensor(data);
|
|
89
110
|
var objectSensor = getObjectSensor(data);
|
|
90
111
|
var noiseSensor = getNoiseSensor(data);
|
|
91
|
-
|
|
92
|
-
|
|
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 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]);
|
|
121
|
+
if (context !== this.colourContext) {
|
|
122
|
+
this.resetColourDetection();
|
|
123
|
+
this.colourContext = context;
|
|
124
|
+
}
|
|
125
|
+
var values = colourChannelsFromValues(colourSensor.vals);
|
|
126
|
+
this.colourSensedDetection.detectColourSensed({ r: values.red, g: values.green, b: values.blue,
|
|
127
|
+
clear: values.clear, air: values.air }, this, profile === null || profile === void 0 ? void 0 : profile.signal, data.addOnInfoValidMs);
|
|
128
|
+
}
|
|
93
129
|
(objectSensor || colourSensor) &&
|
|
94
130
|
this.objectSenseDetection.detectObjectSense(getObjectIsTouchingFromVals(objectSensor, colourSensor), this);
|
|
95
131
|
lightSensor &&
|
|
@@ -101,9 +137,44 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
|
|
|
101
137
|
this.connectedSensors.objectSensor = !!objectSensor || !!colourSensor;
|
|
102
138
|
this.connectedSensors.noiseSensor = !!noiseSensor;
|
|
103
139
|
};
|
|
104
|
-
PublishedDataAnalyser.prototype.setColourSensed = function (colour) {
|
|
140
|
+
PublishedDataAnalyser.prototype.setColourSensed = function (colour, confidence) {
|
|
141
|
+
if (confidence === void 0) { confidence = null; }
|
|
142
|
+
var changed = colour !== this.martyState.colourSensed;
|
|
105
143
|
this.martyState.colourSensed = colour;
|
|
106
|
-
this.
|
|
144
|
+
this.martyState.colourConfidence = confidence;
|
|
145
|
+
if (changed)
|
|
146
|
+
this.emit(this.eventsMap.colourSensed[colour]);
|
|
147
|
+
};
|
|
148
|
+
PublishedDataAnalyser.prototype.resetColourDetection = function () {
|
|
149
|
+
if (this.colourExpiry !== null)
|
|
150
|
+
clearTimeout(this.colourExpiry);
|
|
151
|
+
this.colourExpiry = null;
|
|
152
|
+
this.colourContext = "";
|
|
153
|
+
this.colourFilter.reset();
|
|
154
|
+
this.lastColourReading = null;
|
|
155
|
+
this.setColourSensed("unclear", null);
|
|
156
|
+
};
|
|
157
|
+
PublishedDataAnalyser.prototype.recordColourReading = function (reading, receivedAtMs) {
|
|
158
|
+
this.lastColourReading = reading;
|
|
159
|
+
var value = this.colourFilter.update(reading, receivedAtMs, Date.now());
|
|
160
|
+
this.setColourSensed(value.colour, value.confidence);
|
|
161
|
+
this.scheduleColourExpiry();
|
|
162
|
+
};
|
|
163
|
+
PublishedDataAnalyser.prototype.scheduleColourExpiry = function () {
|
|
164
|
+
var _this = this;
|
|
165
|
+
if (this.colourExpiry !== null)
|
|
166
|
+
clearTimeout(this.colourExpiry);
|
|
167
|
+
var expiresAt = this.colourFilter.nextExpiryAtMs;
|
|
168
|
+
this.colourExpiry = expiresAt === null ? null : setTimeout(function () {
|
|
169
|
+
_this.colourExpiry = null;
|
|
170
|
+
if (_this.marty.isVerified === false || document.hidden) {
|
|
171
|
+
_this.resetColourDetection();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
var value = _this.colourFilter.current(Date.now());
|
|
175
|
+
_this.setColourSensed(value.colour, value.confidence);
|
|
176
|
+
_this.scheduleColourExpiry();
|
|
177
|
+
}, Math.max(0, expiresAt - Date.now()));
|
|
107
178
|
};
|
|
108
179
|
PublishedDataAnalyser.prototype.setLightSense = function (lightSense) {
|
|
109
180
|
this.martyState.lightSense = lightSense;
|
|
@@ -122,9 +193,11 @@ var PublishedDataAnalyser = /** @class */ (function (_super) {
|
|
|
122
193
|
var ColourSensedDetection = /** @class */ (function () {
|
|
123
194
|
function ColourSensedDetection() {
|
|
124
195
|
}
|
|
125
|
-
ColourSensedDetection.prototype.detectColourSensed = function (_a, analyser, profile) {
|
|
196
|
+
ColourSensedDetection.prototype.detectColourSensed = function (_a, analyser, profile, receivedAtMs) {
|
|
126
197
|
var r = _a.r, g = _a.g, b = _a.b, clear = _a.clear, air = _a.air;
|
|
127
|
-
|
|
198
|
+
if (receivedAtMs === void 0) { receivedAtMs = Date.now(); }
|
|
199
|
+
var reading = classifyColour({ red: r, green: g, blue: b, clear: clear, air: air }, profile);
|
|
200
|
+
return analyser.recordColourReading(reading, receivedAtMs);
|
|
128
201
|
};
|
|
129
202
|
return ColourSensedDetection;
|
|
130
203
|
}());
|
package/dist-types/src/screens/configuration/marty/Diagnostics/ColourSensorDiagnostics/messages.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ declare const colourDiagnosticsEnglish: {
|
|
|
16
16
|
prompt: string;
|
|
17
17
|
airPrompt: string;
|
|
18
18
|
result: string;
|
|
19
|
+
confidence: string;
|
|
20
|
+
confidenceUnavailable: string;
|
|
21
|
+
confidenceAbout: string;
|
|
22
|
+
confidenceHelp: string;
|
|
19
23
|
withoutAir: string;
|
|
20
24
|
firmwareAir: string;
|
|
21
25
|
waiting: string;
|
package/dist-types/src/screens/configuration/marty/Diagnostics/ColourSensorDiagnostics/messages.js
CHANGED
|
@@ -11,6 +11,10 @@ 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
|
+
confidence: "Confidence {{score}}/100",
|
|
15
|
+
confidenceUnavailable: "Confidence unavailable",
|
|
16
|
+
confidenceAbout: "About confidence",
|
|
17
|
+
confidenceHelp: "Rule strength, not probability. Higher scores mean more room before a colour or air cutoff. Air needs a saved calibration to score.",
|
|
14
18
|
withoutAir: "Firmware reports air; the RGB signal matches {{result}}.",
|
|
15
19
|
firmwareAir: "Firmware Air",
|
|
16
20
|
waiting: "Select a sensor and wait for fresh readings.",
|
|
@@ -9,6 +9,13 @@ export type ColourSignalProfile = {
|
|
|
9
9
|
margin: RGB;
|
|
10
10
|
};
|
|
11
11
|
export type ColourName = "red" | "yellow" | "green" | "blue" | "purple" | "air" | "unclear";
|
|
12
|
+
export type ColourConfidenceFactors = {
|
|
13
|
+
hue?: number;
|
|
14
|
+
chroma?: number;
|
|
15
|
+
saturation?: number;
|
|
16
|
+
air?: number;
|
|
17
|
+
};
|
|
18
|
+
export declare const COLOUR_CONFIDENCE_METHOD: string;
|
|
12
19
|
/** Published channels are white-normalised by the SDK; do not clamp them for classification. */
|
|
13
20
|
export declare const classifyColour: (values: RGB & {
|
|
14
21
|
clear: number;
|
|
@@ -18,12 +25,15 @@ export declare const classifyColour: (values: RGB & {
|
|
|
18
25
|
colourWithoutAirOverride: ColourName;
|
|
19
26
|
hue: number | null;
|
|
20
27
|
chroma: number;
|
|
28
|
+
confidence: number | null;
|
|
29
|
+
confidenceMethod: string;
|
|
30
|
+
confidenceFactors: ColourConfidenceFactors;
|
|
21
31
|
saturation: number;
|
|
22
32
|
signalAboveAir: boolean;
|
|
23
33
|
signalSource: string;
|
|
24
34
|
classificationReason: string;
|
|
25
35
|
};
|
|
26
|
-
export declare const colourChannelsFromValues: (vals: Record<string,
|
|
36
|
+
export declare const colourChannelsFromValues: (vals: Record<string, unknown>) => {
|
|
27
37
|
red: number;
|
|
28
38
|
green: number;
|
|
29
39
|
blue: number;
|
|
@@ -1,4 +1,40 @@
|
|
|
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
|
import policy from "./marty-colour-policy.json";
|
|
13
|
+
export var COLOUR_CONFIDENCE_METHOD = policy.confidence.method;
|
|
14
|
+
var channels = ["red", "green", "blue"];
|
|
15
|
+
var marginScore = function (fraction) { return Number.isFinite(fraction) ? Math.round(100 * Math.max(0, Math.min(1, fraction))) : 0; };
|
|
16
|
+
var colourConfidenceFactors = function (values, hue, chroma, saturation, match, profile) {
|
|
17
|
+
var _a = match.hue, lowerHue = _a[0], upperHue = _a[1];
|
|
18
|
+
// Red's two table entries are one continuous interval across 360°/0°.
|
|
19
|
+
// That seam is not a decision boundary, so pure red must not get a zero score.
|
|
20
|
+
var sameBand = policy.boundaries.filter(function (band) { return band.name === match.name &&
|
|
21
|
+
band.chroma[0] === match.chroma[0] && band.chroma[1] === match.chroma[1]; });
|
|
22
|
+
var start = sameBand.find(function (band) { return band.hue[0] === 0; });
|
|
23
|
+
var end = sameBand.find(function (band) { return band.hue[1] >= 360; });
|
|
24
|
+
if (start && end) {
|
|
25
|
+
lowerHue = end.hue[0] - 360;
|
|
26
|
+
upperHue = start.hue[1];
|
|
27
|
+
if (hue > 180)
|
|
28
|
+
hue -= 360;
|
|
29
|
+
}
|
|
30
|
+
// Two channels determine chroma. Use the two largest channel margins as a
|
|
31
|
+
// conservative noise scale, in the same white-normalised units as the RGB.
|
|
32
|
+
var noise = channels.map(function (channel) { var _a; return (_a = profile === null || profile === void 0 ? void 0 : profile.margin[channel]) !== null && _a !== void 0 ? _a : policy.confidence.defaultChannelMargin; })
|
|
33
|
+
.sort(function (a, b) { return b - a; });
|
|
34
|
+
return __assign({ hue: marginScore(Math.min(hue - lowerHue, upperHue - hue) / policy.confidence.fullHueMarginDegrees), chroma: marginScore(Math.min(chroma - Math.max(match.chroma[0], policy.signal.minimumChroma), match.chroma[1] - chroma) / (noise[0] + noise[1])), saturation: marginScore((saturation - policy.signal.minimumSaturation) / policy.confidence.fullSaturationMargin) }, (profile && { air: marginScore(Math.max.apply(Math, channels.map(function (channel) {
|
|
35
|
+
return (values[channel] - profile.airMax[channel] - profile.margin[channel]) / profile.margin[channel];
|
|
36
|
+
}))) }));
|
|
37
|
+
};
|
|
2
38
|
/** Published channels are white-normalised by the SDK; do not clamp them for classification. */
|
|
3
39
|
export var classifyColour = function (values, profile) {
|
|
4
40
|
var r = values.red, g = values.green, b = values.blue, clear = values.clear, air = values.air;
|
|
@@ -20,7 +56,24 @@ export var classifyColour = function (values, profile) {
|
|
|
20
56
|
var colourWithoutAirOverride = confident && match ? match.name : "unclear";
|
|
21
57
|
var classifiedColour = colourWithoutAirOverride !== "unclear" ? colourWithoutAirOverride :
|
|
22
58
|
valid && air ? "air" : "unclear";
|
|
23
|
-
|
|
59
|
+
// A rule-strength score, not a statistical probability or a new acceptance
|
|
60
|
+
// gate. The weakest decision margin wins; existing colour decisions stay intact.
|
|
61
|
+
var confidence = 0;
|
|
62
|
+
var confidenceFactors = {};
|
|
63
|
+
if (colourWithoutAirOverride !== "unclear" && match) {
|
|
64
|
+
confidenceFactors = colourConfidenceFactors(values, hue, chroma, saturation, match, profile);
|
|
65
|
+
confidence = Math.min.apply(Math, Object.values(confidenceFactors));
|
|
66
|
+
}
|
|
67
|
+
else if (classifiedColour === "air") {
|
|
68
|
+
// Firmware Air alone has no measurable confidence. With calibration, all
|
|
69
|
+
// channels must agree with the air ceiling; contrary RGB evidence scores 0.
|
|
70
|
+
confidence = profile ? marginScore(Math.min.apply(Math, channels.map(function (channel) {
|
|
71
|
+
return (profile.airMax[channel] + profile.margin[channel] - values[channel]) / profile.margin[channel];
|
|
72
|
+
}))) : null;
|
|
73
|
+
if (confidence !== null)
|
|
74
|
+
confidenceFactors = { air: confidence };
|
|
75
|
+
}
|
|
76
|
+
return { classifiedColour: classifiedColour, colourWithoutAirOverride: colourWithoutAirOverride, hue: Number.isFinite(hue) ? hue : null, chroma: chroma, confidence: confidence, confidenceMethod: COLOUR_CONFIDENCE_METHOD, confidenceFactors: confidenceFactors, saturation: saturation, signalAboveAir: exceedsAir, signalSource: profile ? "calibratedAir" : "defaultRules", classificationReason: !valid ? "invalid" :
|
|
24
77
|
!exceedsAir ? "withinCalibratedAir" : confident && match ? air ? "rgbOverridesFirmwareAir" : "rgbColour" :
|
|
25
78
|
air ? "firmwareAir" : "weakOrUnknownColour" };
|
|
26
79
|
};
|
|
@@ -33,5 +86,5 @@ export var colourChannelsFromValues = function (vals) {
|
|
|
33
86
|
values[channel.toLowerCase()] = vals[key];
|
|
34
87
|
}
|
|
35
88
|
return { red: values.red, green: values.green, blue: values.blue,
|
|
36
|
-
clear: values.clear, air: [true, false, 0, 1].
|
|
89
|
+
clear: values.clear, air: [true, false, 0, 1].some(function (value) { return value === values.air; }) ? Boolean(values.air) : undefined };
|
|
37
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 };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import colourPolicy from "./marty-colour-policy.json";
|
|
2
|
-
import { ColourSignalProfile } from "./colour-classifier";
|
|
2
|
+
import { ColourConfidenceFactors, ColourSignalProfile } from "./colour-classifier";
|
|
3
3
|
import { ColourSensorProfile } from "./colour-sensor-profile";
|
|
4
4
|
import type { RaftObserver } from "../application/RAFTs/RaftObserver";
|
|
5
5
|
import type { RestMessageOptions } from "../types/communication-between-apps/wrapper-communication";
|
|
@@ -68,6 +68,9 @@ export type ColourReading = {
|
|
|
68
68
|
hue: number | null;
|
|
69
69
|
chroma: number;
|
|
70
70
|
classifiedColour: string;
|
|
71
|
+
confidence?: number | null;
|
|
72
|
+
confidenceMethod?: string;
|
|
73
|
+
confidenceFactors?: ColourConfidenceFactors;
|
|
71
74
|
colourWithoutAirOverride?: string;
|
|
72
75
|
saturation?: number;
|
|
73
76
|
signalAboveAir?: boolean;
|
|
@@ -93,6 +96,7 @@ export type ColourSnapshot = {
|
|
|
93
96
|
signal?: typeof colourPolicy.signal;
|
|
94
97
|
profile?: ColourSensorProfile;
|
|
95
98
|
clearBoundsUsedForClassification?: boolean;
|
|
99
|
+
confidence?: typeof colourPolicy.confidence;
|
|
96
100
|
};
|
|
97
101
|
device: {
|
|
98
102
|
id: string;
|
|
@@ -148,6 +152,9 @@ export declare const interpretColour: (vals: Record<string, number | boolean>, p
|
|
|
148
152
|
colourWithoutAirOverride: import("./colour-classifier").ColourName;
|
|
149
153
|
hue: number | null;
|
|
150
154
|
chroma: number;
|
|
155
|
+
confidence: number | null;
|
|
156
|
+
confidenceMethod: string;
|
|
157
|
+
confidenceFactors: ColourConfidenceFactors;
|
|
151
158
|
saturation: number;
|
|
152
159
|
signalAboveAir: boolean;
|
|
153
160
|
signalSource: string;
|
|
@@ -263,7 +263,8 @@ var captureUnlocked = function (sensor, expectedColour, session, signal) { retur
|
|
|
263
263
|
if (samples.length < 2)
|
|
264
264
|
throw new ColourDiagnosticError("staleReading");
|
|
265
265
|
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
|
|
266
|
+
signal: colourPolicy.signal, profile: getActiveColourSensorProfile(sensor), clearBoundsUsedForClassification: false,
|
|
267
|
+
confidence: colourPolicy.confidence }, device: {
|
|
267
268
|
id: sensor.raftId, type: sensor.raft.type, name: ((_c = (_b = sensor.raft).getFriendlyName) === null || _c === void 0 ? void 0 : _c.call(_b)) || sensor.raftId,
|
|
268
269
|
firmwareVersion: ((_d = sensor.raft.systemInfo) === null || _d === void 0 ? void 0 : _d.SystemVersion) || null, connectionMethod: sensor.raft.connectionMethod || null,
|
|
269
270
|
}, sensor: { id: sensor.addon.id, name: sensor.addon.name, whoAmI: sensor.addon.whoAmI } }, (((_f = (_e = sensor.raft.raftStateInfo) === null || _e === void 0 ? void 0 : _e.power) === null || _f === void 0 ? void 0 : _f.powerStatus) && { powerStatus: {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"source": "Marty web app RGB signal policy v4",
|
|
3
3
|
"clearBoundsUsedForClassification": false,
|
|
4
4
|
"signal": { "minimumChroma": 8, "minimumSaturation": 0.15, "airNoiseMarginRaw": 3 },
|
|
5
|
+
"confidence": { "method": "rule-margin-v1", "fullHueMarginDegrees": 15, "fullSaturationMargin": 0.15, "defaultChannelMargin": 3 },
|
|
5
6
|
"boundaries": [
|
|
6
7
|
{ "hue": [0, 15], "chroma": [50, 200], "clear": [40, 150], "name": "red" },
|
|
7
8
|
{ "hue": [20, 65], "chroma": [40, 300], "clear": [100, 255], "name": "yellow" },
|