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

Use Bacon for packet reads #10

Merged
merged 4 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 };
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this because I was sick of checking of the type was data every time I wanted to grab the payload.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I wonder if we can use the host_cmd_finished packet when we make our output pipes to know when we can send the next packet down the line.

Copy link
Owner

@noahm noahm Apr 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we definitely need to throttle our writes based on the readystate communicated back here. I think I know how to do that...

Also, if you don't want to be stuck always checking for a payload then it sounds like we need another downstream filter on the other event streams that filters out the cmd finished responses.


/**
* 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
Loading