Skip to content

Commit

Permalink
Add support for 2025 URCL
Browse files Browse the repository at this point in the history
  • Loading branch information
jwbonner committed Oct 31, 2024
1 parent b94f45f commit cf51e7d
Show file tree
Hide file tree
Showing 9 changed files with 17,195 additions and 30 deletions.
77 changes: 67 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "11.1.3",
"@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,7 @@
"@types/pngjs": "^6.0.5",
"@types/ssh2": "^1.11.13",
"@types/three": "^0.168.0",
"bignumber.js": "^9.1.2",
"camera-controls": "^2.9.0",
"chart.js": "^4.4.0",
"color-convert": "^2.0.1",
Expand All @@ -63,6 +65,7 @@
"simple-statistics": "^7.8.3",
"three": "^0.168.0",
"tslib": "^2.6.2",
"type-fest": "^4.26.1",
"typescript": "5.2.2"
},
"dependencies": {
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);
136 changes: 136 additions & 0 deletions src/hub/dataSources/schema/URCLSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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-public";

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 = Object.entries(sparkFramesSpec.periodicFrames)
.filter(([name, _]) => name.startsWith("STATUS_"))
.sort(([nameA, _A], [nameB, _B]) => nameA.localeCompare(nameB))
.map(([_, spec]) => spec);
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 2.
*/
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 firmwareValues = parseCanFrame(FIRMWARE_FRAME_SPEC, { data: messageValue });
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];
let signalLogKey = (deviceKey = "/" + (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(key, JSON.stringify({ description: signalSpec.description }));
}
});
}
}
}
}
}
Loading

0 comments on commit cf51e7d

Please sign in to comment.