Skip to content

Commit

Permalink
Merge pull request #11 from noahm/fc/bacon-output
Browse files Browse the repository at this point in the history
Fairly major bacon.js input/output overhaul
  • Loading branch information
fchorney authored Apr 7, 2024
2 parents 6ee4687 + 1f014cd commit 8aef507
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 247 deletions.
21 changes: 8 additions & 13 deletions sdk/commands/sensor_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export class SMXPanelTestData {
dip_switch_value = -1;
bad_jumper: Array<boolean> = Array(SENSOR_COUNT).fill(false);

constructor(data: Decoded<typeof detail_data_t>, mode: SensorTestMode) {
constructor(data: Decoded<typeof detail_data_t>, 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.
Expand Down Expand Up @@ -134,29 +134,24 @@ 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.
* Display the square root, since the panels don't do this for us.
* 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;
/**
Expand All @@ -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;
}

Expand All @@ -180,7 +175,7 @@ export class SMXPanelTestData {
export class SMXSensorTestData {
panels: Array<SMXPanelTestData> = [];

constructor(data: Array<number>, mode: SensorTestMode) {
constructor(data: Array<number>, mode: SensorTestMode, isFsr: boolean) {
/**
* The first 3 bytes are the preamble.
*
Expand Down Expand Up @@ -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));
}
}
}
107 changes: 14 additions & 93 deletions sdk/index.ts
Original file line number Diff line number Diff line change
@@ -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<T>(ev: Bacon.Event<T>): ev is Bacon.Value<T> {
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<DataView, PacketHandlingState, Packet> = (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";
74 changes: 14 additions & 60 deletions sdk/packet.ts
Original file line number Diff line number Diff line change
@@ -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<DataView> {
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.
Expand All @@ -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;
Expand All @@ -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<number>, 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<number>): Array<Uint8Array> {
const packets = [];
Expand Down Expand Up @@ -100,27 +78,3 @@ export function make_packets(data: Array<number>): Array<Uint8Array> {

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<number>, 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);
}
}
Loading

0 comments on commit 8aef507

Please sign in to comment.