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

Sensor Test Cleanup #8

Merged
merged 7 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ dist
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# python/pyenv
.python-version
22 changes: 22 additions & 0 deletions sdk/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,25 @@ export const API_COMMAND = {
export const SMX_USB_VENDOR_ID = 0x2341;
export const SMX_USB_PRODUCT_ID = 0x8037;
export const SMX_USB_PRODUCT_NAME = "StepManiaX";

export const PANEL_COUNT = 9;
export const SENSOR_COUNT = 4;

export enum Panel {
UpLeft = 0,
Up = 1,
UpRight = 2,
Left = 3,
Center = 4,
Right = 5,
DownLeft = 6,
Down = 7,
DownRight = 8,
}

export enum Sensor {
Left = 0,
Right = 1,
Up = 2,
Down = 3,
}
215 changes: 116 additions & 99 deletions sdk/commands/sensor_test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
import { StructBuffer, bitFields, bits, bool, int16_t, uint16_t, uint8_t } from "@nmann/struct-buffer";
import { API_COMMAND, char2byte } from "../api";
import { StructBuffer, bitFields, bits, int16_t, uint16_t, uint8_t } from "@nmann/struct-buffer";
import { API_COMMAND, PANEL_COUNT, SENSOR_COUNT } from "../api";
import type { Decoded } from "./config";
import type { EachPanel, EachSensor } from "./inputs";

/**
* Sensor Test Mode values the stages expect
*/
export const SENSOR_TEST_MODE = {
export enum SensorTestMode {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Converted this to an Enum. Not sure it matters too much, I think they just end up just being consts anyway right? I like the idea of assigning an argument as an Enum type instead of just saying number

Copy link
Owner

Choose a reason for hiding this comment

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

yeah... they kind of become consts, but not really. they're honestly a weird relic of the early days of typescript and there are some foot-gun type properties about them. this is fine for our purposes though.

/** Actual 0 value */
OFF: 0,

/** Return the raw uncalibrated value of each sensor */
UNCALIBRATED_VALUES: char2byte("0"),

/** Return the calibrated value of each sensor */
CALIBRATED_VALUES: char2byte("1"),

/** Return the sensor noise value */
NOISE: char2byte("2"),

/** Return the sensor tare value */
TARE: char2byte("3"),
};
Off = 0,

/**
* Return the raw uncalibrated value of each sensor.
* 48 represents the char "0"
**/
UncalibratedValues = 48,

/**
* Return the calibrated value of each sensor
* 49 represents the char "1"
**/
CalibratedValues = 49,

/**
* Return the sensor noise value
* 50 represents the char "2"
**/
Noise = 50,

/**
* Return the sensor tare value
* 51 represents the char "3"
**/
Tare = 51,
}

/**
* The first byte of the test mode detail data contains
Expand Down Expand Up @@ -77,27 +88,12 @@ const detail_data_t = new StructBuffer("detail_data_t", {
*/
export class SMXPanelTestData {
have_data_from_panel: boolean;
sensor_level: EachSensor<number> = {
up: 0,
right: 0,
down: 0,
left: 0,
};
bad_sensor_input: EachSensor<boolean> = {
up: false,
right: false,
down: false,
left: false,
};
sensor_level: Array<number> = Array(SENSOR_COUNT).fill(-1);
bad_sensor_input: Array<boolean> = Array(SENSOR_COUNT).fill(false);
dip_switch_value = -1;
bad_jumper: EachSensor<boolean> = {
up: false,
right: false,
down: false,
left: false,
};

constructor(data: Decoded<typeof detail_data_t>) {
bad_jumper: Array<boolean> = Array(SENSOR_COUNT).fill(false);

constructor(data: Decoded<typeof detail_data_t>, mode: SensorTestMode) {
/**
* 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 All @@ -107,74 +103,118 @@ export class SMXPanelTestData {
return;
}

// Assuming the sig bits are correct, we can confirm here that we have proper data
this.have_data_from_panel = true;

/**
* These bits are true if that sensor's most recent reading is invalid.
* A sensors reading could be considered invalid if the sensor has been turned
* off in the config tool.
*/
this.bad_sensor_input = {
up: data.sig_bad.bad_sensor_0,
right: data.sig_bad.bad_sensor_1,
down: data.sig_bad.bad_sensor_2,
left: data.sig_bad.bad_sensor_3,
};
this.bad_sensor_input = [
data.sig_bad.bad_sensor_0,
data.sig_bad.bad_sensor_1,
data.sig_bad.bad_sensor_2,
data.sig_bad.bad_sensor_3,
];

// This is what the dipswitch is set to for this panel
this.dip_switch_value = data.dips.dip;

// These are true if the sensor has the incorrect jumper set
this.bad_jumper = {
up: !!data.dips.bad_sensor_dip_0,
right: !!data.dips.bad_sensor_dip_1,
down: !!data.dips.bad_sensor_dip_2,
left: !!data.dips.bad_sensor_dip_3,
};
this.bad_jumper = [
!!data.dips.bad_sensor_dip_0,
!!data.dips.bad_sensor_dip_1,
!!data.dips.bad_sensor_dip_2,
!!data.dips.bad_sensor_dip_3,
];

/**
* These are 16-bit signed integers for the sensor values.
* 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 = {
left: data.sensors[0],
right: data.sensors[1],
up: data.sensors[2],
down: data.sensors[3],
};
this.sensor_level = data.sensors.map((value) => this.clamp_sensor_value(value, mode));
}

private clamp_sensor_value(value: number, mode: SensorTestMode) {
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);
Comment on lines +143 to +151
Copy link
Owner

Choose a reason for hiding this comment

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

We can also resolve this by just displaying the squared values as they come across the wire in a more abbreviated form e.g. 12k instead of 12345. I'm not even sure what the value in the noise test mode is, so we can wait to decide later.

}

// 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;
Comment on lines +154 to +156
Copy link
Owner

Choose a reason for hiding this comment

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

we gotta become good friends with someone on gen1 pads (werd) or convince zewt to actually come over here and implement support for us because... 😓

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The config will tell us if it's an fsr or not. We just need to get that in here, but also yeah maybe werd can help us test things out. There's a chunk of old config or load cell stuff that we just can't test.


// TODO: This may be necessary for defining sensor value vertial bars in the UI
// const max_value = isFSR ? 250 : 500;

let clamped_value = value;
/**
* Very slightly negative values happen due to noise.
* The don't indicate a problem, but they're confusing
* in the UI, so clamp them away.
*/
if (value < 0 && value >= -10) {
clamped_value = 0;
}

// FSR values are bitshifted right by 2 (effectively a div by 4).
if (isFSR) {
clamped_value >>= 2;
}

return clamped_value;
}
}

export class SMXSensorTestData {
panels: EachPanel<SMXPanelTestData>;
panels: Array<SMXPanelTestData> = [];

constructor(data: Array<number>) {
/**
* The first 3 bytes are the preamble.
*
* "y" is a response to our "y" query. This is binary data with the format:
* yAB......
* where A is our original query mode (currently "0", "1", "2", or "3"), and
* B is the number of bits from each panel in the response.
* Each bit is encoded as a 16-bit int, with each int having the response
* bits from each panel.
* where A (mode) is our original query mode (currently "0", "1", "2", or "3"), and
* B (size) is the number of 16-Bit Integers that contain all of our panel data.
*
* The explanation of how these bits are interlaced and decoded can be found
* in the README.
*
* TODO: Put in readme link here
*/
console.assert(data[0] === API_COMMAND.GET_SENSOR_TEST_DATA); // Expected to be 'y'
// const mode = data[1]; // If we know what command we sent we could confirm we get the right response
const preamble = 3;

// 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}`);

const size = data[2];
console.assert(size === 80, `Unknown PanelTestData Size: ${size}`);

/**
* Convert the data from 8-Bit Little Endian bytes to 16-Bit Integers
* Convert the data from 8-Bit Little Endian Bytes to 16-Bit Integers
*/
const sensor_data_t = new StructBuffer("sensor_data_t", {
data: uint16_t[size],
});
const decoded_data = sensor_data_t.decode(data.slice(3), true);
const panel_count = 9; // TODO: This could be a const somewhere?
const panel_data = [];
const sensor_data_t = new StructBuffer("sensor_data_t", { data: uint16_t[size] });
const decoded_data = sensor_data_t.decode(data.slice(preamble), true);

// Cycle through each panel and grab the data
// TODO: Document exactly how we're dealing with the bits here and how things are layed out
for (let panel = 0; panel < panel_count; panel++) {
for (let panel = 0; panel < PANEL_COUNT; panel++) {
let idx = 0;
const out_bytes: Array<number> = [];

Expand All @@ -185,41 +225,18 @@ export class SMXSensorTestData {
for (const _ of Array.from({ length: size / 8 })) {
let result = 0;

// Read each bit in each byte
// Read each bit in each 8-bit byte
for (let bit = 0; bit < 8; bit++, idx++) {
const new_bit = decoded_data.data[idx] & (1 << panel);
result |= new_bit << bit;
}

// We need to shift the result by the panel to move it back to fit within
// an 8-bit byte
// We need to shift the result by the panel to move it back to fit within an 8-bit byte
out_bytes.push(result >> panel);
}

console.log(`Panel ${panel}: ${out_bytes}`);
panel_data.push(detail_data_t.decode(out_bytes, true));
// Generate an SMXPanelTestData object for each panel
this.panels.push(new SMXPanelTestData(detail_data_t.decode(out_bytes, true), mode));
}

const panels = [];
for (let panel = 0; panel < 9; panel++) {
panels.push(new SMXPanelTestData(panel_data[panel]));
}

this.panels = {
up_left: panels[0],
up: panels[1],
up_right: panels[2],
left: panels[3],
center: panels[4],
right: panels[5],
down_left: panels[6],
down: panels[7],
down_right: panels[8],
};

console.log(decoded_data);
console.log(panel_data);

console.log(this);
}
}
24 changes: 19 additions & 5 deletions sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as Bacon from "baconjs";
import type { StateF } from "baconjs/types/withstatemachine";
import { API_COMMAND } from "./api";
import { StageInputs } from "./commands/inputs";
import { SENSOR_TEST_MODE } from "./commands/sensor_test";
import { SMXSensorTestData, SensorTestMode } from "./commands/sensor_test";
import {
HID_REPORT_INPUT,
HID_REPORT_INPUT_STATE,
Expand All @@ -13,20 +13,34 @@ import {
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);
return process_packets(dev, 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);
return process_packets(dev, 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("Same Thing:", response.slice(2, -1).toString() === buf.toString());
}
return smxconfig;
}

export async function getSensorTestData(dev: HIDDevice) {
await send_data(dev, [API_COMMAND.GET_SENSOR_TEST_DATA, SENSOR_TEST_MODE.CALIBRATED_VALUES], true);
return process_packets(dev, true);
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);
return new SMXSensorTestData(response);
}

interface PacketHandlingState {
Expand Down
Loading