diff --git a/sdk/commands/sensor_test.ts b/sdk/commands/sensor_test.ts index 2b1cc15..fdaa829 100644 --- a/sdk/commands/sensor_test.ts +++ b/sdk/commands/sensor_test.ts @@ -93,7 +93,7 @@ export class SMXPanelTestData { dip_switch_value = -1; bad_jumper: Array = Array(SENSOR_COUNT).fill(false); - constructor(data: Decoded, mode: SensorTestMode) { + constructor(data: Decoded, mode: SensorTestMode, isFsr: boolean) { /** * Check the header. this is always `false true false` or `0 1 0` to identify it as a response, * and not as random steps from the player. @@ -134,10 +134,10 @@ export class SMXPanelTestData { * These are signed as they can be negative, but I imagine them going * negative is just kind of noise from the hardware. */ - this.sensor_level = data.sensors.map((value) => this.clamp_sensor_value(value, mode)); + this.sensor_level = data.sensors.map((value) => this.clamp_sensor_value(value, mode, isFsr)); } - private clamp_sensor_value(value: number, mode: SensorTestMode) { + private clamp_sensor_value(value: number, mode: SensorTestMode, isFsr: boolean) { if (mode === SensorTestMode.Noise) { /** * In Noise mode, we receive standard deviation values squared. @@ -145,18 +145,13 @@ export class SMXPanelTestData { * This makes the number different than the configured value * (square it to convert back), but without this we display a bunch * of four and five digit numbers that are too hard to read. - * - * TODO: Do we want to round this value or just display decimal values? */ return Math.sqrt(value); } - // TODO: We need a way to pass in if the stage we are getting this data for - // is using FSRs or not. Defined as `true` for now. - const isFSR = true; - // TODO: This may be necessary for defining sensor value vertial bars in the UI - // const max_value = isFSR ? 250 : 500; + // This will probably go in the UI and not here + // const max_value = isFsr ? 250 : 500; let clamped_value = value; /** @@ -169,7 +164,7 @@ export class SMXPanelTestData { } // FSR values are bitshifted right by 2 (effectively a div by 4). - if (isFSR) { + if (isFsr) { clamped_value >>= 2; } @@ -180,7 +175,7 @@ export class SMXPanelTestData { export class SMXSensorTestData { panels: Array = []; - constructor(data: Array, mode: SensorTestMode) { + constructor(data: Array, mode: SensorTestMode, isFsr: boolean) { /** * The first 3 bytes are the preamble. * @@ -236,7 +231,7 @@ export class SMXSensorTestData { } // Generate an SMXPanelTestData object for each panel - this.panels.push(new SMXPanelTestData(detail_data_t.decode(out_bytes, true), data_mode)); + this.panels.push(new SMXPanelTestData(detail_data_t.decode(out_bytes, true), data_mode, isFsr)); } } } diff --git a/sdk/index.ts b/sdk/index.ts index 41aa464..538132a 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -1,95 +1,16 @@ -import * as Bacon from "baconjs"; -import type { StateF } from "baconjs/types/withstatemachine"; -import { - PACKET_FLAG_DEVICE_INFO, - PACKET_FLAG_END_OF_COMMAND, - PACKET_FLAG_HOST_CMD_FINISHED, - PACKET_FLAG_START_OF_COMMAND, -} from "./packet"; - -// TODO: Probably move all this bacon packet stuff to `packet.js`? -interface PacketHandlingState { - currentPacket: Uint8Array; -} - -function hasValue(ev: Bacon.Event): ev is Bacon.Value { - return ev.hasValue; -} - -export type Packet = { type: "host_cmd_finished"; payload: [] } | { type: "data"; payload: Uint8Array }; - -/** - * Gets called when a packet is received, returns a tuple of new state and an array of +/* + * This file exports our public API to external consumers */ -export const handlePacket: StateF = (state, event) => { - if (!hasValue(event)) { - console.log("No Event Value"); - return [state, []]; - } - - let currentPacket = state.currentPacket; - const data = new Uint8Array(event.value.buffer); - - // console.log("Raw Packet Data: ", data); - - // Return if packet is empty - if (data.length <= 3) { - console.log("Empty Packet"); - return [state, []]; - } - const cmd = data[0]; - const byte_len = data[1]; - - if (cmd & PACKET_FLAG_DEVICE_INFO) { - // This is a response to RequestDeviceInfo. Since any application can send this, - // we ignore the packet if we didn't request it, since it might be requested - // for a different program. - // TODO: Handle this? Not sure there's anything to handle here tbh - console.log("Found Packet Flag Device Info"); - } - - // TODO: Make some consts for these 2's everywhere - if (2 + byte_len > data.length) { - // TODO: Can this even happen??? - console.log("Communication Error: Oversized Packet (ignored)"); - return [state, []]; - } - - // The data exists after the first 2 bytes - const dataBody = data.slice(2, 2 + byte_len); - - if ((cmd & PACKET_FLAG_START_OF_COMMAND) === 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. - * This shouldn't happen, so warn about it and recover by clearing the junk in the buffer. - * TODO: Again, does this actually happen???!? - */ - console.log( - "Got PACKET_FLAG_OF_START_COMMAND, but we had ${current_packet.length} bytes in the buffer. Dropping it and continuing.", - ); - currentPacket = new Uint8Array(0); - } - - // concat the new data onto the current packet - const nextPacket = new Uint8Array(currentPacket.byteLength + dataBody.byteLength); - nextPacket.set(currentPacket); - nextPacket.set(dataBody, currentPacket.byteLength); - const eventsToPass: Packet[] = []; - - let newState = { currentPacket: nextPacket }; - - // Note that if PACKET_FLAG_HOST_CMD_FINISHED is set, PACKET_FLAG_END_OF_COMMAND will also be set - if ((cmd & PACKET_FLAG_HOST_CMD_FINISHED) === PACKET_FLAG_HOST_CMD_FINISHED) { - // This tells us that a command we wrote to the device has finished executing, and it's safe to start writing another. - //console.log("Packet Complete"); - eventsToPass.push({ type: "host_cmd_finished", payload: [] }); - } - - if ((cmd & PACKET_FLAG_END_OF_COMMAND) === PACKET_FLAG_END_OF_COMMAND) { - newState = { currentPacket: new Uint8Array(0) }; - eventsToPass.push({ type: "data", payload: nextPacket }); - } - return [newState, eventsToPass.map((e) => new Bacon.Next(e))]; -}; +export { + SMX_USB_PRODUCT_ID, + SMX_USB_PRODUCT_NAME, + SMX_USB_VENDOR_ID, + Sensor, + Panel, + SENSOR_COUNT, + PANEL_COUNT, +} from "./api.js"; +export { type PanelName, type EachPanel } from "./commands/inputs.js"; +export { SensorTestMode, type SMXPanelTestData, type SMXSensorTestData } from "./commands/sensor_test.js"; +export { SMXStage } from "./smx.js"; diff --git a/sdk/packet.ts b/sdk/packet.ts index 58b6a7c..3f984a9 100644 --- a/sdk/packet.ts +++ b/sdk/packet.ts @@ -1,37 +1,5 @@ import { pad_packet } from "./utils.ts"; -/** - * Gets the next report from the device matching a given reportId, - * wrapped in a promise for convenience. Times out after 500ms looking - * for a response. - */ -export function nextReportCommand(dev: HIDDevice): Promise { - return new Promise((resolve, reject) => { - // set a 500ms timeout - // TODO: Should this be longer? - const timeoutHandle = window.setTimeout(() => { - // stop listening - dev.removeEventListener("inputreport", handleReport); - reject("timeout"); - }, 500); - - function handleReport(event: HIDInputReportEvent) { - // TODO other more specific filtering here? - if (event.reportId !== HID_REPORT_INPUT) { - return; // not the report we're looking for - } - // stop listening - dev.removeEventListener("inputreport", handleReport); - // stop timeout - clearTimeout(timeoutHandle); - // return data to caller - resolve(event.data); - } - - dev.addEventListener("inputreport", handleReport); - }); -} - /* StepManiaX Stages expect packets that are exactly 64-bytes in length. @@ -43,7 +11,7 @@ Thus, we're going to set the packet length to 63, since the Report ID will be added to the data going out, making it 64 bits. */ export const MAX_PACKET_SIZE = 63; -const PACKET_PREAMBLE_SIZE = 2; +export const PACKET_PREAMBLE_SIZE = 2; // USB Communication Packet Flags export const PACKET_FLAG_START_OF_COMMAND = 0x04; @@ -56,9 +24,19 @@ export const HID_REPORT_INPUT_STATE = 0x03; export const HID_REPORT_OUTPUT = 0x05; export const HID_REPORT_INPUT = 0x06; -// Acknowledge Code -// TODO: Decide what to do with this -const ACK_COMMAND = 0x7; +export async function send_data(dev: HIDDevice, data: Array, debug = false) { + // Split data into packets + const packets = make_packets(data); + + if (debug) { + console.log("Sending Packets: ", packets); + } + + // Send each packet + for (const packet of packets) { + await dev.sendReport(HID_REPORT_OUTPUT, packet); + } +} export function make_packets(data: Array): Array { const packets = []; @@ -100,27 +78,3 @@ export function make_packets(data: Array): Array { return packets; } - -export async function requestSpecialDeviceInfo(dev: HIDDevice, debug = false) { - const packet = pad_packet([PACKET_FLAG_DEVICE_INFO]); - - if (debug) { - console.log("Sending Packets: ", packet); - } - - await dev.sendReport(HID_REPORT_OUTPUT, packet); -} - -export async function send_data(dev: HIDDevice, data: Array, debug = false) { - // Split data into packets - const packets = make_packets(data); - - if (debug) { - console.log("Sending Packets: ", packets); - } - - // Send each packet - for (const packet of packets) { - await dev.sendReport(HID_REPORT_OUTPUT, packet); - } -} diff --git a/sdk/smx.ts b/sdk/smx.ts index d6c0287..44568db 100644 --- a/sdk/smx.ts +++ b/sdk/smx.ts @@ -1,124 +1,181 @@ import * as Bacon from "baconjs"; -import { handlePacket } from "./index"; -import type { Packet } from "./index"; +import { collatePackets, type DataPacket } from "./state-machines/collate-packets"; import { API_COMMAND, char2byte } from "./api"; -import { SMXConfig } from "./commands/config"; +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, - PACKET_FLAG_DEVICE_INFO, - requestSpecialDeviceInfo, - send_data, -} from "./packet"; +import { HID_REPORT_INPUT, HID_REPORT_INPUT_STATE, send_data } from "./packet"; import { SMXSensorTestData, SensorTestMode } from "./commands/sensor_test"; +/** + * Class purely to set up in/out event stream "pipes" to properly throttle and sync input/output from a stage + * this does not read or write to a stage at all. consumers can write to `output$`, and pass throttled events from + * `eventsToSend$` on to a stage. Read from `inputState$` to see panel pressed state reports, and from `otherReports$` + * to see responses to commands sent to the stage. + **/ class SMXEvents { - input$; - inputState$; - otherReports$; + /** read from this to see the constant on/off state reports for all panels */ + public readonly inputState$; + /** read from this to see all reports sent by the stage in response to a command */ + public readonly otherReports$: Bacon.EventStream; + /** push to this to write commands to the stage */ + public readonly output$: Bacon.Bus; + /** this is everything pushed to `output$` but properly throttled/timed to the device's ack responses */ + public readonly eventsToSend$: Bacon.EventStream; constructor(dev: HIDDevice) { // Main USB Ingestor - this.input$ = Bacon.fromEvent(dev, "inputreport"); + const rawReport$ = Bacon.fromEvent(dev, "inputreport"); // Panel Input State (If a panel is active or not) - this.inputState$ = this.input$ + this.inputState$ = rawReport$ .filter((e) => e.reportId === HID_REPORT_INPUT_STATE) .map((e) => StageInputs.decode(e.data, true)); // All other reports (command responses) - this.otherReports$ = this.input$ + const report$ = rawReport$ .filter((e) => e.reportId === HID_REPORT_INPUT) .map((e) => e.data) .filter((d) => d.byteLength !== 0) - .withStateMachine({ currentPacket: new Uint8Array() }, handlePacket); + .withStateMachine({ currentPacket: new Uint8Array() }, collatePackets); - // this.otherReports$.onValue((value) => console.log("Packet: ", value)); + this.otherReports$ = (report$.filter((e) => e.type === "data") as Bacon.EventStream).map( + (e) => e.payload, + ); + + const finishedCommand$ = report$ + .filter((e) => e.type === "host_cmd_finished") + .map((e) => e.type === "host_cmd_finished"); + + // finishedCommand$.log("Cmd Finished"); + + const okSend$ = finishedCommand$.startWith(true); + + // Main USB Output + this.output$ = new Bacon.Bus>(); + + // Config writes should only happen at most once per second. + const configOutput$ = this.output$.filter((e) => e[0] === API_COMMAND.WRITE_CONFIG_V5).throttle(1000); + + // All other writes are passed through unchanged + const otherOutput$ = this.output$.filter((e) => e[0] !== API_COMMAND.WRITE_CONFIG_V5); + + // combine together the throttled and unthrottled writes + // and only emit one per each "ok" signal we get back following each previous output + this.eventsToSend$ = configOutput$.merge(otherOutput$).zip(okSend$, (nextToSend) => nextToSend); } } export class SMXStage { private dev: HIDDevice; - private events: SMXEvents; + public readonly events: SMXEvents; info: SMXDeviceInfo | null = null; config: SMXConfig | null = null; test: SMXSensorTestData | null = null; - private test_mode: SensorTestMode = SensorTestMode.CalibratedValues; - debug = true; + inputs: Array | null = null; + private test_mode: SensorTestMode = SensorTestMode.CalibratedValues; // TODO: Maybe we just let this be public + private debug = false; + + private configResponse$: Bacon.EventStream; constructor(dev: HIDDevice) { this.dev = dev; this.events = new SMXEvents(this.dev); + // write outgoing events to the device + this.events.eventsToSend$.onValue(async (value) => { + this.debug && console.log("writing to HID"); + await send_data(this.dev, value, this.debug); + }); + // Set the device info handler this.events.otherReports$ - .filter((e) => e.payload[0] === char2byte("I")) // We send 'i' but for some reason we get back 'I' + .filter((e) => e[0] === char2byte("I")) // We send 'i' but for some reason we get back 'I' .onValue((value) => this.handleDeviceInfo(value)); // Set the config request handler - this.events.otherReports$ - .filter((e) => e.payload[0] === API_COMMAND.GET_CONFIG_V5) - .onValue((value) => this.handleConfig(value)); + this.configResponse$ = this.events.otherReports$ + .filter((e) => e[0] === API_COMMAND.GET_CONFIG_V5) + .map((value) => this.handleConfig(value)); + // note that the above map function only runs when there are listeners + // subscribed to `this.configResponse$`, otherwise nothing happens! // Set the test data request handler this.events.otherReports$ - .filter((e) => e.payload[0] === API_COMMAND.GET_SENSOR_TEST_DATA) + .filter((e) => e[0] === API_COMMAND.GET_SENSOR_TEST_DATA) .onValue((value) => this.handleTestData(value)); - } - async init() { - /** - * This is a special RequestDeviceInfo packet. This is the same as sending an - * 'i' command, but we can send it safely at any time, even if another - * application is talking to the device. Thus we can do this during enumeration. - */ - await requestSpecialDeviceInfo(this.dev); + // Set the inputs data request handler + this.events.inputState$.onValue((value) => this.handleInputs(value)); + } - // Request the config for this stage - await this.updateConfig(); + async init(): Promise { + // Request the device information + this.updateDeviceInfo(); // Request some initial test data - await this.updateTestData(); + this.updateTestData(); + + // Request the config for this stage + return this.updateConfig(); } - async updateDeviceInfo() { - await send_data(this.dev, [API_COMMAND.GET_DEVICE_INFO]); + updateDeviceInfo() { + this.events.output$.push([API_COMMAND.GET_DEVICE_INFO]); } - async updateConfig() { - await send_data(this.dev, [API_COMMAND.GET_CONFIG_V5]); + updateConfig(): Promise { + this.events.output$.push([API_COMMAND.GET_CONFIG_V5]); + return this.configResponse$.firstToPromise(); } - async updateTestData(mode: SensorTestMode | null = null) { + updateTestData(mode: SensorTestMode | null = null): void { if (mode) { this.test_mode = mode; } - await send_data(this.dev, [API_COMMAND.GET_SENSOR_TEST_DATA, this.test_mode]); + this.events.output$.push([API_COMMAND.GET_SENSOR_TEST_DATA, this.test_mode]); } - private handleConfig(data: Packet) { - this.config = new SMXConfig(Array.from(data.payload)); + private handleConfig(data: Uint8Array) { + 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); - console.log("Config Encodes Correctly: ", data.payload.slice(2, -1).toString() === buf.toString()); + this.debug && console.log("Config Encodes Correctly: ", data.slice(2, -1).toString() === buf.toString()); } - console.log("Got Config: ", this.config); + this.debug && console.log("Got Config: ", this.config); + return this.config; } - private handleTestData(data: Packet) { - this.test = new SMXSensorTestData(Array.from(data.payload), this.test_mode); + private handleTestData(data: Uint8Array) { + this.test = new SMXSensorTestData( + Array.from(data), + this.test_mode, + this.config?.config?.flags?.PlatformFlags_FSR || true, + ); - console.log("Got Test: ", this.test); + this.debug && console.log("Got Test: ", this.test); } - private handleDeviceInfo(data: Packet) { - this.info = new SMXDeviceInfo(Array.from(data.payload)); + private handleDeviceInfo(data: Uint8Array) { + this.info = new SMXDeviceInfo(Array.from(data)); + + this.debug && console.log("Got Info: ", this.info); + } - console.log("Got Info: ", this.info); + private handleInputs(data: Decoded) { + this.inputs = [ + data.up_left, + data.up, + data.up_right, + data.left, + data.center, + data.right, + data.down_left, + data.down, + data.down_right, + ]; } } diff --git a/sdk/state-machines/collate-packets.ts b/sdk/state-machines/collate-packets.ts new file mode 100644 index 0000000..a7660ca --- /dev/null +++ b/sdk/state-machines/collate-packets.ts @@ -0,0 +1,92 @@ +import * as Bacon from "baconjs"; +import type { StateF } from "baconjs/types/withstatemachine"; +import { + PACKET_FLAG_DEVICE_INFO, + PACKET_FLAG_END_OF_COMMAND, + PACKET_FLAG_HOST_CMD_FINISHED, + PACKET_FLAG_START_OF_COMMAND, + PACKET_PREAMBLE_SIZE, +} from "../packet"; + +interface PacketHandlingState { + currentPacket: Uint8Array; +} + +export type DataPacket = { type: "data"; payload: Uint8Array }; +export type Packet = { type: "host_cmd_finished" } | DataPacket; + +/** + * Gets called when a packet is received, returns a tuple of new state and an array of + */ +export const collatePackets: StateF = (state, event) => { + // TODO: This whole function could maybe just use a bit more comments + if (!Bacon.hasValue(event)) { + console.log("No Event Value"); + return [state, []]; + } + + let currentPacket = state.currentPacket; + const data = new Uint8Array(event.value.buffer); + + // console.log("Raw Packet Data: ", data); + + // Return if packet is empty + if (data.length <= PACKET_PREAMBLE_SIZE) { + console.log("Empty Packet"); + return [state, []]; + } + const cmd = data[0]; + const byte_len = data[1]; + + if (cmd & PACKET_FLAG_DEVICE_INFO) { + // This is a response to RequestDeviceInfo. Since any application can send this, + // we ignore the packet if we didn't request it, since it might be requested + // for a different program. + // TODO: Handle this? Not sure there's anything to handle here tbh + console.log("Found Packet Flag Device Info"); + } + + if (PACKET_PREAMBLE_SIZE + byte_len > data.length) { + // TODO: Can this even happen??? + console.log("Communication Error: Oversized Packet (ignored)"); + return [state, []]; + } + + // The data exists after the first 2 bytes + const dataBody = data.slice(2, 2 + byte_len); + + if ((cmd & PACKET_FLAG_START_OF_COMMAND) === 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. + * This shouldn't happen, so warn about it and recover by clearing the junk in the buffer. + * TODO: Again, does this actually happen???!? + */ + console.log( + "Got PACKET_FLAG_OF_START_COMMAND, but we had ${current_packet.length} bytes in the buffer. Dropping it and continuing.", + ); + currentPacket = new Uint8Array(0); + } + + // concat the new data onto the current packet + const nextPacket = new Uint8Array(currentPacket.byteLength + dataBody.byteLength); + nextPacket.set(currentPacket); + nextPacket.set(dataBody, currentPacket.byteLength); + const eventsToPass: Packet[] = []; + + let newState = { currentPacket: nextPacket }; + + // Note that if PACKET_FLAG_HOST_CMD_FINISHED is set, PACKET_FLAG_END_OF_COMMAND will also be set + if ((cmd & PACKET_FLAG_HOST_CMD_FINISHED) === PACKET_FLAG_HOST_CMD_FINISHED) { + // This tells us that a command we wrote to the device has finished executing, and it's safe to start writing another. + //console.log("Packet Complete"); + eventsToPass.push({ type: "host_cmd_finished" }); + } + + if ((cmd & PACKET_FLAG_END_OF_COMMAND) === PACKET_FLAG_END_OF_COMMAND) { + newState = { currentPacket: new Uint8Array(0) }; + eventsToPass.push({ type: "data", payload: nextPacket }); + } + + return [newState, eventsToPass.map((e) => new Bacon.Next(e))]; +}; diff --git a/ui/pad-coms.ts b/ui/pad-coms.ts index 655de00..df940ef 100644 --- a/ui/pad-coms.ts +++ b/ui/pad-coms.ts @@ -1,5 +1,4 @@ -import { SMX_USB_PRODUCT_ID, SMX_USB_VENDOR_ID } from "../sdk/api"; -import { SMXStage } from "../sdk/smx"; +import { SMX_USB_PRODUCT_ID, SMX_USB_VENDOR_ID, SMXStage } from "../sdk"; import { stages$, nextStatusTextLine$, statusText$, uiState } from "./state"; export async function promptSelectDevice() { diff --git a/ui/stage/fsr-panel.tsx b/ui/stage/fsr-panel.tsx index 2637eab..eb77447 100644 --- a/ui/stage/fsr-panel.tsx +++ b/ui/stage/fsr-panel.tsx @@ -1,6 +1,5 @@ import cn from "classnames"; -import type { SMXPanelTestData } from "../../sdk/commands/sensor_test"; -import { Sensor } from "../../sdk/api"; +import { Sensor, type SMXPanelTestData } from "../../sdk"; interface EnabledProps { data: SMXPanelTestData; diff --git a/ui/stage/stage-test.tsx b/ui/stage/stage-test.tsx index 9daf6ec..25c08fa 100644 --- a/ui/stage/stage-test.tsx +++ b/ui/stage/stage-test.tsx @@ -1,27 +1,29 @@ import { useAtomValue, type Atom } from "jotai"; import { useEffect, useState } from "react"; -import { SensorTestMode, type SMXPanelTestData, type SMXSensorTestData } from "../../sdk/commands/sensor_test"; import { FsrPanel } from "./fsr-panel"; -import { StageInputs, type EachPanel, type PanelName } from "../../sdk/commands/inputs"; +import { type SMXStage, SensorTestMode, type SMXPanelTestData, type SMXSensorTestData } from "../../sdk/"; import { displayTestData$ } from "../state"; -import type { SMXStage } from "../../sdk/smx"; -/* -function useInputState(dev: HIDDevice | undefined) { - const [panelStates, setPanelStates] = useState | undefined>(); +// UI Update Rate in Milliseconds +const UI_UPDATE_RATE = 50; + +function useInputState(stage: SMXStage | undefined) { + const [panelStates, setPanelStates] = useState | null>(); + useEffect(() => { - if (!dev) return; - function handleInputReport(e: HIDInputReportEvent) { - if (e.reportId !== HID_REPORT_INPUT_STATE) return; - const state = StageInputs.decode(e.data, true); - setPanelStates(state); + if (!stage) return; + + const d = stage; + async function update() { + setPanelStates(d.inputs); } - dev.addEventListener("inputreport", handleInputReport); - return () => dev.removeEventListener("inputreport", handleInputReport); - }, [dev]); + + const handle = setInterval(update, UI_UPDATE_RATE); + return () => clearInterval(handle); + }, [stage]); + return panelStates; } -*/ function useTestData(stage: SMXStage | undefined) { const readTestData = useAtomValue(displayTestData$); @@ -34,13 +36,11 @@ function useTestData(stage: SMXStage | undefined) { const d = stage; async function update() { - await d.updateTestData(SensorTestMode.CalibratedValues); + d.updateTestData(SensorTestMode.CalibratedValues); setTestData(d.test); - handle = requestAnimationFrame(update); } - let handle = setInterval(update, 50); - + const handle = setInterval(update, UI_UPDATE_RATE); return () => clearInterval(handle); }, [stage, readTestData]); @@ -54,18 +54,18 @@ export function StageTest({ }) { const stage = useAtomValue(stageAtom); const testData = useTestData(stage); - // const inputState = useInputState(stage); + const inputState = useInputState(stage); - if (!testData) { + if (!testData || !inputState) { return null; } - const entries = Object.entries(testData.panels) as [PanelName, SMXPanelTestData][]; + const entries = Object.entries(testData.panels) as [string, SMXPanelTestData][]; return (
{entries.map(([key, data]) => ( - + ))}
); diff --git a/ui/state.ts b/ui/state.ts index fc5ec12..3f02195 100644 --- a/ui/state.ts +++ b/ui/state.ts @@ -1,5 +1,5 @@ import { atom, createStore } from "jotai"; -import type { SMXStage } from "../sdk/smx"; +import type { SMXStage } from "../sdk"; export const browserSupported = "hid" in navigator;