Skip to content

Commit

Permalink
Add more SDK Commands for basic functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
fchorney committed Apr 8, 2024
1 parent 1b8a70b commit 6203b0a
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 42 deletions.
4 changes: 2 additions & 2 deletions sdk/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export class SMXConfig {
* TODO: Make this private again later, and maybe make a function called
* "write_to_stage" or something? Depends on how we want to handle writing/reading
*/
encode(): DataView {
return smx_config_t.encode(this.config, true);
encode(): Array<number> {
return Array.from(new Uint8Array(smx_config_t.encode(this.config, true).buffer));
}
}
2 changes: 1 addition & 1 deletion sdk/commands/sensor_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export class SMXPanelTestData {
* (square it to convert back), but without this we display a bunch
* of four and five digit numbers that are too hard to read.
*/
return Math.sqrt(value);
return Math.round((Math.sqrt(value) + Number.EPSILON) * 100) / 100;
}

// TODO: This may be necessary for defining sensor value vertial bars in the UI
Expand Down
141 changes: 111 additions & 30 deletions sdk/smx.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as Bacon from "baconjs";
import { collatePackets, type DataPacket } from "./state-machines/collate-packets";
import { collatePackets, type AckPacket, type DataPacket } from "./state-machines/collate-packets";
import { API_COMMAND, char2byte } from "./api";
import { SMXConfig, type Decoded } from "./commands/config";
import { SMXDeviceInfo } from "./commands/data_info";
import { StageInputs } from "./commands/inputs";
import { HID_REPORT_INPUT, HID_REPORT_INPUT_STATE, send_data } from "./packet";
import { SMXSensorTestData, SensorTestMode } from "./commands/sensor_test";
import { RGB } from "./utils";

/**
* Class purely to set up in/out event stream "pipes" to properly throttle and sync input/output from a stage
Expand All @@ -18,6 +19,8 @@ class SMXEvents {
public readonly inputState$;
/** read from this to see all reports sent by the stage in response to a command */
public readonly otherReports$: Bacon.EventStream<Uint8Array>;
/** read from this to see if we've received an ACK */
public readonly ackReports$: Bacon.EventStream<AckPacket>;
/** push to this to write commands to the stage */
public readonly output$: Bacon.Bus<number[]>;
/** this is everything pushed to `output$` but properly throttled/timed to the device's ack responses */
Expand All @@ -42,12 +45,13 @@ class SMXEvents {
this.otherReports$ = (report$.filter((e) => e.type === "data") as Bacon.EventStream<DataPacket>).map(
(e) => e.payload,
);
this.ackReports$ = report$.filter((e) => e.type === "ack") as Bacon.EventStream<AckPacket>;

const finishedCommand$ = report$
.filter((e) => e.type === "host_cmd_finished")
.map((e) => e.type === "host_cmd_finished");

// finishedCommand$.log("Cmd Finished");
finishedCommand$.log("Cmd Finished");

const okSend$ = finishedCommand$.startWith(true);

Expand All @@ -69,8 +73,8 @@ class SMXEvents {
export class SMXStage {
private dev: HIDDevice;
private readonly events: SMXEvents;
private test_mode: SensorTestMode = SensorTestMode.CalibratedValues; // TODO: Maybe we just let this be public
private debug = false;
private test_mode: SensorTestMode = SensorTestMode.CalibratedValues;
private debug = true;

info: SMXDeviceInfo | null = null;
config: SMXConfig | null = null;
Expand All @@ -79,8 +83,8 @@ export class SMXStage {

public readonly inputState$: Bacon.EventStream<boolean[]>;
public readonly deviceInfo$: Bacon.EventStream<SMXDeviceInfo>;
public readonly configResponse$: Bacon.EventStream<SMXConfig>;
public readonly testDataResponse$: Bacon.EventStream<SMXSensorTestData>;
public readonly configResponse$: Bacon.EventStream<SMXConfig | undefined>;
public readonly testDataResponse$: Bacon.EventStream<SMXSensorTestData | undefined>;

constructor(dev: HIDDevice) {
this.dev = dev;
Expand Down Expand Up @@ -115,68 +119,144 @@ export class SMXStage {
this.inputState$ = this.events.inputState$.map((value) => this.handleInputs(value));
}

async init(): Promise<SMXConfig> {
async init(): Promise<SMXSensorTestData | undefined> {
// Request the device information
this.updateDeviceInfo();
return this.updateDeviceInfo().then(() => {
// Request the config for this stage
return this.updateConfig()?.then(() => {
return this.updateTestData();
});
});
}

/**
* TODO: To Implement:
* Stretch Goal:
* SET_PANEL_TEST_MODE
*
* Double Stretch Goal:
* GET_CONFIG (old firmware)
* WRITE_CONFIG (old_firmware)
* SET_SERIAL_NUMBERS
*/

// Request some initial test data
this.updateTestData();
writeConfig(): Promise<AckPacket | undefined> {
if (!this.info || !this.config) return Promise.resolve(undefined);

// Request the config for this stage
return this.updateConfig();
const command = this.info.firmware_version < 5 ? API_COMMAND.WRITE_CONFIG : API_COMMAND.WRITE_CONFIG_V5;
const encoded_config = this.config.encode();
this.events.output$.push([command, encoded_config.length].concat(encoded_config));

return this.events.ackReports$.firstToPromise();
}

updateDeviceInfo() {
setLightStrip(color: RGB): Promise<AckPacket | undefined> {
if (!this.info) return Promise.resolve(undefined);

const led_strip_index = 0; // Always 0
const number_of_leds = 44; // Always 44 (Unless some older or newer versions have more/less?)
const rgb = color.toArray();
const light_command = [API_COMMAND.SET_LIGHT_STRIP, led_strip_index, number_of_leds];

for (let i = 0; i < number_of_leds; i++) {
light_command.push(...rgb);
}

console.log(light_command);
this.events.output$.push(light_command);

return this.events.ackReports$.firstToPromise();
}

factoryReset(): Promise<AckPacket | undefined> {
if (!this.info || !this.config) return Promise.resolve(undefined);

/**
* Factor reset resets the platform strip color saved to the
* configuration, but it doesn't actually apply it to the lights.
*
* Do this for firmware v5 and up.
*/
if (this.info.firmware_version >= 5) {
const color = this.config.config.platformStripColor;
this.setLightStrip(new RGB(color.r, color.g, color.b));
}

this.events.output$.push([API_COMMAND.FACTORY_RESET]);
return this.events.ackReports$.firstToPromise();
}

forceRecalibration(): Promise<AckPacket> {
this.events.output$.push([API_COMMAND.FORCE_RECALIBRATION]);
return this.events.ackReports$.firstToPromise();
}

updateDeviceInfo(): Promise<SMXDeviceInfo> {
this.events.output$.push([API_COMMAND.GET_DEVICE_INFO]);
return this.deviceInfo$.firstToPromise();
}

updateConfig(): Promise<SMXConfig> {
this.events.output$.push([API_COMMAND.GET_CONFIG_V5]);
updateConfig(): Promise<SMXConfig | undefined> {
if (!this.info) return Promise.resolve(undefined);

const command = this.info.firmware_version < 5 ? API_COMMAND.GET_CONFIG : API_COMMAND.GET_CONFIG_V5;
this.events.output$.push([command]);
return this.configResponse$.firstToPromise();
}

updateTestData(mode: SensorTestMode | null = null): Promise<SMXSensorTestData> {
if (mode) {
this.test_mode = mode;
}
updateTestData(mode: SensorTestMode | null = null): Promise<SMXSensorTestData | undefined> {
if (!this.config) return Promise.resolve(undefined);
if (mode) this.test_mode = mode;

this.events.output$.push([API_COMMAND.GET_SENSOR_TEST_DATA, this.test_mode]);
return this.testDataResponse$.firstToPromise();
}

private handleConfig(data: Uint8Array) {
private handleConfig(data: Uint8Array): SMXConfig | undefined {
if (!this.info) return;

/*
// TODO: Figure out how we want to handle this? I think we can actually convert to/from the new config
// from the old config
if (this.info.firmware_version < 5) {
this.config = new SMXConfigOld(Array.from(data));
} else {
this.config = new SMXConfig(Array.from(data));
}
*/
this.config = new SMXConfig(Array.from(data));

// Right now I just want to confirm that decoding and encoding gives us back the same data
const encoded_config = this.config.encode();
if (encoded_config) {
const buf = new Uint8Array(encoded_config.buffer);
this.debug && console.log("Config Encodes Correctly: ", data.slice(2, -1).toString() === buf.toString());
this.debug &&
console.log("Config Encodes Correctly: ", data.slice(2, -1).toString() === encoded_config.toString());
}
this.debug && console.log("Got Config: ", this.config);

return this.config;
}

private handleTestData(data: Uint8Array) {
this.test = new SMXSensorTestData(
Array.from(data),
this.test_mode,
this.config?.config?.flags?.PlatformFlags_FSR || true,
);
private handleTestData(data: Uint8Array): SMXSensorTestData | undefined {
if (!this.config) return;

this.test = new SMXSensorTestData(Array.from(data), this.test_mode, this.config.config.flags.PlatformFlags_FSR);

this.debug && console.log("Got Test: ", this.test);

return this.test;
}

private handleDeviceInfo(data: Uint8Array) {
private handleDeviceInfo(data: Uint8Array): SMXDeviceInfo {
this.info = new SMXDeviceInfo(Array.from(data));

this.debug && console.log("Got Info: ", this.info);

return this.info;
}

private handleInputs(data: Decoded<typeof StageInputs>) {
private handleInputs(data: Decoded<typeof StageInputs>): Array<boolean> {
this.inputs = [
data.up_left,
data.up,
Expand All @@ -188,6 +268,7 @@ export class SMXStage {
data.down,
data.down_right,
];

return this.inputs;
}
}
21 changes: 17 additions & 4 deletions sdk/state-machines/collate-packets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import {
PACKET_PREAMBLE_SIZE,
} from "../packet";

const ACK_PACKET = 0x7;

interface PacketHandlingState {
currentPacket: Uint8Array;
}

export type DataPacket = { type: "data"; payload: Uint8Array };
export type Packet = { type: "host_cmd_finished" } | DataPacket;
export type AckPacket = { type: "ack" };
export type Packet = { type: "host_cmd_finished" } | DataPacket | AckPacket;

/**
* Gets called when a packet is received, returns a tuple of new state and an array of
Expand Down Expand Up @@ -53,9 +56,15 @@ export const collatePackets: StateF<DataView, PacketHandlingState, Packet> = (st
}

// The data exists after the first 2 bytes
const dataBody = data.slice(2, 2 + byte_len);
const dataBody = data.slice(PACKET_PREAMBLE_SIZE, PACKET_PREAMBLE_SIZE + byte_len);

/**
* If packet starts with `ACK_PACKET`, the `byte_len` is 0, and the whole array is just 0,
* then this is an ack packet.
*/
const isAck = (cmd & ACK_PACKET) === ACK_PACKET && byte_len === 0 && dataBody.byteLength === 0;

if ((cmd & PACKET_FLAG_START_OF_COMMAND) === PACKET_FLAG_START_OF_COMMAND && state.currentPacket.length > 0) {
if (cmd & PACKET_FLAG_START_OF_COMMAND && state.currentPacket.length > 0) {
/**
* When we get a start packet, the read buffer should already be empty. If it isn't,
* we got a command that didn't end with an END_OF_COMMAND packet and something is wrong.
Expand Down Expand Up @@ -85,7 +94,11 @@ export const collatePackets: StateF<DataView, PacketHandlingState, Packet> = (st

if ((cmd & PACKET_FLAG_END_OF_COMMAND) === PACKET_FLAG_END_OF_COMMAND) {
newState = { currentPacket: new Uint8Array(0) };
eventsToPass.push({ type: "data", payload: nextPacket });
if (isAck) {
eventsToPass.push({ type: "ack" });
} else {
eventsToPass.push({ type: "data", payload: nextPacket });
}
}

return [newState, eventsToPass.map((e) => new Bacon.Next(e))];
Expand Down
20 changes: 20 additions & 0 deletions sdk/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,23 @@ import { MAX_PACKET_SIZE } from "./packet.ts";
export function pad_packet(packet: Array<number>, padding = 0): Uint8Array {
return Uint8Array.from({ length: MAX_PACKET_SIZE }, (_, i) => packet[i] ?? 0);
}

export class RGB {
public r: number;
public g: number;
public b: number;

constructor(r: number, g: number, b: number) {
this.r = this.clamp(r);
this.g = this.clamp(g);
this.b = this.clamp(b);
}

private clamp(value: number) {
return value < 0 ? 0 : value > 255 ? 255 : value;
}

toArray(): Array<number> {
return [this.r, this.g, this.b];
}
}
4 changes: 3 additions & 1 deletion ui/common/typed-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export function TypedSelect<Opts extends string>(props: Props<Opts>) {
return (
<select {...selectProps} onChange={(e) => onOptSelected?.(e.currentTarget.value as Opts)}>
{options.map(([key, label]) => (
<option value={key}>{label}</option>
<option key={key} value={key}>
{label}
</option>
))}
</select>
);
Expand Down
3 changes: 3 additions & 0 deletions ui/pad-coms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,8 @@ type FunctionKeys<T extends object> = keyof {
/** anything here will appear in the debug UI to dispatch at will */
export const DEBUG_COMMANDS: Record<string, FunctionKeys<SMXStage>> = {
requestConfig: "updateConfig",
writeConfig: "writeConfig",
requestTestData: "updateTestData",
forceRecalibration: "forceRecalibration",
factoryReset: "factoryReset",
};
13 changes: 12 additions & 1 deletion ui/stage/stage-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { timez } from "./util";
const UI_UPDATE_RATE = 50;

function useInputState(stage: SMXStage | undefined) {
const readTestData = useAtomValue(displayTestData$);
const [panelStates, setPanelStates] = useState<Array<boolean> | null>();
useEffect(() => {
return stage?.inputState$.throttle(UI_UPDATE_RATE).onValue(setPanelStates);
Expand All @@ -25,7 +26,17 @@ function useTestData(stage: SMXStage | undefined) {
if (!stage || !testDataMode) {
return;
}
const testMode = testDataMode === "raw" ? SensorTestMode.UncalibratedValues : SensorTestMode.CalibratedValues;
let testMode = SensorTestMode.UncalibratedValues;
switch (testDataMode) {
case "calibrated":
testMode = SensorTestMode.CalibratedValues;
break;
case "noise":
testMode = SensorTestMode.Noise;
break;
case "tare":
testMode = SensorTestMode.Tare;
}
const handle = setInterval(() => stage.updateTestData(testMode), UI_UPDATE_RATE);
return () => clearInterval(handle);
}, [stage, testDataMode]);
Expand Down
4 changes: 2 additions & 2 deletions ui/state.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { atom, createStore } from "jotai";
import type { SMXStage } from "../sdk";
import { SensorTestMode, type SMXStage } from "../sdk";

export const browserSupported = "hid" in navigator;

Expand All @@ -9,7 +9,7 @@ export const uiState = createStore();
/** backing atom of all known stages */
export const stages$ = atom<Record<number, SMXStage | undefined>>({});

export const displayTestData$ = atom<"" | "raw" | "calibrated">("");
export const displayTestData$ = atom<"" | "raw" | "calibrated" | "noise" | "tare">("");

/** current p1 pad, derived from devices$ above */
export const p1Stage$ = atom<SMXStage | undefined, [SMXStage | undefined], void>(
Expand Down
Loading

0 comments on commit 6203b0a

Please sign in to comment.