Files
DS5Dongle-OLED-Edition-stea…/src/cmd.cpp
T
MarcelineVPQandClaude Opus 4.7 1b1944c006 feat(remap): on-dongle button remapping over 0xF6/0xF7
16-entry source→target table in its own flash sector (-3, magic DS5\x03),
identity default, 0xFF = disabled. Applied to the OUTGOING host report copy
only — raw interrupt_in_data (OLED screens + PS+Mute reboot combo) stays
physical. Edited via the existing 0xF6/0xF7 vendor reports with a hardened
RM+version frame (no HID-descriptor change, so Windows enumeration is
unaffected) plus a revision counter the host polls to confirm a write.
Apply logic + button set ported from SundayMoments/DS5_Bridge (credit).

scripts/remap_test.py exercises the path over /dev/hidraw without the web
tool. Verified on hardware: swap applied, read back, survived reboot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 10:42:29 -06:00

317 lines
12 KiB
C++

//
// Created by awalol on 2026/5/4.
//
#include "cmd.h"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include "audio.h"
#include "bt.h"
#include "config.h"
#include "device/usbd.h"
#include "pico/time.h"
#include "slots.h"
#include "remap.h"
#include "hardware/clocks.h"
#include "hardware/adc.h"
#include "hardware/vreg.h"
uint16_t cpu_temp_raw_smoothed() {
// One-time ADC bring-up. This is the only place the ADC is initialised
// now (oled.cpp's CPU screen calls through here too). Runs on core0
// under the cooperative main loop; adc_select_input(4) is set before
// every read, so the shared ADC needs no locking.
static bool adc_ready = false;
if (!adc_ready) {
adc_init();
adc_set_temp_sensor_enabled(true);
adc_ready = true;
}
adc_select_input(4);
// The temp sensor has a shallow slope (-1.721 mV/C) and ~1 LSB ≈ 0.47 C,
// so a lone 12-bit sample swings several tenths of a degree frame to
// frame. Average a big block to kill that...
constexpr int kSamples = 256;
uint32_t acc = 0;
for (int i = 0; i < kSamples; i++) acc += adc_read();
const float mean = (float)acc / (float)kSamples;
// ...then a slow EMA so the displayed value glides to the true die
// temperature rather than mirroring the latest block. Seeded on the
// first call so it doesn't ramp up from zero.
static float ema = -1.0f;
if (ema < 0.0f) ema = mean;
else ema += (mean - ema) * 0.15f;
return (uint16_t)(ema + 0.5f);
}
// Mic-debug globals (defined in main.cpp). File-scope extern so the
// linker resolves them once and cmd.cpp's 0xFD handler reads the same
// memory main.cpp writes to.
extern volatile uint32_t g_bt_31_packets;
extern volatile uint32_t g_bt_other_packets;
extern volatile uint8_t g_last_other_id;
extern volatile uint8_t g_other_id_or;
extern volatile uint8_t g_31_b2_or;
extern volatile uint8_t g_last_31_b2;
extern volatile uint16_t g_31_len_min;
extern volatile uint16_t g_31_len_max;
extern volatile uint8_t g_last_other_prefix[8];
extern volatile uint8_t g_last_any_prefix[16];
extern volatile uint16_t g_longest_len;
extern volatile uint8_t g_longest_frame[80];
extern volatile uint32_t g_host_out02_total;
extern volatile uint32_t g_host_out02_trig_allow;
extern volatile uint32_t g_host_out02_to_bt;
bool is_pico_cmd(uint8_t report_id) {
if (report_id == 0xf6 ||
report_id == 0xf7 ||
report_id == 0xf8 ||
report_id == 0xf9 ||
report_id == 0xfa ||
report_id == 0xfb ||
report_id == 0xfc ||
report_id == 0xfd || // mic-debug counters
report_id == 0xfe // mic-debug longest-frame dump
) {
return true;
}
return false;
}
uint16_t pico_cmd_get(uint8_t report_id, uint8_t *buffer, uint16_t reqlen) {
if (report_id == 0xf7) {
printf("[HID] Receive 0xf7 getting config\n");
const size_t cfg_len = sizeof(Config_body);
if (cfg_len > reqlen) {
printf("[Config] Warning: Config_body overflow\n");
}
const auto len = std::min(cfg_len, static_cast<size_t>(reqlen));
memcpy(buffer, &get_config(), len);
// OLED Edition: append the button-remap block right after Config_body
// when the host asked for enough room. Old clients request exactly
// sizeof(Config_body) and never see it; new web tools read config +
// remap in one GET (the 0xF6/0xF7 reports are 63 bytes, plenty).
// [+0] 'R'
// [+1] 'M'
// [+2] protocol version (kRemapProtoVer)
// [+3..+4] revision uint16 LE (bumps on each successful set)
// [+5..+20] 16-byte remap table (source idx -> target idx, 0xFF=off)
constexpr size_t kRemapBlock = 5 + kRemapCount;
if (reqlen >= cfg_len + kRemapBlock) {
uint8_t *p = buffer + cfg_len;
p[0] = 'R';
p[1] = 'M';
p[2] = kRemapProtoVer;
const uint16_t rev = remap_revision();
p[3] = (uint8_t)(rev & 0xFF);
p[4] = (uint8_t)((rev >> 8) & 0xFF);
remap_get(p + 5);
return cfg_len + kRemapBlock;
}
return len;
}
if (report_id == 0xf8) {
printf("[HID] Receive 0xf8 getting firmware version\n");
const auto len = std::min(strlen(PICO_PROGRAM_VERSION_STRING), static_cast<size_t>(reqlen));
memcpy(buffer, PICO_PROGRAM_VERSION_STRING, len);
return len;
}
if (report_id == 0xf9) {
// [-128,0]
int8_t rssi = 0;
bt_get_signal_strength(&rssi);
if (reqlen == 0) {
return 0;
}
buffer[0] = rssi;
#if ENABLE_VERBOSE
printf("[HID] 0xf9 RSSI=%d raw=0x%02X\n", rssi, buffer[0]);
#endif
return 1;
}
if (report_id == 0xfa) {
// OLED Edition: 4 x bd_addr (6 bytes each) + 4 x occupied flag = 28 bytes.
constexpr uint16_t want = 28;
if (reqlen < want) {
printf("[HID] 0xfa reqlen=%u too small for slots payload (%u)\n", reqlen, want);
return 0;
}
for (int i = 0; i < 4; i++) {
uint8_t addr[6];
bt_slot_get_addr(i, addr);
memcpy(buffer + i * 6, addr, 6);
}
for (int i = 0; i < 4; i++) {
buffer[24 + i] = bt_slot_occupied(i) ? 1 : 0;
}
return want;
}
if (report_id == 0xfb) {
// OLED Edition: diagnostics + audio meters for the web emulator.
constexpr uint16_t want = 18;
if (reqlen < want) {
printf("[HID] 0xfb reqlen=%u too small for diag payload (%u)\n", reqlen, want);
return 0;
}
const uint32_t uptime_s = time_us_32() / 1000000u;
const uint32_t usb_frames = audio_usb_frames();
const uint32_t bt_packets = audio_bt_packets();
const uint32_t hci_errs = bt_hci_err_count();
memcpy(buffer + 0, &uptime_s, 4);
memcpy(buffer + 4, &usb_frames, 4);
memcpy(buffer + 8, &bt_packets, 4);
buffer[12] = audio_peak_speaker();
buffer[13] = audio_peak_haptic();
memcpy(buffer + 14, &hci_errs, 4);
return want;
}
if (report_id == 0xfc) {
// OLED Edition: CPU / Clock telemetry for the web emulator. 11 bytes:
// [0..3] set_khz uint32 configured clk_sys (SYS_CLOCK_KHZ)
// [4..7] real_khz uint32 measured clk_sys (cached, see below)
// [8] vcode uint8 vreg_get_voltage() raw enum code
// [9..10] temp_raw uint16 ADC ch4 12-bit reading
// The web side does the volts/temperature math (same formulas as
// render_screen_cpu) so the firmware HID path stays float-free.
constexpr uint16_t want = 11;
if (reqlen < want) {
printf("[HID] 0xfc reqlen=%u too small for cpu payload (%u)\n", reqlen, want);
return 0;
}
const uint32_t set_khz = (uint32_t)SYS_CLOCK_KHZ;
// clk_sys is fixed at boot and frequency_count_khz() busy-waits a few
// ms — measure exactly once (lazily) and cache. Doing it here on the
// first poll keeps it off the boot path; one ~ms stall in a single
// GET_REPORT is acceptable.
static uint32_t cached_real_khz = 0;
if (cached_real_khz == 0) {
cached_real_khz = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS);
}
const uint16_t temp_raw = cpu_temp_raw_smoothed();
const uint8_t vcode = (uint8_t)vreg_get_voltage();
memcpy(buffer + 0, &set_khz, 4);
memcpy(buffer + 4, &cached_real_khz, 4);
buffer[8] = vcode;
memcpy(buffer + 9, &temp_raw, 2);
return want;
}
if (report_id == 0xfd) {
// Bridge-diagnostics feature report. 44-byte payload.
// Section 1: mic-investigation counters (original 0..31).
// [0..3] uint32 BT 0x31 input report count
// [4..7] uint32 BT non-0x31 input report count
// [8] uint8 last non-0x31 report ID seen
// [9] uint8 OR mask of all non-0x31 report IDs seen
// [10] uint8 OR mask of byte[2] across all 0x31 frames
// [11] uint8 last value of byte[2] in a 0x31 frame
// [12..13] uint16 min frame length seen
// [14..15] uint16 max frame length seen
// [16..23] uint8[8] first 8 bytes of last non-0x31 frame
// [24..31] uint8[8] first 8 bytes of most recent ANY frame
// Section 2: trigger-flow counters (issue #3 triage).
// [32..35] uint32 host 0x02 OUT reports received total
// [36..39] uint32 ...of those, with Allow*TriggerFFB set
// [40..43] uint32 ...forwarded as BT 0x31 sub-0x10
constexpr uint16_t want = 44;
for (uint16_t i = 0; i < want && i < reqlen; i++) buffer[i] = 0;
const uint32_t bt31 = g_bt_31_packets;
const uint32_t btother = g_bt_other_packets;
const uint16_t lmin = g_31_len_min == 0xFFFF ? 0 : g_31_len_min;
const uint16_t lmax = g_31_len_max;
memcpy(buffer + 0, &bt31, 4);
memcpy(buffer + 4, &btother, 4);
buffer[8] = g_last_other_id;
buffer[9] = g_other_id_or;
buffer[10] = g_31_b2_or;
buffer[11] = g_last_31_b2;
memcpy(buffer + 12, &lmin, 2);
memcpy(buffer + 14, &lmax, 2);
for (int i = 0; i < 8 && (16 + i) < reqlen; i++) buffer[16 + i] = g_last_other_prefix[i];
for (int i = 0; i < 8 && (24 + i) < reqlen; i++) buffer[24 + i] = g_last_any_prefix[i];
const uint32_t out02 = g_host_out02_total;
const uint32_t out02_t = g_host_out02_trig_allow;
const uint32_t out02_b = g_host_out02_to_bt;
if ((32 + 4) <= reqlen) memcpy(buffer + 32, &out02, 4);
if ((36 + 4) <= reqlen) memcpy(buffer + 36, &out02_t, 4);
if ((40 + 4) <= reqlen) memcpy(buffer + 40, &out02_b, 4);
return (reqlen < want) ? reqlen : want;
}
if (report_id == 0xfe) {
// 0xFE: full content of the LONGEST 0x31 frame seen. Bytes 0-1
// = length (uint16 LE), bytes 2+ = the captured frame bytes.
constexpr uint16_t want = 82; // 2 length + 80 frame bytes
const uint16_t lim = (reqlen < want) ? reqlen : want;
for (uint16_t i = 0; i < lim; i++) buffer[i] = 0;
const uint16_t llen = g_longest_len;
if (lim >= 2) {
buffer[0] = (uint8_t)(llen & 0xFF);
buffer[1] = (uint8_t)((llen >> 8) & 0xFF);
}
for (uint16_t i = 0; i < 80 && (i + 2) < lim; i++) {
buffer[2 + i] = g_longest_frame[i];
}
return lim;
}
return 0;
}
void pico_cmd_set(uint8_t report_id, uint8_t const *buffer, uint16_t bufsize) {
(void) report_id;
if (bufsize == 0) {
return;
}
// 0x01 update config in variable
// 0x02 write config to flash
// 0x03 reconnect tinyusb device;
if (buffer[0] == 0x01) {
printf("[CMD] Enter config set func\n");
set_config(buffer + 1, bufsize - 1);
}
if (buffer[0] == 0x02) {
printf("[CMD] Enter config save func\n");
config_save();
}
if (buffer[0] == 0x03) {
printf("[CMD] Enter tud reconnect func\n");
tud_disconnect();
sleep_ms(150);
tud_connect();
}
// 0x10 set button-remap table (OLED Edition). Hardened framing so a stray
// write to 0xF6 can't corrupt the map: magic 'R''M' + protocol version gate
// before remap_set() (which itself validates each entry <16 or 0xFF=off).
// [0] 0x10 func-id
// [1] 'R'
// [2] 'M'
// [3] protocol version (must == kRemapProtoVer)
// [4..19] 16-byte remap table
if (buffer[0] == 0x10) {
constexpr uint16_t kNeed = 4 + kRemapCount;
if (bufsize < kNeed) {
printf("[CMD] 0x10 remap-set too short (%u<%u)\n", bufsize, kNeed);
return;
}
if (buffer[1] != 'R' || buffer[2] != 'M' || buffer[3] != kRemapProtoVer) {
printf("[CMD] 0x10 remap-set bad magic/version\n");
return;
}
if (remap_set(buffer + 4)) printf("[CMD] remap set ok (rev=%u)\n", remap_revision());
else printf("[CMD] remap set rejected (invalid table)\n");
}
}