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>
317 lines
12 KiB
C++
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");
|
|
}
|
|
}
|