Skip to content

Commit

Permalink
Use Bacon for packet reads
Browse files Browse the repository at this point in the history
- Made SMXStage class to hook into all the bacon stuff
- Modified everything else to mostly work with it
- Probably did some dumb stuff
  • Loading branch information
fchorney committed Apr 5, 2024
1 parent d763eba commit 46e8996
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 268 deletions.
12 changes: 6 additions & 6 deletions sdk/commands/sensor_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export class SMXPanelTestData {
export class SMXSensorTestData {
panels: Array<SMXPanelTestData> = [];

constructor(data: Array<number>) {
constructor(data: Array<number>, mode: SensorTestMode) {
/**
* The first 3 bytes are the preamble.
*
Expand All @@ -199,10 +199,10 @@ export class SMXSensorTestData {
// Expected to be 'y'
console.assert(data[0] === API_COMMAND.GET_SENSOR_TEST_DATA, `Unknown PanelTestData Response: ${data[0]}`);

// TODO: We need to somehow know what mode we requested, so we can potentially check
// here that we got the right response.
const mode = data[1];
console.assert(SensorTestMode[mode] !== undefined, `Unknown SensorTestMode: ${mode}`);
// Make sure we have the correct mode
const data_mode = data[1];
console.assert(SensorTestMode[data_mode] !== undefined, `Unknown SensorTestMode: ${mode}`);
console.assert(mode === data_mode, `Test Mode is Unexpected: ${mode} !== ${data_mode}`);

const size = data[2];
console.assert(size === 80, `Unknown PanelTestData Size: ${size}`);
Expand Down Expand Up @@ -236,7 +236,7 @@ export class SMXSensorTestData {
}

// Generate an SMXPanelTestData object for each panel
this.panels.push(new SMXPanelTestData(detail_data_t.decode(out_bytes, true), mode));
this.panels.push(new SMXPanelTestData(detail_data_t.decode(out_bytes, true), data_mode));
}
}
}
89 changes: 22 additions & 67 deletions sdk/index.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,14 @@
import * as Bacon from "baconjs";
import type { StateF } from "baconjs/types/withstatemachine";
import { API_COMMAND } from "./api";
import { StageInputs } from "./commands/inputs";
import { SMXSensorTestData, SensorTestMode } from "./commands/sensor_test";
import {
HID_REPORT_INPUT,
HID_REPORT_INPUT_STATE,
PACKET_FLAG_DEVICE_INFO,
PACKET_FLAG_END_OF_COMMAND,
PACKET_FLAG_HOST_CMD_FINISHED,
PACKET_FLAG_START_OF_COMMAND,
process_packets,
send_data,
} from "./packet";
import { SMXConfig } from "./commands/config";
import { SMXDeviceInfo } from "./commands/data_info";

export async function getDeviceInfo(dev: HIDDevice) {
await send_data(dev, [API_COMMAND.GET_DEVICE_INFO], true);
const packet = await process_packets(dev, API_COMMAND.GET_DEVICE_INFO, true);
return new SMXDeviceInfo(packet);
}

export async function getStageConfig(dev: HIDDevice) {
await send_data(dev, [API_COMMAND.GET_CONFIG_V5], true);
const response = await process_packets(dev, API_COMMAND.GET_CONFIG_V5, true);

const smxconfig = new SMXConfig(response);

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

console.log(smxconfig);
return smxconfig;
}

export async function getSensorTestData(dev: HIDDevice) {
await send_data(dev, [API_COMMAND.GET_SENSOR_TEST_DATA, SensorTestMode.CalibratedValues], true);
const response = await process_packets(dev, API_COMMAND.GET_SENSOR_TEST_DATA, true);
if (response.length === 0) {
return null;
}
return new SMXSensorTestData(response);
}

// TODO: Probably move all this bacon packet stuff to `packet.js`?
interface PacketHandlingState {
currentPacket: Uint8Array;
}
Expand All @@ -56,24 +17,36 @@ function hasValue<T>(ev: Bacon.Event<T>): ev is Bacon.Value<T> {
return ev.hasValue;
}

type Packet = { type: "host_cmd_finished" } | { type: "data"; payload: Uint8Array };
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
*/
const handlePacket: StateF<DataView, PacketHandlingState, Packet> = (state, event) => {
if (!hasValue(event)) return [state, []];
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);
if (data.length <= 3) return [state, []];

// 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) === PACKET_FLAG_DEVICE_INFO) {
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
Expand Down Expand Up @@ -109,9 +82,9 @@ const handlePacket: StateF<DataView, PacketHandlingState, Packet> = (state, even

// 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 devide has finished executing, and it's safe to start writing another.
console.log("Packet Complete");
eventsToPass.push({ type: "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) {
Expand All @@ -120,22 +93,4 @@ const handlePacket: StateF<DataView, PacketHandlingState, Packet> = (state, even
}

return [newState, eventsToPass.map((e) => new Bacon.Next(e))];
};

export function SmxStage(device: HIDDevice) {
const input$ = Bacon.fromEvent<HIDInputReportEvent>(device, "inputreport");
const inputState$ = input$
.filter((e) => e.reportId === HID_REPORT_INPUT_STATE)
.map((e) => StageInputs.decode(e.data, true));
const otherReports$ = input$
.filter((e) => e.reportId === HID_REPORT_INPUT)
.map((e) => e.data)
.filter((d) => d.byteLength !== 0)
.withStateMachine({ currentPacket: new Uint8Array() }, handlePacket);

return {
device,
inputState$,
otherReports$,
};
}
};
130 changes: 14 additions & 116 deletions sdk/packet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ 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 function make_packets(data: Array<number>): Array<Uint8Array> {
Expand Down Expand Up @@ -100,129 +101,26 @@ export function make_packets(data: Array<number>): Array<Uint8Array> {
return packets;
}

export async function send_data(dev: HIDDevice, data: Array<number>, debug = false) {
// Split data into packets
const packets = make_packets(data);
export async function requestSpecialDeviceInfo(dev: HIDDevice, debug = false) {
const packet = pad_packet([PACKET_FLAG_DEVICE_INFO]);

// Send each packet
for (const packet of packets) {
await dev.sendReport(HID_REPORT_OUTPUT, packet);
if (debug) {
console.log("Sending Packets: ", packet);
}
}

export async function process_packets(dev: HIDDevice, responseType: number, debug = false): Promise<Array<number>> {
let current_packet: Array<number> = [];
let seenFirstPacket = false;
let readingPacket = false;

while (true) {
const data = await nextReportCommand(dev);

// Just continue if there's no data (I feel like this doesn't work here without a timeout)
if (data.byteLength === 0) {
continue;
}

// Create the data buffer here so we can check for start flags and ACKs
const data_buffer = new Uint8Array(data.buffer);

// If we see an ACK Command just ignore it and restart
if (data_buffer[0] === ACK_COMMAND) {
readingPacket = false;
seenFirstPacket = false;
current_packet = [];
continue;
}

// Wait until we see a packet start flag so we don't grab a
// packet half way through
if (!readingPacket) {
if (data_buffer[0] & PACKET_FLAG_START_OF_COMMAND) {
readingPacket = true;
} else {
continue;
}
}

if (handle_packet(data_buffer, current_packet)) {
break;
}

if (!seenFirstPacket) {
if (current_packet[0] === responseType) {
seenFirstPacket = true;
} else {
// we just picked up some other data that has nothing to do with
// what we're waiting on, so just discard it and keep waiting
current_packet = [];
}
}

// If we've gotten here and our current packet is empty, that means
// we flushed it at some point. Restart
if (current_packet.length === 0) {
readingPacket = false;
}
}

// TODO: Handle Acknowledgements

return current_packet;
await dev.sendReport(HID_REPORT_OUTPUT, packet);
}

function handle_packet(dataIn: Uint8Array, currentPacketIn: Array<number>): boolean {
let data = dataIn;
let currentPacket = currentPacketIn;

// Return if packet is empty, or it's an HID Serial packet (< 3 elements)
if (data.length <= 3) {
return false;
}

const cmd = data[0];
const byte_len = data[1];

if ((cmd & PACKET_FLAG_DEVICE_INFO) === 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
}

// 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 false;
}

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

if ((cmd & PACKET_FLAG_START_OF_COMMAND) === PACKET_FLAG_START_OF_COMMAND && 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 = [];
}

currentPacket.push(...data);
export async function send_data(dev: HIDDevice, data: Array<number>, debug = false) {
// Split data into packets
const packets = make_packets(data);

// 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 devide has finished executing, and it's safe to start writing another.
// console.log("Packet Complete");
if (debug) {
console.log("Sending Packets: ", packets);
}

if ((cmd & PACKET_FLAG_END_OF_COMMAND) === PACKET_FLAG_END_OF_COMMAND) {
return true;
// Send each packet
for (const packet of packets) {
await dev.sendReport(HID_REPORT_OUTPUT, packet);
}

return false;
}
Loading

0 comments on commit 46e8996

Please sign in to comment.