Skip to content

Commit

Permalink
Add support for 2025 version of URCL (#267)
Browse files Browse the repository at this point in the history
* Add support for 2025 URCL

* Fix formatting

* URCL clean up

* Fix URCL

* Update Spark frame spec

* Rename `REVSchemaLegacy`
  • Loading branch information
jwbonner authored Nov 9, 2024
1 parent 7d6b11a commit 51deacf
Show file tree
Hide file tree
Showing 9 changed files with 16,729 additions and 73 deletions.
234 changes: 183 additions & 51 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "11.1.3",
"@rollup/plugin-typescript": "12.1.1",
"@types/bignumber.js": "^5.0.4",
"@types/chart.js": "^2.9.38",
"@types/color-convert": "^2.0.3",
"@types/download": "^8.0.2",
Expand All @@ -44,6 +45,8 @@
"@types/pngjs": "^6.0.5",
"@types/ssh2": "^1.11.13",
"@types/three": "^0.168.0",
"bignumber.js": "^9.1.2",
"buffer": "^6.0.3",
"camera-controls": "^2.9.0",
"chart.js": "^4.4.0",
"color-convert": "^2.0.1",
Expand All @@ -63,7 +66,8 @@
"simple-statistics": "^7.8.3",
"three": "^0.168.0",
"tslib": "^2.6.2",
"typescript": "5.2.2"
"type-fest": "^4.26.1",
"typescript": "5.6.3"
},
"dependencies": {
"@distube/ytdl-core": "^4.14.4",
Expand Down
8 changes: 5 additions & 3 deletions src/hub/dataSources/schema/CustomSchemas.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import Log from "../../../shared/log/Log";
import PhotonSchema from "./PhotonSchema";
import REVSchemas from "./REVSchema";
import URCLSchema from "./URCLSchema";
import URCLSchemaLegacy from "./URCLSchemaLegacy";

/** Schemas that require custom handling because they can't be decoded using just the log data. */
const CustomSchemas: Map<string, (log: Log, key: string, timestamp: number, value: Uint8Array) => void> = new Map();
export default CustomSchemas;

CustomSchemas.set("rawBytes", PhotonSchema); // PhotonVision 2023.1.2
CustomSchemas.set("URCL", REVSchemas.parseURCLr1);
CustomSchemas.set("URCLr2_periodic", REVSchemas.parseURCLr2);
CustomSchemas.set("URCL", URCLSchemaLegacy.parseURCLr1);
CustomSchemas.set("URCLr2_periodic", URCLSchemaLegacy.parseURCLr2);
CustomSchemas.set("URCLr3_periodic", URCLSchema.parseURCLr3);
161 changes: 161 additions & 0 deletions src/hub/dataSources/schema/URCLSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import BigNumber from "bignumber.js";
import Log from "../../../shared/log/Log";
import { getOrDefault } from "../../../shared/log/LogUtil";
import LoggableType from "../../../shared/log/LoggableType";
import { parseCanFrame } from "./spark/can-spec-util";
import { sparkFramesSpec } from "./spark/spark-frames";

type FirmwareVersion = {
major: number;
minor: number;
build: number;
};

const PERSISTENT_SIZE = 8;
const PERIODIC_SIZE = 14;
const PERIODIC_API_CLASS = sparkFramesSpec.periodicFrames.STATUS_0.apiClass;
const PERIODIC_FRAME_SPECS = [
sparkFramesSpec.periodicFrames.STATUS_0,
sparkFramesSpec.periodicFrames.STATUS_1,
sparkFramesSpec.periodicFrames.STATUS_2,
sparkFramesSpec.periodicFrames.STATUS_3,
sparkFramesSpec.periodicFrames.STATUS_4,
sparkFramesSpec.periodicFrames.STATUS_5,
sparkFramesSpec.periodicFrames.STATUS_6,
sparkFramesSpec.periodicFrames.STATUS_7
];
const FIRMWARE_FRAME_SPEC = sparkFramesSpec.nonPeriodicFrames.GET_FIRMWARE_VERSION;
const FIRMWARE_API = (FIRMWARE_FRAME_SPEC.apiClass << 4) | FIRMWARE_FRAME_SPEC.apiIndex;

const DEFAULT_ALIASES = Uint8Array.of(0x7b, 0x7d);
const TEXT_DECODER = new TextDecoder("UTF-8");

export default class URCLSchema {
private constructor() {}

/**
* Parses a set of frames recorded by URCL using revision 3.
*/
static parseURCLr3(log: Log, key: string, timestamp: number, value: Uint8Array) {
let devices: { [key: string]: { alias?: string; firmware?: FirmwareVersion } } = {};
if (!key.endsWith("Raw/Periodic")) return;
const rootKey = key.slice(0, key.length - "Raw/Periodic".length);
const aliasKey = rootKey + "Raw/Aliases";
const persistentKey = rootKey + "Raw/Persistent";
let getName = (deviceId: string): string => {
if (devices[deviceId].alias === undefined) {
return "Spark-" + deviceId;
} else {
return devices[deviceId].alias!;
}
};

// Read aliases
let aliasesRaw = getOrDefault(log, aliasKey, LoggableType.Raw, timestamp, null);
if (aliasesRaw === null) aliasesRaw = DEFAULT_ALIASES;
let aliases = JSON.parse(TEXT_DECODER.decode(aliasesRaw));
Object.keys(aliases).forEach((idString) => {
devices[idString] = { alias: aliases[idString] };
});

// Read persistent
let persistentRaw: Uint8Array | null = getOrDefault(log, persistentKey, LoggableType.Raw, timestamp, null);
if (persistentRaw === null) return;
const persistentDataView = new DataView(persistentRaw.buffer, persistentRaw.byteOffset, persistentRaw.byteLength);
for (let position = 0; position < persistentRaw.length; position += PERSISTENT_SIZE) {
let messageId = persistentDataView.getUint16(position, true);
let messageValue = persistentRaw.slice(position + 2, position + 8);
let deviceId = messageId & 0x3f;
if (!(deviceId in devices)) {
devices[deviceId] = {};
}
if (((messageId >> 6) & 0x3ff) === FIRMWARE_API) {
// Firmware frame
let fullMessageValue = new Uint8Array(8);
fullMessageValue.set(messageValue, 0);
let firmwareValues = parseCanFrame(FIRMWARE_FRAME_SPEC, { data: fullMessageValue });
devices[deviceId].firmware = {
major: Number(firmwareValues.MAJOR),
minor: Number(firmwareValues.MINOR),
build: Number(firmwareValues.FIX)
};
}
}

// Write firmware versions to log
Object.keys(devices).forEach((deviceId) => {
if (devices[deviceId].firmware === undefined) {
return;
}
let firmwareString =
devices[deviceId].firmware?.major.toString() +
"." +
devices[deviceId].firmware?.minor.toString() +
"." +
devices[deviceId].firmware?.build.toString();
let firmwareKey = rootKey + getName(deviceId) + "/Firmware";
log.putString(firmwareKey, timestamp, firmwareString);
log.createBlankField(rootKey + getName(deviceId), LoggableType.Empty);
log.setGeneratedParent(rootKey + getName(deviceId));
});

// Read periodic frames
const periodicDataView = new DataView(value.buffer, value.byteOffset, value.byteLength);
for (let position = 0; position < value.length; position += PERIODIC_SIZE) {
let messageTimestamp = Number(periodicDataView.getUint32(position, true)) / 1e3;
let messageId = periodicDataView.getUint16(position + 4, true);
let messageValue = value.slice(position + 6, position + 14);
let deviceId = messageId & 0x3f;
if (!(deviceId in devices) || devices[deviceId].firmware === undefined || devices[deviceId].firmware.major < 25) {
continue;
}

if (((messageId >> 10) & 0x3f) === PERIODIC_API_CLASS) {
// Periodic frame
let frameIndex = (messageId >> 6) & 0xf;
let deviceKey = rootKey + getName(deviceId.toString());
let frameKey = deviceKey + "/PeriodicFrame/" + frameIndex.toFixed();
log.putRaw(frameKey, messageTimestamp, messageValue);
if (frameIndex >= 0 && frameIndex < PERIODIC_FRAME_SPECS.length) {
let frameSpec = PERIODIC_FRAME_SPECS[frameIndex];
let frameValues = parseCanFrame(frameSpec, { data: messageValue }) as { [key: string]: BigNumber | boolean };
Object.entries(frameValues).forEach(([signalKey, signalValue]) => {
if (!(signalKey in frameSpec.signals)) return;
let signalSpec = (frameSpec.signals as { [key: string]: any })[signalKey];
if (signalSpec.name.includes("Reserved")) return;
let signalGroup = "";
if (signalSpec.name.includes("Fault")) {
signalGroup = "Fault";
} else if (signalSpec.name.includes("Warning")) {
signalGroup = "Warning";
}
let signalLogKey =
deviceKey +
"/" +
(signalGroup.length === 0 ? "" : signalGroup + "/") +
(signalSpec.name as string).replaceAll(" ", "");
switch (signalSpec.type as string) {
case "int":
case "uint":
case "float":
log.putNumber(signalLogKey, messageTimestamp, Number(signalValue));
break;
case "boolean":
log.putBoolean(signalLogKey, messageTimestamp, signalValue as boolean);
break;
}
if ("description" in signalSpec) {
log.setMetadataString(signalLogKey, JSON.stringify({ description: signalSpec.description }));
}
if (signalSpec.name === "Applied Output") {
const voltage = getOrDefault(log, deviceKey + "/Voltage", LoggableType.Number, timestamp, 0);
if (voltage > 0) {
log.putNumber(deviceKey + "/AppliedOutputVoltage", timestamp, Number(signalValue) * voltage);
}
}
});
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const DEFAULT_ALIASES = Uint8Array.of(0x7b, 0x7d);

const TEXT_DECODER = new TextDecoder("UTF-8");

export default class REVSchema {
export default class URCLSchemaLegacy {
private constructor() {}

/**
Expand Down Expand Up @@ -87,10 +87,10 @@ export default class REVSchema {
}
if (((messageId >> 6) & 0x3ff) === FIRMWARE_API) {
// Firmware frame
devices[deviceId].firmware = REVSchema.parseFirmware(messageValue);
devices[deviceId].firmware = URCLSchemaLegacy.parseFirmware(messageValue);
} else if (((messageId >> 6) & 0x3ff) === MODEL_API) {
// Device model frame
devices[deviceId].model = REVSchema.parseDeviceModel(messageValue);
devices[deviceId].model = URCLSchemaLegacy.parseDeviceModel(messageValue);
}
}

Expand Down Expand Up @@ -128,8 +128,8 @@ export default class REVSchema {
let deviceKey = rootKey + getName(deviceId.toString());
let frameKey = deviceKey + "/PeriodicFrame/" + frameIndex.toFixed();
log.putRaw(frameKey, messageTimestamp, messageValue);
if (frameIndex >= 0 && frameIndex < REVSchema.PARSE_PERIODIC.length) {
REVSchema.PARSE_PERIODIC[frameIndex](
if (frameIndex >= 0 && frameIndex < URCLSchemaLegacy.PARSE_PERIODIC.length) {
URCLSchemaLegacy.PARSE_PERIODIC[frameIndex](
log,
deviceKey,
messageTimestamp,
Expand Down Expand Up @@ -161,10 +161,10 @@ export default class REVSchema {
}
if (((messageId >> 6) & 0x3ff) === FIRMWARE_API) {
// Firmware frame
devices[deviceId].firmware = REVSchema.parseFirmware(messageValue);
devices[deviceId].firmware = URCLSchemaLegacy.parseFirmware(messageValue);
} else if (((messageId >> 6) & 0x3ff) === MODEL_API) {
// Device model frame
devices[deviceId].model = REVSchema.parseDeviceModel(messageValue);
devices[deviceId].model = URCLSchemaLegacy.parseDeviceModel(messageValue);
}
}

Expand Down Expand Up @@ -203,8 +203,8 @@ export default class REVSchema {
let deviceKey = key + "/" + devices[deviceId].model + "-" + deviceId.toString();
let frameKey = deviceKey + "/PeriodicFrame/" + frameIndex.toFixed();
log.putRaw(frameKey, messageTimestamp, messageValue);
if (frameIndex >= 0 && frameIndex < REVSchema.PARSE_PERIODIC.length) {
REVSchema.PARSE_PERIODIC[frameIndex](
if (frameIndex >= 0 && frameIndex < URCLSchemaLegacy.PARSE_PERIODIC.length) {
URCLSchemaLegacy.PARSE_PERIODIC[frameIndex](
log,
deviceKey,
messageTimestamp,
Expand Down Expand Up @@ -243,14 +243,14 @@ export default class REVSchema {
}

private static PARSE_PERIODIC = [
REVSchema.parsePeriodic0,
REVSchema.parsePeriodic1,
REVSchema.parsePeriodic2,
REVSchema.parsePeriodic3,
REVSchema.parsePeriodic4,
REVSchema.parsePeriodic5,
REVSchema.parsePeriodic6,
REVSchema.parsePeriodic7
URCLSchemaLegacy.parsePeriodic0,
URCLSchemaLegacy.parsePeriodic1,
URCLSchemaLegacy.parsePeriodic2,
URCLSchemaLegacy.parsePeriodic3,
URCLSchemaLegacy.parsePeriodic4,
URCLSchemaLegacy.parsePeriodic5,
URCLSchemaLegacy.parsePeriodic6,
URCLSchemaLegacy.parsePeriodic7
];

/**
Expand Down
1 change: 1 addition & 0 deletions src/hub/dataSources/schema/spark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
These files are provided by REV and should not be modified, except to remove the dependency on @rev-robotics/can-bridge.
Loading

0 comments on commit 51deacf

Please sign in to comment.