From 47f6af8e47b457788b4be503d9a8219dda7248ed Mon Sep 17 00:00:00 2001 From: Fernando Chorney Date: Fri, 5 Apr 2024 09:23:37 -0500 Subject: [PATCH 01/12] Get bacon output workin --- sdk/smx.ts | 70 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/sdk/smx.ts b/sdk/smx.ts index d6c0287..41fc957 100644 --- a/sdk/smx.ts +++ b/sdk/smx.ts @@ -2,24 +2,28 @@ import * as Bacon from "baconjs"; import { handlePacket } from "./index"; import type { Packet } from "./index"; 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 { SMXSensorTestData, SensorTestMode } from "./commands/sensor_test"; class SMXEvents { + private dev; + private dontSend: Bacon.Property; input$; inputState$; otherReports$; + output$; constructor(dev: HIDDevice) { + this.dev = dev; + // Main USB Ingestor this.input$ = Bacon.fromEvent(dev, "inputreport"); @@ -35,7 +39,31 @@ class SMXEvents { .filter((d) => d.byteLength !== 0) .withStateMachine({ currentPacket: new Uint8Array() }, handlePacket); + this.dontSend = this.otherReports$ + .filter((e) => e.type === 'host_cmd_finished') + .map((e) => e.type !== "host_cmd_finished") + .toProperty(false); + // this.otherReports$.onValue((value) => console.log("Packet: ", value)); + + // 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) + .holdWhen(this.dontSend) + .onValue(async (value) => await this.writeToHID(value)); + + const otherOutput$ = this.output$ + .filter((e) => e[0] !== API_COMMAND.WRITE_CONFIG_V5) + .holdWhen(this.dontSend) + .onValue(async (value) => await this.writeToHID(value)); + } + + private async writeToHID(value: Array) { + await send_data(this.dev, value); } } @@ -45,8 +73,9 @@ export class SMXStage { 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 = true; constructor(dev: HIDDevice) { this.dev = dev; @@ -66,6 +95,9 @@ export class SMXStage { this.events.otherReports$ .filter((e) => e.payload[0] === API_COMMAND.GET_SENSOR_TEST_DATA) .onValue((value) => this.handleTestData(value)); + + // Set the inputs data request handler + this.events.inputState$.onValue((value) => this.handleInputs(value)); } async init() { @@ -77,25 +109,25 @@ export class SMXStage { await requestSpecialDeviceInfo(this.dev); // Request the config for this stage - await this.updateConfig(); + this.updateConfig(); // Request some initial test data - await this.updateTestData(); + this.updateTestData(); } - 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() { + this.events.output$.push([API_COMMAND.GET_CONFIG_V5]); } - async updateTestData(mode: SensorTestMode | null = null) { + updateTestData(mode: SensorTestMode | null = null) { 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) { @@ -121,4 +153,18 @@ export class SMXStage { 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, + ]; + } } From edcdb9ccbf25e51692485d8b3394be27f10c66a9 Mon Sep 17 00:00:00 2001 From: Fernando Chorney Date: Fri, 5 Apr 2024 14:00:10 -0500 Subject: [PATCH 02/12] Testing more bacon --- sdk/smx.ts | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/sdk/smx.ts b/sdk/smx.ts index 41fc957..f4ae7e2 100644 --- a/sdk/smx.ts +++ b/sdk/smx.ts @@ -15,7 +15,7 @@ import { SMXSensorTestData, SensorTestMode } from "./commands/sensor_test"; class SMXEvents { private dev; - private dontSend: Bacon.Property; + private startedSend$: Bacon.Bus; input$; inputState$; otherReports$; @@ -39,12 +39,20 @@ class SMXEvents { .filter((d) => d.byteLength !== 0) .withStateMachine({ currentPacket: new Uint8Array() }, handlePacket); - this.dontSend = this.otherReports$ + // this.otherReports$.onValue((value) => console.log("Packet: ", value)); + + const finishedCommand$ = this.otherReports$ .filter((e) => e.type === 'host_cmd_finished') - .map((e) => e.type !== "host_cmd_finished") - .toProperty(false); + .map((e) => e.type === 'host_cmd_finished') + .map((e) => !e); - // this.otherReports$.onValue((value) => console.log("Packet: ", value)); + this.startedSend$ = new Bacon.Bus(); + + // false means "it's ok to send", true means "don't send" + const dontSend$ = new Bacon.Bus() + .merge(finishedCommand$) // Returns false when host_cmd_finished + .merge(this.startedSend$) // Return true when starting to send + .toProperty(false); // Main USB Output this.output$ = new Bacon.Bus>(); @@ -52,17 +60,20 @@ class SMXEvents { // 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) - .holdWhen(this.dontSend) - .onValue(async (value) => await this.writeToHID(value)); + .throttle(1000); const otherOutput$ = this.output$ - .filter((e) => e[0] !== API_COMMAND.WRITE_CONFIG_V5) - .holdWhen(this.dontSend) + .filter((e) => e[0] !== API_COMMAND.WRITE_CONFIG_V5); + + const combinedOutput$ = new Bacon.Bus>() + .merge(configOutput$) + .merge(otherOutput$) + .holdWhen(dontSend$) .onValue(async (value) => await this.writeToHID(value)); } private async writeToHID(value: Array) { + this.startedSend$.push(true); await send_data(this.dev, value); } } From b1d588f7eea32051321cadf2e9e46b0ded766b61 Mon Sep 17 00:00:00 2001 From: Fernando Chorney Date: Fri, 5 Apr 2024 14:38:58 -0500 Subject: [PATCH 03/12] sort of works --- sdk/packet.ts | 2 +- sdk/smx.ts | 28 ++++++++++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/sdk/packet.ts b/sdk/packet.ts index 58b6a7c..b1b9f67 100644 --- a/sdk/packet.ts +++ b/sdk/packet.ts @@ -101,7 +101,7 @@ export function make_packets(data: Array): Array { return packets; } -export async function requestSpecialDeviceInfo(dev: HIDDevice, debug = false) { +export async function makeSpecialDevicePacket(dev: HIDDevice, debug = false) { const packet = pad_packet([PACKET_FLAG_DEVICE_INFO]); if (debug) { diff --git a/sdk/smx.ts b/sdk/smx.ts index f4ae7e2..347616e 100644 --- a/sdk/smx.ts +++ b/sdk/smx.ts @@ -41,18 +41,21 @@ class SMXEvents { // this.otherReports$.onValue((value) => console.log("Packet: ", value)); + this.otherReports$ + .filter((e) => e.type === 'host_cmd_finished') + .onValue((e) => console.log("Cmd Finished")); + const finishedCommand$ = this.otherReports$ .filter((e) => e.type === 'host_cmd_finished') - .map((e) => e.type === 'host_cmd_finished') - .map((e) => !e); + .map((e) => e.type === 'host_cmd_finished'); this.startedSend$ = new Bacon.Bus(); - // false means "it's ok to send", true means "don't send" - const dontSend$ = new Bacon.Bus() - .merge(finishedCommand$) // Returns false when host_cmd_finished - .merge(this.startedSend$) // Return true when starting to send - .toProperty(false); + // true means "it's ok to send", false means "don't send" + const okSend$ = new Bacon.Bus() + .merge(finishedCommand$) // Returns true when host_cmd_finished + .merge(this.startedSend$.not()) // Return false when starting to send + .toProperty(true); // Main USB Output this.output$ = new Bacon.Bus>(); @@ -68,12 +71,15 @@ class SMXEvents { const combinedOutput$ = new Bacon.Bus>() .merge(configOutput$) .merge(otherOutput$) - .holdWhen(dontSend$) + .bufferingThrottle(100) + .takeWhile(okSend$) + .doAction(_ => this.startedSend$.push(true)) .onValue(async (value) => await this.writeToHID(value)); } private async writeToHID(value: Array) { - this.startedSend$.push(true); + //this.startedSend$.push(true); + console.log("writing to HID"); await send_data(this.dev, value); } } @@ -117,7 +123,9 @@ export class SMXStage { * '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); + //await requestSpecialDeviceInfo(this.dev); // Modify `send_data` to accept this somehow? + + this.updateDeviceInfo(); // Request the config for this stage this.updateConfig(); From 0ed901b2ff06d99ee65eb0eb8cd0b0c316896b0e Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Fri, 5 Apr 2024 21:13:29 -0700 Subject: [PATCH 04/12] fix test data still being feteched after disabling --- ui/stage/stage-test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/stage/stage-test.tsx b/ui/stage/stage-test.tsx index 9daf6ec..051da98 100644 --- a/ui/stage/stage-test.tsx +++ b/ui/stage/stage-test.tsx @@ -36,7 +36,9 @@ function useTestData(stage: SMXStage | undefined) { async function update() { await d.updateTestData(SensorTestMode.CalibratedValues); setTestData(d.test); - handle = requestAnimationFrame(update); + if (readTestData) { + handle = requestAnimationFrame(update); + } } let handle = setInterval(update, 50); From c49e19012c3c2b514d717fde8b499aef42bb226b Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 6 Apr 2024 00:22:05 -0700 Subject: [PATCH 05/12] reorganize a lot of sdk public interface --- sdk/index.ts | 107 ++++---------------------- sdk/smx.ts | 44 ++++++----- sdk/state-machines/collate-packets.ts | 91 ++++++++++++++++++++++ ui/pad-coms.ts | 3 +- ui/stage/fsr-panel.tsx | 3 +- ui/stage/stage-test.tsx | 10 ++- ui/state.ts | 2 +- 7 files changed, 138 insertions(+), 122 deletions(-) create mode 100644 sdk/state-machines/collate-packets.ts diff --git a/sdk/index.ts b/sdk/index.ts index 41aa464..a225d7e 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 } from "./commands/inputs.js"; +export { SensorTestMode, type SMXPanelTestData, type SMXSensorTestData } from "./commands/sensor_test.js"; +export { SMXStage } from "./smx.js"; diff --git a/sdk/smx.ts b/sdk/smx.ts index 347616e..31f3f34 100644 --- a/sdk/smx.ts +++ b/sdk/smx.ts @@ -1,6 +1,5 @@ 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, type Decoded } from "./commands/config"; import { SMXDeviceInfo } from "./commands/data_info"; @@ -33,22 +32,25 @@ class SMXEvents { .map((e) => StageInputs.decode(e.data, true)); // All other reports (command responses) - this.otherReports$ = this.input$ + const report$ = this.input$ .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"); - this.otherReports$ - .filter((e) => e.type === 'host_cmd_finished') - .onValue((e) => console.log("Cmd Finished")); + // this.otherReports$.onValue((value) => console.log("Packet: ", value)); - const finishedCommand$ = this.otherReports$ - .filter((e) => e.type === 'host_cmd_finished') - .map((e) => e.type === 'host_cmd_finished'); + finishedCommand$.log("Cmd Finished"); + // we write a `true` to this whenever a series of packets is going out to the device this.startedSend$ = new Bacon.Bus(); // true means "it's ok to send", false means "don't send" @@ -100,17 +102,17 @@ export class SMXStage { // 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) + .filter((e) => e[0] === API_COMMAND.GET_CONFIG_V5) .onValue((value) => this.handleConfig(value)); // 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)); // Set the inputs data request handler @@ -149,26 +151,26 @@ export class SMXStage { 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()); + console.log("Config Encodes Correctly: ", data.slice(2, -1).toString() === buf.toString()); } console.log("Got Config: ", 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); 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)); console.log("Got Info: ", this.info); } diff --git a/sdk/state-machines/collate-packets.ts b/sdk/state-machines/collate-packets.ts new file mode 100644 index 0000000..ab96100 --- /dev/null +++ b/sdk/state-machines/collate-packets.ts @@ -0,0 +1,91 @@ +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"; + +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) => { + 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 <= 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" }); + } + + 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 051da98..6bc71d8 100644 --- a/ui/stage/stage-test.tsx +++ b/ui/stage/stage-test.tsx @@ -1,10 +1,14 @@ 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 PanelName, + type SMXStage, + SensorTestMode, + type SMXPanelTestData, + type SMXSensorTestData, +} from "../../sdk/"; import { displayTestData$ } from "../state"; -import type { SMXStage } from "../../sdk/smx"; /* function useInputState(dev: HIDDevice | undefined) { 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; From 00dab56d8cd79b132c65be33a3e2f26593931b4f Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 6 Apr 2024 00:22:48 -0700 Subject: [PATCH 06/12] simplify event pipes a bit more --- sdk/smx.ts | 48 +++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/sdk/smx.ts b/sdk/smx.ts index 31f3f34..facd534 100644 --- a/sdk/smx.ts +++ b/sdk/smx.ts @@ -4,12 +4,7 @@ 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, - 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 SMXEvents { @@ -17,7 +12,7 @@ class SMXEvents { private startedSend$: Bacon.Bus; input$; inputState$; - otherReports$; + otherReports$: Bacon.EventStream; output$; constructor(dev: HIDDevice) { @@ -54,29 +49,28 @@ class SMXEvents { this.startedSend$ = new Bacon.Bus(); // true means "it's ok to send", false means "don't send" - const okSend$ = new Bacon.Bus() - .merge(finishedCommand$) // Returns true when host_cmd_finished - .merge(this.startedSend$.not()) // Return false when starting to send + const okSend$ = finishedCommand$ // Returns true when host_cmd_finished + .merge(this.startedSend$.not()) // Return false when starting to send .toProperty(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); - - const otherOutput$ = this.output$ - .filter((e) => e[0] !== API_COMMAND.WRITE_CONFIG_V5); - - const combinedOutput$ = new Bacon.Bus>() - .merge(configOutput$) - .merge(otherOutput$) - .bufferingThrottle(100) - .takeWhile(okSend$) - .doAction(_ => this.startedSend$.push(true)) - .onValue(async (value) => await this.writeToHID(value)); + // 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 + // TODO find an alternative to using `bufferedThrottle` here + // (seemingly takeWhile lets too much through via race conditions against the okSend property changing) + const eventsToSend$ = configOutput$.merge(otherOutput$).bufferingThrottle(100).takeWhile(okSend$); + + eventsToSend$.onValue(async (value) => { + this.startedSend$.push(true); + await this.writeToHID(value); + }); } private async writeToHID(value: Array) { @@ -88,7 +82,7 @@ class SMXEvents { 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; @@ -148,7 +142,7 @@ export class SMXStage { if (mode) { this.test_mode = mode; } - this.events.output$.push([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: Uint8Array) { From c5ec4a42f3a2151f69c20921bd5953cb6663ec6d Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 6 Apr 2024 00:26:54 -0700 Subject: [PATCH 07/12] re-implement active input state in UI --- sdk/index.ts | 2 +- ui/stage/stage-test.tsx | 21 +++++++-------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/sdk/index.ts b/sdk/index.ts index a225d7e..538132a 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -11,6 +11,6 @@ export { SENSOR_COUNT, PANEL_COUNT, } from "./api.js"; -export { type PanelName } from "./commands/inputs.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/ui/stage/stage-test.tsx b/ui/stage/stage-test.tsx index 6bc71d8..cfef55e 100644 --- a/ui/stage/stage-test.tsx +++ b/ui/stage/stage-test.tsx @@ -7,25 +7,18 @@ import { SensorTestMode, type SMXPanelTestData, type SMXSensorTestData, + type EachPanel, } from "../../sdk/"; import { displayTestData$ } from "../state"; -/* -function useInputState(dev: HIDDevice | undefined) { +function useInputState(stage: SMXStage | undefined) { const [panelStates, setPanelStates] = useState | undefined>(); 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); - } - dev.addEventListener("inputreport", handleInputReport); - return () => dev.removeEventListener("inputreport", handleInputReport); - }, [dev]); + if (!stage) return; + return stage.events.inputState$.onValue(setPanelStates); + }, [stage]); return panelStates; } -*/ function useTestData(stage: SMXStage | undefined) { const readTestData = useAtomValue(displayTestData$); @@ -60,7 +53,7 @@ export function StageTest({ }) { const stage = useAtomValue(stageAtom); const testData = useTestData(stage); - // const inputState = useInputState(stage); + const inputState = useInputState(stage); if (!testData) { return null; @@ -71,7 +64,7 @@ export function StageTest({ return (
{entries.map(([key, data]) => ( - + ))}
); From 731004dce663d0e53edbe84ddee2b6c21ed44aca Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 6 Apr 2024 00:33:55 -0700 Subject: [PATCH 08/12] prototype of returning a command response in a promise --- sdk/smx.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sdk/smx.ts b/sdk/smx.ts index facd534..ad010f9 100644 --- a/sdk/smx.ts +++ b/sdk/smx.ts @@ -90,6 +90,8 @@ export class SMXStage { private test_mode: SensorTestMode = SensorTestMode.CalibratedValues; // TODO: Maybe we just let this be public private debug = true; + private configResponse$: Bacon.EventStream; + constructor(dev: HIDDevice) { this.dev = dev; this.events = new SMXEvents(this.dev); @@ -100,9 +102,11 @@ export class SMXStage { .onValue((value) => this.handleDeviceInfo(value)); // Set the config request handler - this.events.otherReports$ + this.configResponse$ = this.events.otherReports$ .filter((e) => e[0] === API_COMMAND.GET_CONFIG_V5) - .onValue((value) => this.handleConfig(value)); + .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$ @@ -136,6 +140,7 @@ export class SMXStage { updateConfig() { this.events.output$.push([API_COMMAND.GET_CONFIG_V5]); + return this.configResponse$.firstToPromise(); } updateTestData(mode: SensorTestMode | null = null) { @@ -155,6 +160,7 @@ export class SMXStage { console.log("Config Encodes Correctly: ", data.slice(2, -1).toString() === buf.toString()); } console.log("Got Config: ", this.config); + return this.config; } private handleTestData(data: Uint8Array) { From c5110a510f12110661d8662b1308f48fab27ca28 Mon Sep 17 00:00:00 2001 From: Fernando Chorney Date: Sat, 6 Apr 2024 12:38:18 -0500 Subject: [PATCH 09/12] Various fixes - Stage Test Value checkbox now correctly stops testing when unselected - Input visualization works, but seems laggy? --- sdk/smx.ts | 13 ++++++------- ui/stage/stage-test.tsx | 29 ++++++++++------------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/sdk/smx.ts b/sdk/smx.ts index ad010f9..6451d06 100644 --- a/sdk/smx.ts +++ b/sdk/smx.ts @@ -74,7 +74,6 @@ class SMXEvents { } private async writeToHID(value: Array) { - //this.startedSend$.push(true); console.log("writing to HID"); await send_data(this.dev, value); } @@ -117,7 +116,7 @@ export class SMXStage { this.events.inputState$.onValue((value) => this.handleInputs(value)); } - async init() { + async init(): Promise { /** * 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 @@ -127,23 +126,23 @@ export class SMXStage { this.updateDeviceInfo(); - // Request the config for this stage - this.updateConfig(); - // Request some initial test data this.updateTestData(); + + // Request the config for this stage + return this.updateConfig(); } updateDeviceInfo() { this.events.output$.push([API_COMMAND.GET_DEVICE_INFO]); } - updateConfig() { + updateConfig(): Promise { this.events.output$.push([API_COMMAND.GET_CONFIG_V5]); return this.configResponse$.firstToPromise(); } - updateTestData(mode: SensorTestMode | null = null) { + updateTestData(mode: SensorTestMode | null = null): void { if (mode) { this.test_mode = mode; } diff --git a/ui/stage/stage-test.tsx b/ui/stage/stage-test.tsx index cfef55e..07184bd 100644 --- a/ui/stage/stage-test.tsx +++ b/ui/stage/stage-test.tsx @@ -1,22 +1,17 @@ import { useAtomValue, type Atom } from "jotai"; import { useEffect, useState } from "react"; import { FsrPanel } from "./fsr-panel"; -import { - type PanelName, - type SMXStage, - SensorTestMode, - type SMXPanelTestData, - type SMXSensorTestData, - type EachPanel, -} from "../../sdk/"; +import { type SMXStage, SensorTestMode, type SMXPanelTestData, type SMXSensorTestData } from "../../sdk/"; import { displayTestData$ } from "../state"; function useInputState(stage: SMXStage | undefined) { - const [panelStates, setPanelStates] = useState | undefined>(); + const [panelStates, setPanelStates] = useState | undefined>(); + const inputs = stage?.inputs || undefined; + useEffect(() => { if (!stage) return; - return stage.events.inputState$.onValue(setPanelStates); - }, [stage]); + return setPanelStates(inputs); //TODO: Figure out why this feels laggy? + }, [stage, inputs]); return panelStates; } @@ -31,15 +26,11 @@ function useTestData(stage: SMXStage | undefined) { const d = stage; async function update() { - await d.updateTestData(SensorTestMode.CalibratedValues); + d.updateTestData(SensorTestMode.CalibratedValues); setTestData(d.test); - if (readTestData) { - handle = requestAnimationFrame(update); - } } - let handle = setInterval(update, 50); - + const handle = setInterval(update, 50); return () => clearInterval(handle); }, [stage, readTestData]); @@ -59,12 +50,12 @@ export function StageTest({ 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]) => ( - + ))}
); From 9f26834eefebad491d69aea9d8ec6f621fcb8fa8 Mon Sep 17 00:00:00 2001 From: Fernando Chorney Date: Sat, 6 Apr 2024 13:25:13 -0500 Subject: [PATCH 10/12] Output Events: Gross but works, holdWhile + bool --- sdk/smx.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/sdk/smx.ts b/sdk/smx.ts index 6451d06..605d399 100644 --- a/sdk/smx.ts +++ b/sdk/smx.ts @@ -15,6 +15,8 @@ class SMXEvents { otherReports$: Bacon.EventStream; output$; + okSendStatus = true; + constructor(dev: HIDDevice) { this.dev = dev; @@ -44,14 +46,18 @@ class SMXEvents { // this.otherReports$.onValue((value) => console.log("Packet: ", value)); finishedCommand$.log("Cmd Finished"); + finishedCommand$.onValue((e) => { + this.okSendStatus = e; + }); // we write a `true` to this whenever a series of packets is going out to the device this.startedSend$ = new Bacon.Bus(); // true means "it's ok to send", false means "don't send" - const okSend$ = finishedCommand$ // Returns true when host_cmd_finished - .merge(this.startedSend$.not()) // Return false when starting to send - .toProperty(true); + const okSendBus$ = finishedCommand$ // Returns true when host_cmd_finished + .merge(this.startedSend$.not()); // Return false when starting to send + + const okSend$ = okSendBus$.toProperty(true); // Main USB Output this.output$ = new Bacon.Bus>(); @@ -65,16 +71,24 @@ class SMXEvents { // combine together the throttled and unthrottled writes // TODO find an alternative to using `bufferedThrottle` here // (seemingly takeWhile lets too much through via race conditions against the okSend property changing) - const eventsToSend$ = configOutput$.merge(otherOutput$).bufferingThrottle(100).takeWhile(okSend$); + //const eventsToSend$ = configOutput$.merge(otherOutput$).bufferingThrottle(100).takeWhile(okSend$); + + const eventsToSend$ = configOutput$.merge(otherOutput$).holdWhen(okSend$.not()); eventsToSend$.onValue(async (value) => { - this.startedSend$.push(true); - await this.writeToHID(value); + if (!this.okSendStatus) { + // Push back onto the stream + this.output$.push(value); + } else { + this.startedSend$.push(true); + this.okSendStatus = false; // Set this here so it's *Fast Enough* to affect the next event + console.log("writing to HID"); + await this.writeToHID(value); + } }); } private async writeToHID(value: Array) { - console.log("writing to HID"); await send_data(this.dev, value); } } From 6de6bdcd92cd14651fbd0af0c50e75c688436d6e Mon Sep 17 00:00:00 2001 From: Noah Manneschmidt Date: Sat, 6 Apr 2024 16:43:48 -0700 Subject: [PATCH 11/12] switch to zip and clean up --- sdk/smx.ts | 71 ++++++++++++++++++++---------------------------------- 1 file changed, 26 insertions(+), 45 deletions(-) diff --git a/sdk/smx.ts b/sdk/smx.ts index 605d399..04d8a4f 100644 --- a/sdk/smx.ts +++ b/sdk/smx.ts @@ -7,29 +7,33 @@ import { StageInputs } from "./commands/inputs"; 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 { - private dev; - private startedSend$: Bacon.Bus; - input$; - inputState$; - otherReports$: Bacon.EventStream; - output$; - - okSendStatus = true; + /** 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) { - this.dev = dev; - // 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) - const report$ = this.input$ + const report$ = rawReport$ .filter((e) => e.reportId === HID_REPORT_INPUT) .map((e) => e.data) .filter((d) => d.byteLength !== 0) @@ -46,18 +50,8 @@ class SMXEvents { // this.otherReports$.onValue((value) => console.log("Packet: ", value)); finishedCommand$.log("Cmd Finished"); - finishedCommand$.onValue((e) => { - this.okSendStatus = e; - }); - - // we write a `true` to this whenever a series of packets is going out to the device - this.startedSend$ = new Bacon.Bus(); - - // true means "it's ok to send", false means "don't send" - const okSendBus$ = finishedCommand$ // Returns true when host_cmd_finished - .merge(this.startedSend$.not()); // Return false when starting to send - const okSend$ = okSendBus$.toProperty(true); + const okSend$ = finishedCommand$.startWith(true); // Main USB Output this.output$ = new Bacon.Bus>(); @@ -69,27 +63,8 @@ class SMXEvents { const otherOutput$ = this.output$.filter((e) => e[0] !== API_COMMAND.WRITE_CONFIG_V5); // combine together the throttled and unthrottled writes - // TODO find an alternative to using `bufferedThrottle` here - // (seemingly takeWhile lets too much through via race conditions against the okSend property changing) - //const eventsToSend$ = configOutput$.merge(otherOutput$).bufferingThrottle(100).takeWhile(okSend$); - - const eventsToSend$ = configOutput$.merge(otherOutput$).holdWhen(okSend$.not()); - - eventsToSend$.onValue(async (value) => { - if (!this.okSendStatus) { - // Push back onto the stream - this.output$.push(value); - } else { - this.startedSend$.push(true); - this.okSendStatus = false; // Set this here so it's *Fast Enough* to affect the next event - console.log("writing to HID"); - await this.writeToHID(value); - } - }); - } - - private async writeToHID(value: Array) { - await send_data(this.dev, value); + // and only emit one per each "ok" signal we get back following each previous output + this.eventsToSend$ = configOutput$.merge(otherOutput$).zip(okSend$, (nextToSend) => nextToSend); } } @@ -109,6 +84,12 @@ export class SMXStage { this.dev = dev; this.events = new SMXEvents(this.dev); + // write outgoing events to the device + this.events.eventsToSend$.onValue(async (value) => { + console.log("writing to HID"); + await send_data(this.dev, value); + }); + // Set the device info handler this.events.otherReports$ .filter((e) => e[0] === char2byte("I")) // We send 'i' but for some reason we get back 'I' From 1f014cde870532851d3b2e3debbd5f1c0c76289a Mon Sep 17 00:00:00 2001 From: Fernando Chorney Date: Sat, 6 Apr 2024 20:30:05 -0500 Subject: [PATCH 12/12] Various Fixes - Pass in `isFsr` to test data parsing - Throttle stage panel inputs in UI for better reactivity - General cleanup --- sdk/commands/sensor_test.ts | 21 +++----- sdk/packet.ts | 74 +++++---------------------- sdk/smx.ts | 32 +++++------- sdk/state-machines/collate-packets.ts | 7 +-- ui/stage/stage-test.tsx | 22 +++++--- 5 files changed, 56 insertions(+), 100 deletions(-) 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/packet.ts b/sdk/packet.ts index b1b9f67..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 makeSpecialDevicePacket(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 04d8a4f..44568db 100644 --- a/sdk/smx.ts +++ b/sdk/smx.ts @@ -47,9 +47,7 @@ class SMXEvents { .filter((e) => e.type === "host_cmd_finished") .map((e) => e.type === "host_cmd_finished"); - // this.otherReports$.onValue((value) => console.log("Packet: ", value)); - - finishedCommand$.log("Cmd Finished"); + // finishedCommand$.log("Cmd Finished"); const okSend$ = finishedCommand$.startWith(true); @@ -76,7 +74,7 @@ export class SMXStage { test: SMXSensorTestData | null = null; inputs: Array | null = null; private test_mode: SensorTestMode = SensorTestMode.CalibratedValues; // TODO: Maybe we just let this be public - private debug = true; + private debug = false; private configResponse$: Bacon.EventStream; @@ -86,8 +84,8 @@ export class SMXStage { // write outgoing events to the device this.events.eventsToSend$.onValue(async (value) => { - console.log("writing to HID"); - await send_data(this.dev, value); + this.debug && console.log("writing to HID"); + await send_data(this.dev, value, this.debug); }); // Set the device info handler @@ -112,13 +110,7 @@ export class SMXStage { } async init(): Promise { - /** - * 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); // Modify `send_data` to accept this somehow? - + // Request the device information this.updateDeviceInfo(); // Request some initial test data @@ -151,22 +143,26 @@ export class SMXStage { const encoded_config = this.config.encode(); if (encoded_config) { const buf = new Uint8Array(encoded_config.buffer); - console.log("Config Encodes Correctly: ", data.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: Uint8Array) { - this.test = new SMXSensorTestData(Array.from(data), this.test_mode); + 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: Uint8Array) { this.info = new SMXDeviceInfo(Array.from(data)); - console.log("Got Info: ", this.info); + this.debug && console.log("Got Info: ", this.info); } private handleInputs(data: Decoded) { diff --git a/sdk/state-machines/collate-packets.ts b/sdk/state-machines/collate-packets.ts index ab96100..a7660ca 100644 --- a/sdk/state-machines/collate-packets.ts +++ b/sdk/state-machines/collate-packets.ts @@ -5,6 +5,7 @@ import { PACKET_FLAG_END_OF_COMMAND, PACKET_FLAG_HOST_CMD_FINISHED, PACKET_FLAG_START_OF_COMMAND, + PACKET_PREAMBLE_SIZE, } from "../packet"; interface PacketHandlingState { @@ -18,6 +19,7 @@ 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, []]; @@ -29,7 +31,7 @@ export const collatePackets: StateF = (st // console.log("Raw Packet Data: ", data); // Return if packet is empty - if (data.length <= 3) { + if (data.length <= PACKET_PREAMBLE_SIZE) { console.log("Empty Packet"); return [state, []]; } @@ -44,8 +46,7 @@ export const collatePackets: StateF = (st console.log("Found Packet Flag Device Info"); } - // TODO: Make some consts for these 2's everywhere - if (2 + byte_len > data.length) { + if (PACKET_PREAMBLE_SIZE + byte_len > data.length) { // TODO: Can this even happen??? console.log("Communication Error: Oversized Packet (ignored)"); return [state, []]; diff --git a/ui/stage/stage-test.tsx b/ui/stage/stage-test.tsx index 07184bd..25c08fa 100644 --- a/ui/stage/stage-test.tsx +++ b/ui/stage/stage-test.tsx @@ -4,14 +4,24 @@ import { FsrPanel } from "./fsr-panel"; import { type SMXStage, SensorTestMode, type SMXPanelTestData, type SMXSensorTestData } from "../../sdk/"; import { displayTestData$ } from "../state"; +// UI Update Rate in Milliseconds +const UI_UPDATE_RATE = 50; + function useInputState(stage: SMXStage | undefined) { - const [panelStates, setPanelStates] = useState | undefined>(); - const inputs = stage?.inputs || undefined; + const [panelStates, setPanelStates] = useState | null>(); useEffect(() => { if (!stage) return; - return setPanelStates(inputs); //TODO: Figure out why this feels laggy? - }, [stage, inputs]); + + const d = stage; + async function update() { + setPanelStates(d.inputs); + } + + const handle = setInterval(update, UI_UPDATE_RATE); + return () => clearInterval(handle); + }, [stage]); + return panelStates; } @@ -30,7 +40,7 @@ function useTestData(stage: SMXStage | undefined) { setTestData(d.test); } - const handle = setInterval(update, 50); + const handle = setInterval(update, UI_UPDATE_RATE); return () => clearInterval(handle); }, [stage, readTestData]); @@ -46,7 +56,7 @@ export function StageTest({ const testData = useTestData(stage); const inputState = useInputState(stage); - if (!testData) { + if (!testData || !inputState) { return null; }