Compare commits
3 commits
4f8177f87f
...
b2d2becc34
| Author | SHA1 | Date | |
|---|---|---|---|
| b2d2becc34 | |||
| 892c7e1d41 | |||
| 1943c02047 |
9 changed files with 476 additions and 47 deletions
69
README.md
69
README.md
|
|
@ -106,7 +106,8 @@ Prereqs: Pico SDK + CMake toolchain set up.
|
|||
### Using `build.py`
|
||||
|
||||
`build.py` configures CMake, builds the firmware, checks that both output formats
|
||||
were created, and flashes the ELF with `picotool`.
|
||||
were created, copies the release artifacts into `firmware/`, and flashes the ELF
|
||||
with `picotool`.
|
||||
|
||||
Before running it:
|
||||
|
||||
|
|
@ -122,6 +123,8 @@ The generated files are:
|
|||
|
||||
- `build/switch-pico.elf`, which `build.py` passes to `picotool`.
|
||||
- `build/switch-pico.uf2`, which can also be copied to the Pico manually.
|
||||
- `firmware/switch-pico.elf` and `firmware/switch-pico.uf2`, refreshed from the
|
||||
corresponding `build/` artifacts after every successful build.
|
||||
|
||||
To customize the controller grip color while building, pass one of these mutually
|
||||
exclusive options:
|
||||
|
|
@ -269,14 +272,14 @@ with SwitchUARTClient("/dev/cu.usbserial-0001") as client:
|
|||
|
||||
## IMU / Motion Controls
|
||||
|
||||
The bridge supports gyroscope and accelerometer passthrough from controllers that have motion sensors (e.g. the Nintendo Switch Pro Controller, DualSense). Motion data is forwarded to the Pico which injects it into the emulated Switch Pro Controller's HID reports.
|
||||
The bridge supports gyroscope and accelerometer passthrough from controllers that have motion sensors (e.g. the Nintendo Switch Pro Controller and DualSense). Motion data is forwarded to the Pico as a rolling three-sample window; the Pico emits standard 0x30 reports at 15 ms intervals and supports both raw IMU mode 1 and packed quaternion mode 2.
|
||||
|
||||
### Requirements
|
||||
- A controller with gyro/accelerometer support (SDL2 must be able to enable sensors on it).
|
||||
- A controller with gyro/accelerometer support that SDL3 can enable.
|
||||
- The Switch will automatically use motion data once the controller is recognised as a Pro Controller.
|
||||
|
||||
### Gyro bias calibration
|
||||
On startup, the bridge collects the first 200 gyro readings while the controller is stationary and averages them to compute a per-axis bias (zero-rate offset). Gyro output is zeroed during this ~1 second calibration window, then bias is subtracted from all subsequent readings. Keep the controller still when starting the bridge for best results. Use `--no-gyro-bias` to skip calibration and use raw values directly.
|
||||
On startup, the bridge collects the first 200 gyro readings while the controller is stationary and averages them to compute a per-axis bias (zero-rate offset). The bias is subtracted from subsequent readings. Keep the controller still during startup for best results.
|
||||
|
||||
### CLI flags
|
||||
- `--debug-imu`: Print raw sensor values (m/s² and rad/s) and converted Switch integer counts every ~200ms. Useful for verifying the sensor is detected and producing sensible data.
|
||||
|
|
@ -284,16 +287,66 @@ On startup, the bridge collects the first 200 gyro readings while the controller
|
|||
- `--gyro-scale FLOAT` (default 1.0): Multiply all gyro values by this factor before sending. Reduce below 1.0 if the camera moves too fast; increase above 1.0 for more sensitivity.
|
||||
|
||||
### Troubleshooting
|
||||
- **Gyro not detected**: Run with `--debug-imu`. If no IMU readings appear, SDL2 cannot see sensors on your controller (may not be supported or driver issue). On Linux, the `hid-nintendo` kernel driver routes Pro Controller IMU to a separate evdev device that SDL2 cannot read; use Windows or macOS for gyro passthrough.
|
||||
- **Wild camera swinging**: Start with `--gyro-scale 0.3` and increase gradually. Ensure the controller is still during the first second of startup (bias calibration).
|
||||
- **Verifying Pico output**: Use `python tools/read_pro_imu.py --vid 0x057E --pid 0x2009` to read raw IMU bytes directly from the Pico's USB HID output and confirm non-zero values appear.
|
||||
- **SDL2 accuracy**: SDL2 (version < 2.32.7) has a known inaccuracy bug with Switch Pro Controller gyro data. Updating the SDL2 shared library to 2.32.7 or later improves accuracy.
|
||||
- **Gyro not detected**: Run with `--debug-imu`. If no IMU readings appear, SDL3 cannot see sensors on the controller. On Linux, the `hid-nintendo` kernel driver may expose Nintendo controller motion differently; DualSense motion is supported by SDL3's PlayStation HID driver.
|
||||
- **Wild camera swinging**: Rebuild and flash the current Pico firmware. Older builds acknowledged quaternion IMU mode 2 but emitted raw mode-1 bytes, which Zelda interpreted as random quaternion data. Keep the controller still during startup, then use `--gyro-scale` only for deliberate sensitivity adjustment.
|
||||
- **Verifying Pico output**: Use `uv run python tools/read_pro_imu.py --vid 0x057E --pid 0x2009` to read raw IMU bytes directly from the Pico's USB HID output. A stationary controller should show gyro values near zero and three non-empty, non-duplicated samples per report.
|
||||
|
||||
### Implementation notes for maintainers
|
||||
|
||||
#### The failure
|
||||
|
||||
Nintendo subcommand `0x40` is a mode selector, not a Boolean enable:
|
||||
|
||||
| Value | Meaning | Required bytes 13-48 in report `0x30` |
|
||||
|---|---|---|
|
||||
| `0` | IMU off | Zero-filled |
|
||||
| `1` | Raw IMU | Three 12-byte accelerometer/gyro samples |
|
||||
| `2` | Quaternion | Nintendo's packed 36-byte mode-2 structure |
|
||||
|
||||
The previous firmware stored the argument in `bool is_imu_enabled`. A mode-2 request therefore enabled the raw mode-1 packer. Zelda then decoded raw sensor bytes as mode bits, compressed quaternion components, deltas, and timestamps, producing apparently random camera rotation. The fake also advertised firmware `4.91`, while the genuine wired Pro Controller used during diagnosis reported `3.48`.
|
||||
|
||||
Keep `SwitchImuMode` as a three-state value. Never acknowledge mode 2 and then emit mode-1 bytes.
|
||||
|
||||
#### Mode-1 implementation
|
||||
|
||||
- Emit one `0x30` report every 15 ms.
|
||||
- Advance the report timer by 3: one timer tick for each nominal 5 ms IMU sample.
|
||||
- Pack three chronological samples as signed little-endian `accel X/Y/Z`, then `gyro X/Y/Z`.
|
||||
- The host bridge must retain and republish its latest three-sample window. Do not drain it at the faster UART rate; that previously produced empty and duplicated USB reports.
|
||||
- With the advertised factory calibration, 1g is approximately 4096 counts and 1 rad/s is approximately 818.5 gyro counts.
|
||||
|
||||
#### Mode-2 implementation
|
||||
|
||||
`switch_pro_driver.cpp` implements this in `integrate_motion_sample()` and `fill_quaternion_imu_report_data()`:
|
||||
|
||||
1. Reset quaternion state to `(0, 0, 0, 1)` when transitioning into mode 2.
|
||||
2. Integrate each report's three gyro samples at 5 ms per sample. The Nintendo quaternion axes use sensor `Y, X, Z`, not `X, Y, Z`.
|
||||
3. Build a delta quaternion from the angular rotation vector, multiply it into the current orientation, and normalize after every sample.
|
||||
4. Select the largest absolute quaternion component. Its index and sign represent the omitted component; encode the other three signed components at 21-bit precision.
|
||||
5. Pack accelerometer data in `Y, X, Z` order, set the mode field to `2`, write the 11-bit millisecond timestamp, and set the timestamp/sample count to `3`.
|
||||
6. Integrate and repack only when transmitting the next 15 ms USB report. Calling the integrator from the unrestricted main loop over-integrates the same UART samples.
|
||||
|
||||
The mode-2 wire format is bit-packed and fields cross byte boundaries. Use `write_bits_le()` rather than C/C++ bitfields so layout does not depend on compiler bitfield rules.
|
||||
|
||||
#### Regression and hardware verification
|
||||
|
||||
After changing any IMU conversion, calibration, timing, or report packing:
|
||||
|
||||
1. Run `uv run --with pytest pytest -q`.
|
||||
2. Build with `cmake --build build -j`.
|
||||
3. Capture at least 200 raw `0x30` reports. Stationary gyro should remain near zero; there should be no empty windows, duplicated three-sample windows, or timer-step errors.
|
||||
4. Send subcommand `0x40` with value `2`. Every resulting report must have mode bits `2` and timestamp count `3`.
|
||||
5. Inject a known single-axis gyro rate and decode the packed quaternion. The corresponding component must change smoothly with the expected sign.
|
||||
6. Perform the decisive end-to-end check: genuine Pro Controller → SDL3 bridge → UART → emulated Pico → Zelda. This path was confirmed correct after the mode-2 fix.
|
||||
|
||||
The quaternion implementation is adapted from HOJA's `NS-LIB-HID` and retains its CC BY-NC 4.0 attribution in `switch_pro_driver.cpp`.
|
||||
|
||||
## References
|
||||
- GP2040-CE (controller firmware ecosystem): https://github.com/OpenStickCommunity/GP2040-CE
|
||||
- nxbt (Switch controller research/tools): https://github.com/Brikwerk/nxbt
|
||||
- Nintendo Switch Reverse Engineering notes: https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
|
||||
- `hid-nintendo` driver reference: https://github.com/DanielOgorchock/linux/blob/ogorchock/drivers/hid/hid-nintendo.c
|
||||
- HOJA `NS-LIB-HID` mode-2 reference: https://github.com/HandHeldLegend/NS-LIB-HID
|
||||
|
||||
## Troubleshooting
|
||||
- **No input on Switch**: verify UART wiring (Pico GPIO4/5), baud matches both sides, Pico flashed with current firmware, and `Pro Controller Wired Communication` is enabled on the Switch.
|
||||
|
|
|
|||
8
build.py
8
build.py
|
|
@ -12,6 +12,9 @@ from pathlib import Path
|
|||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
CONFIG_FILE = SCRIPT_DIR / "controller_color_config.h"
|
||||
BUILD_DIR = SCRIPT_DIR / "build"
|
||||
FIRMWARE_DIR = SCRIPT_DIR / "firmware"
|
||||
FIRMWARE_ELF_PATH = FIRMWARE_DIR / "switch-pico.elf"
|
||||
FIRMWARE_UF2_PATH = FIRMWARE_DIR / "switch-pico.uf2"
|
||||
|
||||
ELF_PATH = Path(os.environ.get("ELF_PATH", BUILD_DIR / "switch-pico.elf")).expanduser()
|
||||
UF2_PATH = Path(os.environ.get("UF2_PATH", BUILD_DIR / "switch-pico.uf2")).expanduser()
|
||||
|
|
@ -126,9 +129,14 @@ def build():
|
|||
missing = ", ".join(str(path) for path in missing_artifacts)
|
||||
sys.stderr.write(f"Error: Build did not produce required artifact(s): {missing}\n")
|
||||
sys.exit(1)
|
||||
FIRMWARE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ELF_PATH, FIRMWARE_ELF_PATH)
|
||||
shutil.copy2(UF2_PATH, FIRMWARE_UF2_PATH)
|
||||
|
||||
print(f"Built ELF: {ELF_PATH}")
|
||||
print(f"Built UF2: {UF2_PATH}")
|
||||
print(f"Copied ELF: {FIRMWARE_ELF_PATH}")
|
||||
print(f"Copied UF2: {FIRMWARE_UF2_PATH}")
|
||||
|
||||
def flash():
|
||||
picotool = resolve_picotool()
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|||
[project]
|
||||
name = "switch-pico-bridge"
|
||||
version = "0.1.0"
|
||||
description = "SDL2-to-UART host bridge and helpers for the switch-pico firmware."
|
||||
description = "SDL3-to-UART host bridge and helpers for the switch-pico firmware."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
authors = [{ name = "Switch Pico Maintainers" }]
|
||||
|
|
@ -13,6 +13,7 @@ dependencies = [
|
|||
"pyserial",
|
||||
"PySDL3",
|
||||
"rich",
|
||||
"hidapi",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ CONTROLLER_DB_URL_DEFAULT = "https://raw.githubusercontent.com/mdqinc/SDL_GameCo
|
|||
SDL_TRUE = True
|
||||
SDL_EVENT_GAMEPAD_SENSOR_UPDATE = getattr(sdl3, "SDL_EVENT_GAMEPAD_SENSOR_UPDATE", 0x658)
|
||||
GYRO_BIAS_SAMPLES = 200
|
||||
IMU_BUFFER_SIZE = 32
|
||||
|
||||
|
||||
def parse_mapping(value: str) -> Tuple[int, str]:
|
||||
|
|
@ -1368,8 +1367,8 @@ def handle_sensor_update(
|
|||
)
|
||||
|
||||
ctx.imu_samples.append(sample)
|
||||
if len(ctx.imu_samples) > IMU_BUFFER_SIZE:
|
||||
ctx.imu_samples = ctx.imu_samples[-IMU_BUFFER_SIZE:]
|
||||
if len(ctx.imu_samples) > IMU_SAMPLES_PER_REPORT:
|
||||
del ctx.imu_samples[:-IMU_SAMPLES_PER_REPORT]
|
||||
|
||||
if config.debug_imu:
|
||||
now = time.monotonic()
|
||||
|
|
@ -1567,12 +1566,9 @@ def service_contexts(
|
|||
try:
|
||||
if now - ctx.last_send >= config.interval:
|
||||
if ctx.sensors_enabled and not config.no_imu:
|
||||
count = min(len(ctx.imu_samples), IMU_SAMPLES_PER_REPORT)
|
||||
if count > 0:
|
||||
ctx.report.imu_samples = ctx.imu_samples[:count]
|
||||
ctx.imu_samples = ctx.imu_samples[count:]
|
||||
else:
|
||||
ctx.report.imu_samples = []
|
||||
# Keep publishing the latest complete sensor window. Draining
|
||||
# this at the faster UART rate leaves most USB reports empty.
|
||||
ctx.report.imu_samples = ctx.imu_samples
|
||||
else:
|
||||
ctx.report.imu_samples = []
|
||||
ctx.uart.send_report(ctx.report)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "switch_pro_driver.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <stdio.h>
|
||||
|
|
@ -16,6 +17,10 @@
|
|||
|
||||
// force a report to be sent every X ms
|
||||
#define SWITCH_PRO_KEEPALIVE_TIMER 5
|
||||
// Real Pro Controller cadence: 3 IMU frames per report, 5ms/frame => 15ms/report
|
||||
// (~66.7Hz). Emitting the 3-frame 0x30 faster makes the console over-integrate
|
||||
// gyro (3 frames assumed 5ms apart delivered too often) => wild camera swing.
|
||||
#define SWITCH_PRO_IMU_REPORT_TIMER 15
|
||||
|
||||
static SwitchInputState g_input_state{
|
||||
false, false, false, false,
|
||||
|
|
@ -41,7 +46,13 @@ static uint8_t handshake_counter = 0;
|
|||
static SwitchDeviceInfo device_info{};
|
||||
static uint8_t player_id = 0;
|
||||
static uint8_t input_mode = 0x30;
|
||||
static bool is_imu_enabled = false;
|
||||
enum class SwitchImuMode : uint8_t {
|
||||
Off = 0,
|
||||
Raw = 1,
|
||||
Quaternion = 2,
|
||||
};
|
||||
|
||||
static SwitchImuMode imu_mode = SwitchImuMode::Off;
|
||||
static bool is_vibration_enabled = false;
|
||||
|
||||
// Optional compile-time colour override (body/buttons/grips).
|
||||
|
|
@ -98,9 +109,11 @@ static const uint8_t factory_config_data[0xEFF] = {
|
|||
0xFF, 0xFF, 0xFF, 0xFF,
|
||||
|
||||
// config & calibration 1
|
||||
0xE3, 0xFF, 0x39, 0xFF, 0xED, 0x01, 0x00, 0x40,
|
||||
0x00, 0x40, 0x00, 0x40, 0x09, 0x00, 0xEA, 0xFF,
|
||||
0xA1, 0xFF, 0x3B, 0x34, 0x3B, 0x34, 0x3B, 0x34,
|
||||
// IMU calibration @ 0x6020: zero offsets, 4096 LSB/g, and
|
||||
// 818.5 LSB/(rad/s), matching the bridge's raw-value conversion.
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40,
|
||||
0x00, 0x40, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x3B, 0x34, 0x3B, 0x34, 0x3B, 0x34,
|
||||
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
|
||||
|
|
@ -184,32 +197,167 @@ static std::map<uint32_t, const uint8_t*> spi_flash_data = {
|
|||
|
||||
static inline uint16_t scale16To12(uint16_t pos) { return pos >> 4; }
|
||||
|
||||
static void fill_imu_report_data(const SwitchInputState& state) {
|
||||
// Quaternion IMU mode 2 is adapted from HandHeldLegend/NS-LIB-HID
|
||||
// (ns_lib_motion.*, 2026, Mitchell Cairns / Hand Held Legend, LLC),
|
||||
// licensed CC BY-NC 4.0: https://creativecommons.org/licenses/by-nc/4.0/
|
||||
struct MotionQuaternion {
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
float w;
|
||||
int16_t accel_x;
|
||||
int16_t accel_y;
|
||||
int16_t accel_z;
|
||||
};
|
||||
|
||||
static MotionQuaternion motion_quaternion{0.0f, 0.0f, 0.0f, 1.0f, 0, 0, 0};
|
||||
|
||||
static void reset_motion_quaternion() {
|
||||
motion_quaternion = {0.0f, 0.0f, 0.0f, 1.0f, 0, 0, 0};
|
||||
}
|
||||
|
||||
static void write_int16_le(uint8_t* dst, int16_t value) {
|
||||
dst[0] = static_cast<uint8_t>(value & 0xFF);
|
||||
dst[1] = static_cast<uint8_t>((value >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
static void write_bits_le(uint8_t* dst, uint16_t bit_offset, uint32_t value, uint8_t width) {
|
||||
for (uint8_t bit = 0; bit < width; ++bit) {
|
||||
if ((value & (1u << bit)) != 0) {
|
||||
uint16_t output_bit = static_cast<uint16_t>(bit_offset + bit);
|
||||
dst[output_bit >> 3] |= static_cast<uint8_t>(1u << (output_bit & 7u));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void integrate_motion_sample(const SwitchImuSample& sample) {
|
||||
constexpr float sample_dt = 0.005f;
|
||||
constexpr float gyro_rad_per_lsb = 1.0f / 818.5f;
|
||||
|
||||
// Nintendo mode 2 uses Y, X, Z sensor order for quaternion axes.
|
||||
float angle_x = static_cast<float>(sample.gyro_y) * gyro_rad_per_lsb * sample_dt;
|
||||
float angle_y = static_cast<float>(sample.gyro_x) * gyro_rad_per_lsb * sample_dt;
|
||||
float angle_z = static_cast<float>(sample.gyro_z) * gyro_rad_per_lsb * sample_dt;
|
||||
float norm = sqrtf(angle_x * angle_x + angle_y * angle_y + angle_z * angle_z);
|
||||
float half = 0.5f * norm;
|
||||
float vector_scale = norm > 1e-12f ? sinf(half) / norm : 0.5f;
|
||||
float scalar = norm > 1e-12f ? cosf(half) : 1.0f;
|
||||
|
||||
float dx = angle_x * vector_scale;
|
||||
float dy = angle_y * vector_scale;
|
||||
float dz = angle_z * vector_scale;
|
||||
float dw = scalar;
|
||||
|
||||
float x = motion_quaternion.w * dx + motion_quaternion.x * dw
|
||||
+ motion_quaternion.y * dz - motion_quaternion.z * dy;
|
||||
float y = motion_quaternion.w * dy - motion_quaternion.x * dz
|
||||
+ motion_quaternion.y * dw + motion_quaternion.z * dx;
|
||||
float z = motion_quaternion.w * dz + motion_quaternion.x * dy
|
||||
- motion_quaternion.y * dx + motion_quaternion.z * dw;
|
||||
float w = motion_quaternion.w * dw - motion_quaternion.x * dx
|
||||
- motion_quaternion.y * dy - motion_quaternion.z * dz;
|
||||
float magnitude = sqrtf(x * x + y * y + z * z + w * w);
|
||||
if (magnitude > 1e-12f) {
|
||||
float inverse = 1.0f / magnitude;
|
||||
motion_quaternion.x = x * inverse;
|
||||
motion_quaternion.y = y * inverse;
|
||||
motion_quaternion.z = z * inverse;
|
||||
motion_quaternion.w = w * inverse;
|
||||
} else {
|
||||
reset_motion_quaternion();
|
||||
}
|
||||
|
||||
motion_quaternion.accel_x = sample.accel_x;
|
||||
motion_quaternion.accel_y = sample.accel_y;
|
||||
motion_quaternion.accel_z = sample.accel_z;
|
||||
}
|
||||
|
||||
static void fill_raw_imu_report_data(const SwitchInputState& state) {
|
||||
if (state.imu_sample_count == 0) {
|
||||
memset(switch_report.imuData, 0x00, sizeof(switch_report.imuData));
|
||||
return;
|
||||
}
|
||||
uint8_t sample_count = state.imu_sample_count > 3 ? 3 : state.imu_sample_count;
|
||||
// If fewer than 3 samples, duplicate the last one to fill all 3 slots
|
||||
uint8_t* dst = switch_report.imuData;
|
||||
for (uint8_t i = 0; i < 3; ++i) {
|
||||
const SwitchImuSample& s = (i < sample_count) ? state.imu_samples[i] : state.imu_samples[sample_count - 1];
|
||||
dst[0] = static_cast<uint8_t>(s.accel_x & 0xFF);
|
||||
dst[1] = static_cast<uint8_t>((s.accel_x >> 8) & 0xFF);
|
||||
dst[2] = static_cast<uint8_t>(s.accel_y & 0xFF);
|
||||
dst[3] = static_cast<uint8_t>((s.accel_y >> 8) & 0xFF);
|
||||
dst[4] = static_cast<uint8_t>(s.accel_z & 0xFF);
|
||||
dst[5] = static_cast<uint8_t>((s.accel_z >> 8) & 0xFF);
|
||||
dst[6] = static_cast<uint8_t>(s.gyro_x & 0xFF);
|
||||
dst[7] = static_cast<uint8_t>((s.gyro_x >> 8) & 0xFF);
|
||||
dst[8] = static_cast<uint8_t>(s.gyro_y & 0xFF);
|
||||
dst[9] = static_cast<uint8_t>((s.gyro_y >> 8) & 0xFF);
|
||||
dst[10] = static_cast<uint8_t>(s.gyro_z & 0xFF);
|
||||
dst[11] = static_cast<uint8_t>((s.gyro_z >> 8) & 0xFF);
|
||||
const SwitchImuSample& sample =
|
||||
(i < sample_count) ? state.imu_samples[i] : state.imu_samples[sample_count - 1];
|
||||
write_int16_le(dst + 0, sample.accel_x);
|
||||
write_int16_le(dst + 2, sample.accel_y);
|
||||
write_int16_le(dst + 4, sample.accel_z);
|
||||
write_int16_le(dst + 6, sample.gyro_x);
|
||||
write_int16_le(dst + 8, sample.gyro_y);
|
||||
write_int16_le(dst + 10, sample.gyro_z);
|
||||
dst += 12;
|
||||
}
|
||||
}
|
||||
|
||||
static void fill_quaternion_imu_report_data(const SwitchInputState& state, uint32_t now_ms) {
|
||||
if (state.imu_sample_count > 0) {
|
||||
uint8_t sample_count = state.imu_sample_count > 3 ? 3 : state.imu_sample_count;
|
||||
for (uint8_t i = 0; i < 3; ++i) {
|
||||
const SwitchImuSample& sample =
|
||||
(i < sample_count) ? state.imu_samples[i] : state.imu_samples[sample_count - 1];
|
||||
integrate_motion_sample(sample);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t* dst = switch_report.imuData;
|
||||
memset(dst, 0x00, sizeof(switch_report.imuData));
|
||||
|
||||
// Mode 2 accelerometer vectors are encoded in Y, X, Z order.
|
||||
write_int16_le(dst + 0, motion_quaternion.accel_y);
|
||||
write_int16_le(dst + 2, motion_quaternion.accel_x);
|
||||
write_int16_le(dst + 4, motion_quaternion.accel_z);
|
||||
|
||||
float quaternion[4] = {
|
||||
motion_quaternion.x,
|
||||
motion_quaternion.y,
|
||||
motion_quaternion.z,
|
||||
motion_quaternion.w,
|
||||
};
|
||||
uint8_t max_index = 0;
|
||||
for (uint8_t i = 1; i < 4; ++i) {
|
||||
if (fabsf(quaternion[i]) > fabsf(quaternion[max_index])) {
|
||||
max_index = i;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t packed_component[3]{};
|
||||
float sign = quaternion[max_index] < 0.0f ? -1.0f : 1.0f;
|
||||
for (uint8_t i = 0; i < 3; ++i) {
|
||||
int32_t component = static_cast<int32_t>(
|
||||
quaternion[(max_index + i + 1) & 3] * 1073741824.0f * sign);
|
||||
packed_component[i] = static_cast<uint32_t>(component >> 10) & 0x1FFFFFu;
|
||||
}
|
||||
|
||||
write_bits_le(dst, 48, 2, 2);
|
||||
write_bits_le(dst, 50, max_index, 2);
|
||||
write_bits_le(dst, 52, packed_component[0], 21);
|
||||
write_bits_le(dst, 73, packed_component[1], 21);
|
||||
write_bits_le(dst, 94, packed_component[2] & 0x3u, 2);
|
||||
write_bits_le(dst, 144, packed_component[2] >> 2, 19);
|
||||
|
||||
uint16_t timestamp = static_cast<uint16_t>(now_ms & 0x7FFu);
|
||||
write_bits_le(dst, 271, timestamp, 11);
|
||||
write_bits_le(dst, 282, 3, 6);
|
||||
}
|
||||
|
||||
static void fill_imu_report_data(const SwitchInputState& state, uint32_t now_ms) {
|
||||
switch (imu_mode) {
|
||||
case SwitchImuMode::Raw:
|
||||
fill_raw_imu_report_data(state);
|
||||
break;
|
||||
case SwitchImuMode::Quaternion:
|
||||
fill_quaternion_imu_report_data(state, now_ms);
|
||||
break;
|
||||
case SwitchImuMode::Off:
|
||||
default:
|
||||
memset(switch_report.imuData, 0x00, sizeof(switch_report.imuData));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static SwitchInputState make_neutral_state() {
|
||||
SwitchInputState s{};
|
||||
s.lx = SWITCH_PRO_JOYSTICK_MID;
|
||||
|
|
@ -425,14 +573,24 @@ static void handle_feature_report(uint8_t switchReportID, uint8_t switchReportSu
|
|||
canSend = true;
|
||||
LOG_PRINTF("[HID] FEATURE SET_HOME_LIGHT\n");
|
||||
break;
|
||||
case TOGGLE_IMU:
|
||||
is_imu_enabled = reportData[11];
|
||||
case TOGGLE_IMU: {
|
||||
SwitchImuMode requested_mode = SwitchImuMode::Off;
|
||||
if (reportData[11] == static_cast<uint8_t>(SwitchImuMode::Raw)) {
|
||||
requested_mode = SwitchImuMode::Raw;
|
||||
} else if (reportData[11] == static_cast<uint8_t>(SwitchImuMode::Quaternion)) {
|
||||
requested_mode = SwitchImuMode::Quaternion;
|
||||
}
|
||||
if (requested_mode == SwitchImuMode::Quaternion && imu_mode != requested_mode) {
|
||||
reset_motion_quaternion();
|
||||
}
|
||||
imu_mode = requested_mode;
|
||||
report_buffer[13] = 0x80;
|
||||
report_buffer[14] = commandID;
|
||||
report_buffer[15] = 0x00;
|
||||
canSend = true;
|
||||
LOG_PRINTF("[HID] FEATURE TOGGLE_IMU %u\n", is_imu_enabled);
|
||||
LOG_PRINTF("[HID] FEATURE TOGGLE_IMU %u\n", static_cast<unsigned>(imu_mode));
|
||||
break;
|
||||
}
|
||||
case IMU_SENSITIVITY:
|
||||
report_buffer[13] = 0x80;
|
||||
report_buffer[14] = commandID;
|
||||
|
|
@ -512,11 +670,12 @@ static void update_switch_report_from_state() {
|
|||
switch_report.inputs.rightStick.setX(std::min(std::max(scaleRightStickX,rightMinX), rightMaxX));
|
||||
switch_report.inputs.rightStick.setY(-std::min(std::max(scaleRightStickY,rightMinY), rightMaxY));
|
||||
|
||||
fill_imu_report_data(g_input_state);
|
||||
switch_report.rumbleReport = 0x09;
|
||||
}
|
||||
|
||||
void switch_pro_init() {
|
||||
imu_mode = SwitchImuMode::Off;
|
||||
reset_motion_quaternion();
|
||||
player_id = 0;
|
||||
last_report_counter = 0;
|
||||
handshake_counter = 0;
|
||||
|
|
@ -531,8 +690,8 @@ void switch_pro_init() {
|
|||
last_report_timer = 0;
|
||||
|
||||
device_info = {
|
||||
.majorVersion = 0x04,
|
||||
.minorVersion = 0x91,
|
||||
.majorVersion = 0x03,
|
||||
.minorVersion = 0x48,
|
||||
.controllerType = SWITCH_TYPE_PRO_CONTROLLER,
|
||||
.unknown00 = 0x02,
|
||||
.macAddress = {0x7c, 0xbb, 0x8a, static_cast<uint8_t>(get_rand_32() % 0xff), static_cast<uint8_t>(get_rand_32() % 0xff), static_cast<uint8_t>(get_rand_32() % 0xff)},
|
||||
|
|
@ -545,8 +704,8 @@ void switch_pro_init() {
|
|||
.timestamp = 0,
|
||||
|
||||
.inputs {
|
||||
.connectionInfo = 0x08, // wired connection
|
||||
.batteryLevel = 0x0F, // full battery
|
||||
.connectionInfo = 0x01, // Pro Controller powered by the console
|
||||
.batteryLevel = 0x08, // full battery
|
||||
|
||||
.buttonY = 0,
|
||||
.buttonX = 0,
|
||||
|
|
@ -618,8 +777,10 @@ void switch_pro_task() {
|
|||
}
|
||||
|
||||
if (is_ready && !report_sent) {
|
||||
if ((now - last_report_timer) > SWITCH_PRO_KEEPALIVE_TIMER) {
|
||||
switch_report.timestamp = last_report_counter;
|
||||
if ((now - last_report_timer) >= SWITCH_PRO_IMU_REPORT_TIMER) {
|
||||
// One timer tick per 5ms IMU frame; three frames per report.
|
||||
fill_imu_report_data(g_input_state, now);
|
||||
switch_report.timestamp += 3;
|
||||
void * inputReport = &switch_report;
|
||||
uint16_t report_size = sizeof(switch_report);
|
||||
if (tud_hid_ready() && send_report(0, inputReport, report_size) == true ) {
|
||||
|
|
|
|||
130
tests/test_controller_uart_bridge.py
Normal file
130
tests/test_controller_uart_bridge.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""Regression tests for SDL sensor buffering in the UART bridge."""
|
||||
|
||||
from argparse import Namespace
|
||||
from io import StringIO
|
||||
from types import SimpleNamespace
|
||||
from typing import Protocol, cast
|
||||
|
||||
import sdl3
|
||||
from rich.console import Console
|
||||
|
||||
import switch_pico_bridge.controller_uart_bridge as bridge
|
||||
from switch_pico_bridge.switch_pico_uart import (
|
||||
IMUSample,
|
||||
PicoUART,
|
||||
SENSOR_ACCEL,
|
||||
SENSOR_GYRO,
|
||||
SwitchButton,
|
||||
SwitchReport,
|
||||
UART_BAUD,
|
||||
)
|
||||
|
||||
|
||||
class MonkeyPatch(Protocol):
|
||||
def setattr(self, target: object, name: str, value: object) -> None: ...
|
||||
|
||||
|
||||
class RecordingUART:
|
||||
def __init__(self) -> None:
|
||||
self.sent_imu: list[tuple[IMUSample, ...]] = []
|
||||
|
||||
def send_report(self, report: SwitchReport) -> None:
|
||||
self.sent_imu.append(tuple(report.imu_samples))
|
||||
|
||||
def read_rumble_payload(self) -> bytes | None:
|
||||
return None
|
||||
|
||||
|
||||
def make_config() -> bridge.BridgeConfig:
|
||||
return bridge.BridgeConfig(
|
||||
interval=0.002,
|
||||
deadzone_raw=0,
|
||||
trigger_threshold=0,
|
||||
zero_sticks=False,
|
||||
zero_hotkey="",
|
||||
swap_hotkey="",
|
||||
button_map_default={},
|
||||
button_map_swapped={},
|
||||
swap_abxy_indices=set(),
|
||||
swap_abxy_ids=set(),
|
||||
swap_abxy_global=False,
|
||||
)
|
||||
|
||||
|
||||
def test_sensor_buffer_retains_latest_three_samples() -> None:
|
||||
controller = cast(sdl3.SDL_Gamepad, object())
|
||||
ctx = bridge.ControllerContext(controller, 7, 0, "dualsense", None, None)
|
||||
ctx.sensors_enabled = True
|
||||
ctx.gyro_bias_locked = True
|
||||
contexts = {ctx.instance_id: ctx}
|
||||
config = make_config()
|
||||
|
||||
accel_event = cast(
|
||||
sdl3.SDL_Event,
|
||||
cast(
|
||||
object,
|
||||
SimpleNamespace(
|
||||
gsensor=SimpleNamespace(
|
||||
which=ctx.instance_id,
|
||||
sensor=SENSOR_ACCEL,
|
||||
data=(0.0, 9.80665, 0.0),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
bridge.handle_sensor_update(accel_event, contexts, config)
|
||||
|
||||
for gyro_x in (1.0, 2.0, 3.0, 4.0):
|
||||
gyro_event = cast(
|
||||
sdl3.SDL_Event,
|
||||
cast(
|
||||
object,
|
||||
SimpleNamespace(
|
||||
gsensor=SimpleNamespace(
|
||||
which=ctx.instance_id,
|
||||
sensor=SENSOR_GYRO,
|
||||
data=(gyro_x, 0.0, 0.0),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
bridge.handle_sensor_update(gyro_event, contexts, config)
|
||||
|
||||
assert len(ctx.imu_samples) == 3
|
||||
assert [sample.gyro_y for sample in ctx.imu_samples] == [
|
||||
bridge.convert_gyro_to_raw(-2.0),
|
||||
bridge.convert_gyro_to_raw(-3.0),
|
||||
bridge.convert_gyro_to_raw(-4.0),
|
||||
]
|
||||
|
||||
|
||||
def test_service_republishes_latest_imu_window(monkeypatch: MonkeyPatch) -> None:
|
||||
uart = RecordingUART()
|
||||
controller = cast(sdl3.SDL_Gamepad, object())
|
||||
ctx = bridge.ControllerContext(
|
||||
controller, 7, 0, "dualsense", "/dev/null", cast(PicoUART, cast(object, uart))
|
||||
)
|
||||
ctx.sensors_enabled = True
|
||||
samples = [
|
||||
IMUSample(1, 2, 3, 4, 5, 6),
|
||||
IMUSample(7, 8, 9, 10, 11, 12),
|
||||
IMUSample(13, 14, 15, 16, 17, 18),
|
||||
]
|
||||
ctx.imu_samples = samples.copy()
|
||||
contexts = {ctx.instance_id: ctx}
|
||||
config = make_config()
|
||||
|
||||
def ignore_poll(
|
||||
_ctx: bridge.ControllerContext, _button_map: dict[int, SwitchButton]
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(bridge, "poll_controller_buttons", ignore_poll)
|
||||
|
||||
args = Namespace(baud=UART_BAUD)
|
||||
console = Console(file=StringIO())
|
||||
bridge.service_contexts(1.0, args, config, contexts, [], console)
|
||||
bridge.service_contexts(2.0, args, config, contexts, [], console)
|
||||
|
||||
assert uart.sent_imu == [tuple(samples), tuple(samples)]
|
||||
assert ctx.imu_samples == samples
|
||||
80
uv.lock
generated
80
uv.lock
generated
|
|
@ -451,6 +451,84 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hidapi"
|
||||
version = "0.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/f6/caad9ed701fbb9223eb9e0b41a5514390769b4cb3084a2704ab69e9df0fe/hidapi-0.15.0.tar.gz", hash = "sha256:ecbc265cbe8b7b88755f421e0ba25f084091ec550c2b90ff9e8ddd4fcd540311", size = 184995, upload-time = "2025-12-09T09:48:54.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/5a/46620fc194f3fa728dce1966ce977334b080fc33b8b525018ad0e0324b91/hidapi-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b0e1781f7fb8b4015e318d839d66fa79e98900d53900e31d04edb336e0103846", size = 70517, upload-time = "2025-12-09T09:44:47.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/97/bcbcb89f9461c29d3b12dd32affd29e6312fd521154ee7f394496d0039a9/hidapi-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fa3e792987d4b7ed66d785491307e23d4f09d3636f8a23665a9694c43e92409", size = 70181, upload-time = "2025-12-09T09:44:49.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/19/b9f1cd2bce226ed43afa7e7649caeee5552e7ea844e997521d747f0da184/hidapi-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c2d3b83a8300953653ddd7f5190d5ee6072d4a6ee5944db47e92d97344ed92", size = 1039531, upload-time = "2025-12-09T09:44:55.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b9/5a6a1a4219ada2da251225c706d450076fd2b3d624000699ff4d329326ab/hidapi-0.15.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:91b43099b123363c8935a264acffb4c7e5c8dba3390f9b2bf19ff76597677a31", size = 1573936, upload-time = "2025-12-09T09:44:51.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/7c/a2df1c993db58f9337d5ee64d3c58f5b9c5127f3ef68907d0c50070d1e50/hidapi-0.15.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:cea09ccc3efa5b92ab18d3ae8636836edce2c421d67ed4409761da090e230e5a", size = 1654515, upload-time = "2025-12-09T09:44:54.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/ca/341b3f5c713f72a7e61a942859b003a507f4a7d7cf5135a20da519bbcf05/hidapi-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b2de66142cf780ee7c797b7c35e488d67bccdf2de98178ac12a4038c490c44ea", size = 669173, upload-time = "2025-12-09T09:44:57.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/55/e5922d48b59959820d976c8e232b14c1f57d65742e416831ce53fda32f6b/hidapi-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e283c3e9255850d66152adbf580744d38c840d399141b98d64d359cb97cbc128", size = 664906, upload-time = "2025-12-09T09:44:58.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b2/87683617901cbc5232a1d99cb8e51a967792aae48d462a8e2a842d63ec25/hidapi-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5b9f6d2f3bc15c718d8e2ce349d2d019a0fcc343ed1a705a91d0f1c7e792d6ea", size = 671123, upload-time = "2025-12-09T09:45:00.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/cc/03f7d56b82a9dc2abcfcd8d55915504e156b6558fb66926629836e551595/hidapi-0.15.0-cp310-cp310-win32.whl", hash = "sha256:81de6b5fcb4fbbbfc71c6d201a2ac6914d1d86e51930ffde5f96faf50e922473", size = 60160, upload-time = "2025-12-09T09:45:04.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/20/a39e33a9088d76f98f80d9320f8413bdaeaa0458b42470e63a47ac4ccf94/hidapi-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:c36895abaef3a4004af6c5020ca214fcdacb2a491e4cde5576afbb1dad903548", size = 67063, upload-time = "2025-12-09T09:45:02.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/c5/3cfa157e7d1fd6af5ad52ea6ea031b1c8da141c61a2506ad5cb3420afa7f/hidapi-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53d018e6ac639a217c019c6dcd79a1d30c2401ac7a8147eedd11f2aa29307661", size = 70578, upload-time = "2025-12-09T09:45:05.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/7e/a43efc66b3b0a68058e76ad0cb2267ce9b30d9006dce10824f3c8b314b08/hidapi-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a11ef4b5ab0a2f36e35f44af394bff41cb645b03ce36c5b2f2c00873f112661f", size = 70233, upload-time = "2025-12-09T09:45:06.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/30/d8551d99528e0b7b2962cc76fb9a2b5990ea16aeb1457d1c3c61015d020f/hidapi-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e47d4fe13ddfc63fd1961974e10f3c7d50a57ae0bb73974e465cfe95dad5da18", size = 1070472, upload-time = "2025-12-09T09:45:16.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/26/433fc4e9efdfb9d1beb52e39ccdec265ae02003b1d2f8395aba3022b665b/hidapi-0.15.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f39917c9e9bea895384b1a8869159d808b4a53561af9b4f7c3148bdf6bfd97a0", size = 1602092, upload-time = "2025-12-09T09:45:08.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/13/911b757f6847a197ab5c978fe8dd55181035096a5659cf7183e013ed1ea2/hidapi-0.15.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f4d294fcd2551fb5b41d5f478a08e8945382e1d86d7d40320d02594922d6b667", size = 1677973, upload-time = "2025-12-09T09:45:10.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/3f/cd92833886a15b34225bbd765ed1eb0270bc78b4cf6385e302c0e4bae494/hidapi-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:68cb85467151a3d4aa6ab4e11ca89a87c2404a778dbccb8fb5ae51e998133bb3", size = 699789, upload-time = "2025-12-09T09:45:18.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/0e/9cdad5783bf1b2aea6667da37f2435b286750d650071dcc50b42834c8810/hidapi-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:06fbcd7696ad768d7e9500b64e2ae742b5d90a2531b5aa8d355c0f049516a09d", size = 695983, upload-time = "2025-12-09T09:45:20.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/ac/a57bc6e28d0dc1a0aed3dcc24302262ecbf2b723f9c4ba1af4ea51add5b4/hidapi-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:221e3a0c181503061bfc4a672eccd9f86b1da1167b31914be00d919edfd6e1bf", size = 697710, upload-time = "2025-12-09T09:45:21.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/40/6a45375a52027d8142e7acdaeab182a44ff6b818df41384019662eb4f351/hidapi-0.15.0-cp311-cp311-win32.whl", hash = "sha256:2c35bd9cc62227ec91047e36b260f75bdbf50814f3cf3c3b28648ac3ffabd9d7", size = 60070, upload-time = "2025-12-09T09:45:24.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/a9/8ad4e6423c7416eb8dd765327f3be67f083c985b41b9b48ef3061a64c5f2/hidapi-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c5f7ee9ce8e3373fdb7002497f16bc652d9d4acf20c91275877f55165caf6f0c", size = 67296, upload-time = "2025-12-09T09:45:23.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/fd/9076aba0736f339b71bda5ee26cb678c02420bf96c7ea5bd9ebcbaf0aa3c/hidapi-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b7eb950214191c936b5bdabaeaf1cab99c0dfc3ae3edc220e3c6d4547296053", size = 70157, upload-time = "2025-12-09T09:45:25.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/9c/9335957cb7e1ca1be997e97afe850f62a9ff0708015048b3871b553eed5c/hidapi-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27933e7f3da7007e5e8d0b25bcf48a79a74e802555e496bba2c7d87f8a77cd61", size = 69597, upload-time = "2025-12-09T09:45:26.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/bc/0eb92f4f238adc8edc3decb859662cf87e302b0a6ecdabe0e4d0560eb5fd/hidapi-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cfdbc74d5c4b5fdeef58a246d80f7dbf4ca33fe741c889505a4a350dd2eb54fb", size = 1082139, upload-time = "2025-12-09T09:45:32.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/04/856827cfecaa89091ed470f697e3f9cfb172fc852960e6b902e765d7d650/hidapi-0.15.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:4b7b6207c1c7df3524e9d4019c7196547327636e9924010b9b0bf0c40029e329", size = 1620074, upload-time = "2025-12-09T09:45:28.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/63/1338cf0e273d95e07202e391b17198f27bd2cfef71348707eac98cd8db2b/hidapi-0.15.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:908e4b8c35041aa800b51094fd9fbaa5d6161bfb71e55821f43c6433f54e2865", size = 1689689, upload-time = "2025-12-09T09:45:31.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/08/28ce99698e14bb6bdf9d0dcf7ab45ab86d8bade894885aa15bb3fa43bcc1/hidapi-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f9356fffebf723ccfbf0f9370d3725de37b3e2f1f6bffc40c3466f854542861c", size = 713596, upload-time = "2025-12-09T09:45:34.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/16/23e446e2d0ad0d75de149d94f8e6313329aec4d7c18d027e56f5a386779e/hidapi-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fd082e13ca92bbd7a4ec65d9fbd9ef06ecd71798ce9f036b7144b3d498623a7e", size = 697243, upload-time = "2025-12-09T09:45:36.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/ef/e2fa4d795235eb09951f806bbfdbd766d1be805df448d67918e828b7cb31/hidapi-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35eb5797d86306bd67aee80ad8973ab0a0b4e6d54018d3efb328767d0e8c9373", size = 717599, upload-time = "2025-12-09T09:45:37.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/a7/9b7fe9acd09ed90d702f8cc01190ab53e01d5022c21808b079144bc9017d/hidapi-0.15.0-cp312-cp312-win32.whl", hash = "sha256:ce6f99554dae15c48cd89a12d5aede77a92a8bd184b45d0b257a17c97e053cdb", size = 60136, upload-time = "2025-12-09T09:45:40.612Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/35/f9e2d3ead60b573140546b041dd41c78a48c0e6b573d61a03130adccd32e/hidapi-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:9abc71a62bf8a8f1d70a6fd3613f86feb37ddb67e05ed934e734df79e27bf4f6", size = 67073, upload-time = "2025-12-09T09:45:39.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/19/bda2dce4af8b8028e6d611d5caf60e223a4d32a638d1155eefdc6d8f2462/hidapi-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f336dd2dc308928d54a6fdce137814a941a3633ad6722919d7a3142dd99005c2", size = 69045, upload-time = "2025-12-09T09:45:41.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/18/9b7d6b6a7561654b9b0d7b22fba9c5c971b36f5fc541b04e02d71928349d/hidapi-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8017f2213810c3e57d9ab64c2328c593a1129b25804240108a6451fe35cde193", size = 68755, upload-time = "2025-12-09T09:45:42.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/b0/f144cee5ce9e45518d234fd07c7a4f9a66de3a78ed42bf9f3f4083165e10/hidapi-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:208c7fc89f78c3448d55957c3e12893778aa91218ed7dc22697e554f091fe4fe", size = 1072286, upload-time = "2025-12-09T09:45:47.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/bf/7fee015cc48b719bd40c6416f65084a900e7889f0cf52fcfedb9be107b10/hidapi-0.15.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:90f8bd963ae43a44036af5d689d090fab0e3eb52861186e4d2b863dd910a2726", size = 1609789, upload-time = "2025-12-09T09:45:44.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/b6/ac33532711c2ecda5a22d8b2db3223c8a13f16db6d2793ee73a809cca5b6/hidapi-0.15.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:cd6edc7af885755163f4ffb29639c14f8a4ad5cca13ed641e47aa11f9646d8b6", size = 1680636, upload-time = "2025-12-09T09:45:45.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/e1/5b4426b8a77f9efbef70c0dbc1d64602d36ea8878f76c3607959901c978c/hidapi-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6ef0f80b2ff32ba446c9a2d4bfca6623ab5f0c05d742a0161ea1f7af2dc33b12", size = 708617, upload-time = "2025-12-09T09:45:48.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/a6/1ecdadc3e19f6755eed40f2111f447ec071e3d4f5f4f99b95d26a6d67219/hidapi-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6106d10873c1e465a2764e979fb96ec0d16bb926eedac829310830f90816cc76", size = 689589, upload-time = "2025-12-09T09:45:50.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/02/579af4220acdde5ab76d091edaa92f62e110f89131a56b11856f77779607/hidapi-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f7a710df215ea3b53a81029488f78e7303b83f01a2e8478db20d7c9756588dd", size = 710213, upload-time = "2025-12-09T09:45:51.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c7/52f2d903a607f024086829349383dcc7f0c22533fc242f26f9754eba2f06/hidapi-0.15.0-cp313-cp313-win32.whl", hash = "sha256:e4ddb57e71e2b8aca2c685b94b8587dc7d7cfae3c529a7c4076ba0775eb28d4a", size = 59749, upload-time = "2025-12-09T09:45:53.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/00/7dd3f866a748b0c9eb4adedaa025ec8c533ed1946e9e918661c948a5c0f4/hidapi-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:04e0c1ed6d742bc6e1d00599394bec6b2afb8ee2fc5a0a183d3c4c2d4e315c34", size = 66362, upload-time = "2025-12-09T09:45:52.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/65/e9c70deb396f5baea92cd2ea7803914a55d455c3982367bae4390483e6cc/hidapi-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a6bb079bcd5152dd8968893aecdcbde3954b849896078f54df2254c0d1f301e", size = 69532, upload-time = "2025-12-09T09:45:55.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/eb/d006a7e28ec63b5db76340e4aca6ff077ad336f8fbe64ac804b968927011/hidapi-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d80c745d285dfc9889e856226daa8eee6f9e83025ef2cb97e5fe0b1396f7818a", size = 69350, upload-time = "2025-12-09T09:45:56.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/2b/95e7595eb61982c651ed2b3ecd9bae05a0298b668c801a992e4e814b22f2/hidapi-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0f4411fd479345ca86742d9dda77200476a902749d8e47ed438f31ec98af418", size = 1065210, upload-time = "2025-12-09T09:46:02.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/a5/0344a7c223c3610510ec7f91f7220b0e5852c92d52aff23377f1c2888880/hidapi-0.15.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:d93f7173dcbec61b928f4cf800bf26535f81ef3b7bd89199237207f6ca7e4f43", size = 1610362, upload-time = "2025-12-09T09:45:58.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/96/781212cf852705bc1db26edd4340b01b9ce04cadd5d327c00b64f7324978/hidapi-0.15.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:dacf0b9e5d776fde69b92d68c09ab878a09486629e1fe8e6ae851252b0f33ce5", size = 1680942, upload-time = "2025-12-09T09:46:00.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/48/6d51a37d3a81355740a82cd265d4fc8485a0a279e4b545cbf790bd106f51/hidapi-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda6ce04a77662e77a4b6b2598211935ca281e1cf663248d54bce75bd684fed8", size = 707550, upload-time = "2025-12-09T09:46:03.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/77/175efa84ecea161c70948dc67b412b7444c2a196e4def8b305cb756077c6/hidapi-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:74900b5c7e884e0e4316d4deb746e5dd294b5aaaf90eac056c4f9f19cf2c8097", size = 689865, upload-time = "2025-12-09T09:46:05.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/29/0e513390a8d686448672fabd30385f9cda6e01663765851d0cef88e6f0b1/hidapi-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:76236468509dfcb256fa0fbd5a73708c598b3819695b7d08499ae489c82116b6", size = 706809, upload-time = "2025-12-09T09:46:07.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/d6/c0b9d8568fdb38eebb9caaa56643a81d92cccb5d59a5b44e0d975a0f3808/hidapi-0.15.0-cp314-cp314-win32.whl", hash = "sha256:901c03817306eac7edd589f09e0e07467871b45f665949cc4caa645b54abc97b", size = 61089, upload-time = "2025-12-09T09:46:09.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/14/3b27516b2867e53545164ebd8ef45d0c36857d2a062036e788e56e2e34ce/hidapi-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:df0dd435562654e27ad0c7d045074eb1e1e016b09167dd48a258dec1ce4b9127", size = 67710, upload-time = "2025-12-09T09:46:08.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/8f/ff034661faf99acaabe8954fb7db87c5242c79bf763dd972b23b1be5f104/hidapi-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2f16e9ea8a1d24ac7a6bf316905823eafd58f78ec815a93118d4710faf95a224", size = 70893, upload-time = "2025-12-09T09:46:10.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/44/617666a0919d06521d732ef07998306b8ddcbd96038e19348c831acd76e8/hidapi-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:77f0965ca81a9be44694e84aa18e501d5cda49e372202a38a52d22acc38deadd", size = 71430, upload-time = "2025-12-09T09:46:11.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/64/129aed49fa45b6a5e2fd955618eebe93ff7e3889f05237da6b45051c77bb/hidapi-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:541c85b8225ce19b38ceacd404283ca1857345ccfa7ca12f54131203ff2d7f75", size = 1094257, upload-time = "2025-12-09T09:46:17.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/ca/266ca521ff643957823a7f04a67030f6d2917e86e31c60c3d4878cd6b913/hidapi-0.15.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:89f909a6783307c2f067e8e7948f38ec43386f15d04759d11e5b32e3b2fdf3d8", size = 1663756, upload-time = "2025-12-09T09:46:13.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/de/eb9d0fe6d9ccd2645e993930cc7fdeb6e12ddcdec086f3ef7dec947286ca/hidapi-0.15.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:fd32f311633fad21a52e750775cd4ea23da912199c63d1331f4d8ae00ab0874b", size = 1710998, upload-time = "2025-12-09T09:46:15.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/43/e5c0cd80df4a65a468b7a9fc6891ea77435a75e64bb273104c37f12e05b3/hidapi-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f7ca38699557ab057333f1628a346073ae6088c09897b2cd9fbc68f4896780c", size = 751383, upload-time = "2025-12-09T09:46:19.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/8f/033e19857bcc3ad1b9880b5c0dfbaa7c945e577b7dee18919c1b2478484d/hidapi-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1c4e707c82629637a311fa1a7adc1ae1386de7b539e84b4e37125e52e449318d", size = 722837, upload-time = "2025-12-09T09:46:20.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/f0/2b4e7ff42cdc6a6bf7def0591dfd3854af81038a1925ac1ed7a96578e834/hidapi-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc99bb16b359023190bf857297f6492a1c8441ccacff66dac881c9e278f5c262", size = 750923, upload-time = "2025-12-09T09:46:23.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/eb/c0676ff1f7e9ec9d99eaa428c66745cdab5b1742808f0c16061c85bd0b18/hidapi-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:005c84c74c940a314b211674edc763a931ace0b63685ace88148496f563e25f9", size = 66288, upload-time = "2025-12-09T09:46:25.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/5b/f22dbf886824559d3e66d84710c5fb48c09856e6816885da97f2f51700eb/hidapi-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e384430363d400c4ffb33fe65f22a61c9f288a870158dc5a9f5c9d917b7250c7", size = 74723, upload-time = "2025-12-09T09:46:24.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/c0/dc8d3fa85a5a4feb6ed8d79da9f5fefdd3b1607ce9220e1b54faa95927e5/hidapi-0.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3d220314975b99a083f4ee21abfd796a9ceeb99cbf3487a41c5eb80269102a4a", size = 71079, upload-time = "2025-12-09T09:46:40.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/c0/73d0b576332467e1ff84c51bef3eb21f07125835a7a580f4047919a67d1d/hidapi-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc61ab7f45dddbb2df2fca93565c575c0b035337d1e13ce2868c092ff24e9c47", size = 70800, upload-time = "2025-12-09T09:46:41.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/a7/9819036af3a3f6ab58570c4292c28bfd42ac9898e04070f598e92939ac51/hidapi-0.15.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aefc2afd146733e3ec8b33474bfb832c9281dd106309a216a20bade19dc0e95f", size = 1039182, upload-time = "2025-12-09T09:46:49.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/c4/f696f62702b75eccc4dbcf619e82cf773b0d892d2bcc7e9cf85aefb3d9f2/hidapi-0.15.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ef3b43b1a4d4df8ea542487cda888138eaac4707db46bbca0f7bb78d97a06323", size = 1572449, upload-time = "2025-12-09T09:46:43.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/7c/e58643470288fc6132933475ba810482616ad08c333770f23e9d4c9f31ca/hidapi-0.15.0-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:0608a882f06d7f37e0f1786e255e9a8b1e2386fdb8f9fb9d209b82682551f4b8", size = 1654806, upload-time = "2025-12-09T09:46:46.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a0/021d8f40ebed1f21a90c0f6c794657e3e9e9fe013788e2467bc5ce3b42f2/hidapi-0.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:788e16711a4530a2a7a8a4cd942f7250fa7f907a3be892b11d8eb51ec697739a", size = 667407, upload-time = "2025-12-09T09:46:50.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/a5/4c7355b133be152d1e085190e37880230a47575a853752a06a824cd37951/hidapi-0.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:48e79274384c23b250bc95016c8aa6497cf0a23b3a112d05376cbf5ace266886", size = 665330, upload-time = "2025-12-09T09:46:51.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/02/24656533d5b7d0047ca70f9abaeae8fd9fe694c32e91a88a5885652aa605/hidapi-0.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c49aa659ecfe46a93a7057bca42208bfd123c2fcbdfd4a28eb1f9b8189260b68", size = 669911, upload-time = "2025-12-09T09:46:53.202Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/4e/7dd283c34d85898b4a259b8ea1116800de2b603256b20d19fad31bf88be4/hidapi-0.15.0-cp39-cp39-win32.whl", hash = "sha256:27d288d405bd4abedd047eefb9c4ab797d232454b35e17e7b61a03edf70c64e8", size = 60498, upload-time = "2025-12-09T09:46:55.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/8c/b51934ac6dd24409d94b949c143bb55207a3a8213a520671cbdd95f24f42/hidapi-0.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:cb835b81624a72830e5ebebde26b93ab5ee812de648386a47d13dfbf64b348c7", size = 67432, upload-time = "2025-12-09T09:46:54.653Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
|
|
@ -859,6 +937,7 @@ name = "switch-pico-bridge"
|
|||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "hidapi" },
|
||||
{ name = "pysdl3" },
|
||||
{ name = "pyserial" },
|
||||
{ name = "rich" },
|
||||
|
|
@ -866,6 +945,7 @@ dependencies = [
|
|||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "hidapi" },
|
||||
{ name = "pysdl3" },
|
||||
{ name = "pyserial" },
|
||||
{ name = "rich" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue