Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fairly major bacon.js input/output overhaul #11

Merged
merged 12 commits into from
Apr 7, 2024
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