rebase: Phase B — trivial files for the v0.6.0 base
Build-system, config, usb, and CI wiring on top of upstream's
v0.6.0-hotfix tree. Build will still fail until Phase C+D land bt.h
accessors that oled.cpp references; that's expected mid-rebase.
CMakeLists.txt: add src/oled.cpp + src/slots.cpp to add_executable,
link hardware_spi, restore OUTPUT_NAME ds5-bridge-oled.
src/config.h: append the 4 OLED Edition Config_body fields
(current_slot + auto_haptics_enable/gain/lowpass). Preserves
upstream's field order for the first 8 fields.
src/config.cpp:
- speaker_volume validity default -100 dB → 0 dB (footgun fix)
- 4 new validity guards for the appended fields with sensible
defaults (Fallback / 100 % / 160 Hz / slot 0)
- config_default() impl (was declared in upstream's config.h since
v0.5.4 but never defined; the OLED Edition's "Reset to defaults"
Settings item calls it)
src/usb.cpp: drop the UAC1 SET_CUR Volume → flash-config sync that
let PipeWire/Pulse silently override the user's saved Spk Vol on
every device reconnect. Borrowed from loteran/DS5Dongle commit
03fa1e4.
src/usb_descriptors.cpp: restore iSerialNumber = STRID_SERIAL.
Upstream's commit e79c762 zeroed it to work around SpecialK (#32),
but that broke Windows device identity (#100) — users lost
per-device volume / app preferences when moving USB ports. The
OLED Edition prioritizes Windows device-identity stability for
the broader user base over SpecialK compat for a narrower one.
.github/workflows/{build,release}.yml: artifact filenames use
ds5-bridge-oled.uf2 (and -debug / -picow variants) per the
OUTPUT_NAME rebrand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0ed05d3f56
commit
4b6a72b70a
+27
-2
@@ -52,8 +52,8 @@ void config_valid() {
|
||||
printf("[Config] Haptics Gain value is invalid\n");
|
||||
}
|
||||
if (std::isnan(body->speaker_volume) || body->speaker_volume < -100 || body->speaker_volume > 0) {
|
||||
body->speaker_volume = -100;
|
||||
printf("[Config] Speaker Volume is invalid\n");
|
||||
body->speaker_volume = 0; // OLED Edition: 0 dB default (unity); -100 would be silent
|
||||
printf("[Config] Speaker Volume is invalid, defaulting to 0 dB\n");
|
||||
}
|
||||
if (body->inactive_time < 5 || body->inactive_time > 60) {
|
||||
body->inactive_time = 30;
|
||||
@@ -79,6 +79,22 @@ void config_valid() {
|
||||
body->controller_mode = 2;
|
||||
printf("[Config] controller_mode is invalid\n");
|
||||
}
|
||||
if (body->current_slot >= 4) {
|
||||
body->current_slot = 0;
|
||||
printf("[Config] current_slot is invalid\n");
|
||||
}
|
||||
if (body->auto_haptics_enable > 3) {
|
||||
body->auto_haptics_enable = 1; // Fallback default
|
||||
printf("[Config] auto_haptics_enable invalid, defaulting to 1 (Fallback)\n");
|
||||
}
|
||||
if (body->auto_haptics_gain > 200) {
|
||||
body->auto_haptics_gain = 100;
|
||||
printf("[Config] auto_haptics_gain invalid, defaulting to 100\n");
|
||||
}
|
||||
if (body->auto_haptics_lowpass > 3) {
|
||||
body->auto_haptics_lowpass = 1; // 160 Hz
|
||||
printf("[Config] auto_haptics_lowpass invalid, defaulting to 1 (160 Hz)\n");
|
||||
}
|
||||
if (body->config_version != CONFIG_VERSION) {
|
||||
body->config_version = CONFIG_VERSION;
|
||||
printf("[Config] Warning: Config may breaking change\n");
|
||||
@@ -91,6 +107,15 @@ void config_load() {
|
||||
config_valid();
|
||||
}
|
||||
|
||||
// Reset RAM-resident config body to firmware defaults. Caller must
|
||||
// config_save() to persist. Filling with 0xFF mirrors the byte pattern
|
||||
// of a freshly-erased flash sector, so every field fails validity and
|
||||
// gets re-initialized to its documented default by config_valid().
|
||||
void config_default() {
|
||||
memset(&config.body, 0xFF, sizeof(config.body));
|
||||
config_valid();
|
||||
}
|
||||
|
||||
bool config_save() {
|
||||
config.crc32 = calc_config_crc(config);
|
||||
alignas(4) uint8_t page[FLASH_PAGE_SIZE];
|
||||
|
||||
@@ -17,6 +17,12 @@ struct __attribute__((packed)) Config_body {
|
||||
uint8_t polling_rate_mode; // 0: 250Hz, 1: 500Hz, 2: real-time
|
||||
uint8_t audio_buffer_length; // [16,128]
|
||||
uint8_t controller_mode; // 0: DS5, 1: DSE, 2: Auto
|
||||
uint8_t current_slot; // [0..3] active multi-pairing slot (OLED Edition Phase G)
|
||||
// Audio Auto Haptics — derive haptic feedback from speaker audio for games that
|
||||
// send no per-frame native haptic data. DSP borrowed from loteran/DS5Dongle 5d6bc2f.
|
||||
uint8_t auto_haptics_enable; // 0=Off, 1=Fallback (default), 2=Mix, 3=Replace
|
||||
uint8_t auto_haptics_gain; // [0,200] percent, default 100
|
||||
uint8_t auto_haptics_lowpass; // 0=80Hz, 1=160Hz (default), 2=250Hz, 3=400Hz
|
||||
};
|
||||
|
||||
struct __attribute__((packed)) Config {
|
||||
|
||||
+1219
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
#ifndef DS5_BRIDGE_OLED_H
|
||||
#define DS5_BRIDGE_OLED_H
|
||||
|
||||
void oled_init();
|
||||
void oled_loop();
|
||||
|
||||
#endif // DS5_BRIDGE_OLED_H
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
// 5x7 ASCII font, ASCII 0x20-0x7E. 5 column bytes per glyph, LSB = top row.
|
||||
// Source: public-domain font from the Adafruit / SSD1306 ecosystem.
|
||||
#ifndef DS5_BRIDGE_OLED_FONT_H
|
||||
#define DS5_BRIDGE_OLED_FONT_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
static constexpr int kFontW = 5;
|
||||
static constexpr int kFontH = 7;
|
||||
|
||||
static const uint8_t kFont5x7[95][5] = {
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00}, // ' '
|
||||
{0x00, 0x00, 0x5F, 0x00, 0x00}, // '!'
|
||||
{0x00, 0x03, 0x00, 0x03, 0x00}, // '"'
|
||||
{0x14, 0x3E, 0x14, 0x3E, 0x14}, // '#'
|
||||
{0x24, 0x2A, 0x7F, 0x2A, 0x12}, // '$'
|
||||
{0x43, 0x33, 0x08, 0x66, 0x61}, // '%'
|
||||
{0x36, 0x49, 0x55, 0x22, 0x50}, // '&'
|
||||
{0x00, 0x05, 0x03, 0x00, 0x00}, // '''
|
||||
{0x00, 0x1C, 0x22, 0x41, 0x00}, // '('
|
||||
{0x00, 0x41, 0x22, 0x1C, 0x00}, // ')'
|
||||
{0x14, 0x08, 0x3E, 0x08, 0x14}, // '*'
|
||||
{0x08, 0x08, 0x3E, 0x08, 0x08}, // '+'
|
||||
{0x00, 0x50, 0x30, 0x00, 0x00}, // ','
|
||||
{0x08, 0x08, 0x08, 0x08, 0x08}, // '-'
|
||||
{0x00, 0x60, 0x60, 0x00, 0x00}, // '.'
|
||||
{0x20, 0x10, 0x08, 0x04, 0x02}, // '/'
|
||||
{0x3E, 0x51, 0x49, 0x45, 0x3E}, // '0'
|
||||
{0x00, 0x04, 0x02, 0x7F, 0x00}, // '1'
|
||||
{0x42, 0x61, 0x51, 0x49, 0x46}, // '2'
|
||||
{0x22, 0x41, 0x49, 0x49, 0x36}, // '3'
|
||||
{0x18, 0x14, 0x12, 0x7F, 0x10}, // '4'
|
||||
{0x27, 0x45, 0x45, 0x45, 0x39}, // '5'
|
||||
{0x3E, 0x49, 0x49, 0x49, 0x32}, // '6'
|
||||
{0x01, 0x01, 0x71, 0x09, 0x07}, // '7'
|
||||
{0x36, 0x49, 0x49, 0x49, 0x36}, // '8'
|
||||
{0x26, 0x49, 0x49, 0x49, 0x3E}, // '9'
|
||||
{0x00, 0x36, 0x36, 0x00, 0x00}, // ':'
|
||||
{0x00, 0x56, 0x36, 0x00, 0x00}, // ';'
|
||||
{0x08, 0x14, 0x22, 0x41, 0x00}, // '<'
|
||||
{0x14, 0x14, 0x14, 0x14, 0x14}, // '='
|
||||
{0x00, 0x41, 0x22, 0x14, 0x08}, // '>'
|
||||
{0x02, 0x01, 0x51, 0x09, 0x06}, // '?'
|
||||
{0x3E, 0x41, 0x59, 0x55, 0x5E}, // '@'
|
||||
{0x7E, 0x09, 0x09, 0x09, 0x7E}, // 'A'
|
||||
{0x7F, 0x49, 0x49, 0x49, 0x36}, // 'B'
|
||||
{0x3E, 0x41, 0x41, 0x41, 0x22}, // 'C'
|
||||
{0x7F, 0x41, 0x41, 0x41, 0x3E}, // 'D'
|
||||
{0x7F, 0x49, 0x49, 0x49, 0x41}, // 'E'
|
||||
{0x7F, 0x09, 0x09, 0x09, 0x01}, // 'F'
|
||||
{0x3E, 0x41, 0x41, 0x49, 0x3A}, // 'G'
|
||||
{0x7F, 0x08, 0x08, 0x08, 0x7F}, // 'H'
|
||||
{0x00, 0x41, 0x7F, 0x41, 0x00}, // 'I'
|
||||
{0x30, 0x40, 0x40, 0x40, 0x3F}, // 'J'
|
||||
{0x7F, 0x08, 0x14, 0x22, 0x41}, // 'K'
|
||||
{0x7F, 0x40, 0x40, 0x40, 0x40}, // 'L'
|
||||
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, // 'M'
|
||||
{0x7F, 0x02, 0x04, 0x08, 0x7F}, // 'N'
|
||||
{0x3E, 0x41, 0x41, 0x41, 0x3E}, // 'O'
|
||||
{0x7F, 0x09, 0x09, 0x09, 0x06}, // 'P'
|
||||
{0x1E, 0x21, 0x21, 0x21, 0x5E}, // 'Q'
|
||||
{0x7F, 0x09, 0x09, 0x09, 0x76}, // 'R'
|
||||
{0x26, 0x49, 0x49, 0x49, 0x32}, // 'S'
|
||||
{0x01, 0x01, 0x7F, 0x01, 0x01}, // 'T'
|
||||
{0x3F, 0x40, 0x40, 0x40, 0x3F}, // 'U'
|
||||
{0x1F, 0x20, 0x40, 0x20, 0x1F}, // 'V'
|
||||
{0x7F, 0x20, 0x10, 0x20, 0x7F}, // 'W'
|
||||
{0x41, 0x22, 0x1C, 0x22, 0x41}, // 'X'
|
||||
{0x07, 0x08, 0x70, 0x08, 0x07}, // 'Y'
|
||||
{0x61, 0x51, 0x49, 0x45, 0x43}, // 'Z'
|
||||
{0x00, 0x7F, 0x41, 0x00, 0x00}, // '['
|
||||
{0x02, 0x04, 0x08, 0x10, 0x20}, // '\\'
|
||||
{0x00, 0x00, 0x41, 0x7F, 0x00}, // ']'
|
||||
{0x04, 0x02, 0x01, 0x02, 0x04}, // '^'
|
||||
{0x40, 0x40, 0x40, 0x40, 0x40}, // '_'
|
||||
{0x00, 0x01, 0x02, 0x04, 0x00}, // '`'
|
||||
{0x20, 0x54, 0x54, 0x54, 0x78}, // 'a'
|
||||
{0x7F, 0x44, 0x44, 0x44, 0x38}, // 'b'
|
||||
{0x38, 0x44, 0x44, 0x44, 0x44}, // 'c'
|
||||
{0x38, 0x44, 0x44, 0x44, 0x7F}, // 'd'
|
||||
{0x38, 0x54, 0x54, 0x54, 0x18}, // 'e'
|
||||
{0x04, 0x04, 0x7E, 0x05, 0x05}, // 'f'
|
||||
{0x08, 0x54, 0x54, 0x54, 0x3C}, // 'g'
|
||||
{0x7F, 0x08, 0x04, 0x04, 0x78}, // 'h'
|
||||
{0x00, 0x44, 0x7D, 0x40, 0x00}, // 'i'
|
||||
{0x20, 0x40, 0x44, 0x3D, 0x00}, // 'j'
|
||||
{0x7F, 0x10, 0x28, 0x44, 0x00}, // 'k'
|
||||
{0x00, 0x41, 0x7F, 0x40, 0x00}, // 'l'
|
||||
{0x7C, 0x04, 0x78, 0x04, 0x78}, // 'm'
|
||||
{0x7C, 0x08, 0x04, 0x04, 0x78}, // 'n'
|
||||
{0x38, 0x44, 0x44, 0x44, 0x38}, // 'o'
|
||||
{0x7C, 0x14, 0x14, 0x14, 0x08}, // 'p'
|
||||
{0x08, 0x14, 0x14, 0x14, 0x7C}, // 'q'
|
||||
{0x00, 0x7C, 0x08, 0x04, 0x04}, // 'r'
|
||||
{0x48, 0x54, 0x54, 0x54, 0x20}, // 's'
|
||||
{0x04, 0x04, 0x3F, 0x44, 0x44}, // 't'
|
||||
{0x3C, 0x40, 0x40, 0x20, 0x7C}, // 'u'
|
||||
{0x1C, 0x20, 0x40, 0x20, 0x1C}, // 'v'
|
||||
{0x3C, 0x40, 0x30, 0x40, 0x3C}, // 'w'
|
||||
{0x44, 0x28, 0x10, 0x28, 0x44}, // 'x'
|
||||
{0x0C, 0x50, 0x50, 0x50, 0x3C}, // 'y'
|
||||
{0x44, 0x64, 0x54, 0x4C, 0x44}, // 'z'
|
||||
{0x00, 0x08, 0x36, 0x41, 0x41}, // '{'
|
||||
{0x00, 0x00, 0x7F, 0x00, 0x00}, // '|'
|
||||
{0x41, 0x41, 0x36, 0x08, 0x00}, // '}'
|
||||
{0x02, 0x01, 0x02, 0x04, 0x02}, // '~'
|
||||
};
|
||||
|
||||
#endif // DS5_BRIDGE_OLED_FONT_H
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// Flash-backed slot table for 4-slot persistent BT pairing.
|
||||
// Modeled on zurce/DS5Dongle-OLED (bt.cpp:29-115). Credit to zurce.
|
||||
|
||||
#include "slots.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
|
||||
#include "hardware/flash.h"
|
||||
#include "hardware/sync.h"
|
||||
|
||||
constexpr uint32_t SLOTS_MAGIC = 0x44533502u; // "DS5\x02"
|
||||
constexpr uint32_t SLOTS_FLASH_OFFSET = PICO_FLASH_SIZE_BYTES - 2u * FLASH_SECTOR_SIZE;
|
||||
|
||||
struct __attribute__((packed)) SlotsData {
|
||||
uint32_t magic;
|
||||
uint8_t addrs[kNumSlots][6];
|
||||
uint8_t occupied[kNumSlots];
|
||||
};
|
||||
|
||||
static_assert(sizeof(SlotsData) <= FLASH_PAGE_SIZE);
|
||||
static_assert(SLOTS_FLASH_OFFSET % FLASH_SECTOR_SIZE == 0);
|
||||
|
||||
static SlotsData g_slots{};
|
||||
|
||||
static const SlotsData *flash_slots() {
|
||||
return reinterpret_cast<const SlotsData *>(XIP_BASE + SLOTS_FLASH_OFFSET);
|
||||
}
|
||||
|
||||
static bool save_slots_to_flash() {
|
||||
alignas(4) uint8_t page[FLASH_PAGE_SIZE];
|
||||
memset(page, 0xff, sizeof(page));
|
||||
memcpy(page, &g_slots, sizeof(g_slots));
|
||||
|
||||
const uint32_t interrupts = save_and_disable_interrupts();
|
||||
flash_range_erase(SLOTS_FLASH_OFFSET, FLASH_SECTOR_SIZE);
|
||||
flash_range_program(SLOTS_FLASH_OFFSET, page, sizeof(page));
|
||||
restore_interrupts(interrupts);
|
||||
|
||||
SlotsData verify{};
|
||||
memcpy(&verify, flash_slots(), sizeof(verify));
|
||||
if (memcmp(&verify, &g_slots, sizeof(g_slots)) == 0) {
|
||||
printf("[Slots] flash write verified\n");
|
||||
return true;
|
||||
}
|
||||
printf("[Slots] flash write VERIFY FAILED\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
void slots_load() {
|
||||
memcpy(&g_slots, flash_slots(), sizeof(g_slots));
|
||||
if (g_slots.magic != SLOTS_MAGIC) {
|
||||
printf("[Slots] flash sector empty/invalid, initializing\n");
|
||||
memset(&g_slots, 0, sizeof(g_slots));
|
||||
g_slots.magic = SLOTS_MAGIC;
|
||||
save_slots_to_flash();
|
||||
}
|
||||
for (int i = 0; i < kNumSlots; i++) {
|
||||
if (g_slots.occupied[i]) {
|
||||
printf("[Slots] %d: %02X:%02X:%02X:%02X:%02X:%02X\n", i,
|
||||
g_slots.addrs[i][0], g_slots.addrs[i][1], g_slots.addrs[i][2],
|
||||
g_slots.addrs[i][3], g_slots.addrs[i][4], g_slots.addrs[i][5]);
|
||||
} else {
|
||||
printf("[Slots] %d: (empty)\n", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool slot_occupied(int slot) {
|
||||
if (slot < 0 || slot >= kNumSlots) return false;
|
||||
return g_slots.occupied[slot] != 0;
|
||||
}
|
||||
|
||||
void slot_get_addr(int slot, uint8_t out[6]) {
|
||||
if (slot < 0 || slot >= kNumSlots) {
|
||||
memset(out, 0, 6);
|
||||
return;
|
||||
}
|
||||
memcpy(out, g_slots.addrs[slot], 6);
|
||||
}
|
||||
|
||||
int slot_owner_of(const uint8_t addr[6]) {
|
||||
for (int i = 0; i < kNumSlots; i++) {
|
||||
if (g_slots.occupied[i] && memcmp(g_slots.addrs[i], addr, 6) == 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void slot_assign(int slot, const uint8_t addr[6]) {
|
||||
if (slot < 0 || slot >= kNumSlots) return;
|
||||
memcpy(g_slots.addrs[slot], addr, 6);
|
||||
g_slots.occupied[slot] = 1;
|
||||
save_slots_to_flash();
|
||||
}
|
||||
|
||||
void slot_forget(int slot) {
|
||||
if (slot < 0 || slot >= kNumSlots) return;
|
||||
memset(g_slots.addrs[slot], 0, 6);
|
||||
g_slots.occupied[slot] = 0;
|
||||
save_slots_to_flash();
|
||||
}
|
||||
|
||||
void slots_wipe_all() {
|
||||
for (int i = 0; i < kNumSlots; i++) {
|
||||
memset(g_slots.addrs[i], 0, 6);
|
||||
g_slots.occupied[i] = 0;
|
||||
}
|
||||
save_slots_to_flash();
|
||||
}
|
||||
|
||||
bool slots_any_empty() {
|
||||
for (int i = 0; i < kNumSlots; i++) {
|
||||
if (!g_slots.occupied[i]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// Persistent 4-slot multi-controller pairing storage. Stores the 4 bonded
|
||||
// DS5 bd_addrs in a custom flash sector (BTstack's NVM keeps the link_keys).
|
||||
//
|
||||
// Multi-slot pairing modeled on zurce/DS5Dongle-OLED. Credit to zurce.
|
||||
|
||||
#ifndef DS5_BRIDGE_SLOTS_H
|
||||
#define DS5_BRIDGE_SLOTS_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
constexpr int kNumSlots = 4;
|
||||
|
||||
void slots_load();
|
||||
bool slot_occupied(int slot);
|
||||
void slot_get_addr(int slot, uint8_t out[6]);
|
||||
int slot_owner_of(const uint8_t addr[6]);
|
||||
void slot_assign(int slot, const uint8_t addr[6]);
|
||||
void slot_forget(int slot);
|
||||
void slots_wipe_all();
|
||||
bool slots_any_empty();
|
||||
|
||||
#endif // DS5_BRIDGE_SLOTS_H
|
||||
+6
-5
@@ -67,11 +67,12 @@ static bool audio10_set_req_entity(tusb_control_request_t const *p_request, uint
|
||||
TU_VERIFY(p_request->wLength == 2);
|
||||
|
||||
volume[index] = static_cast<float>(*reinterpret_cast<int16_t const *>(pBuff)) / 256;
|
||||
if (entityID == UAC1_ENTITY_SPK_FEATURE_UNIT) {
|
||||
auto config = get_config();
|
||||
config.speaker_volume = volume[index];
|
||||
set_config(config);
|
||||
}
|
||||
// Do NOT sync the USB-side UAC1 volume control into our
|
||||
// flash-persisted config. The host (PipeWire/Pulse) re-applies
|
||||
// its last-known UAC1 volume on every device reconnect, which
|
||||
// would silently override the user's saved speaker_volume.
|
||||
// Fix borrowed from loteran/DS5Dongle commit 03fa1e4.
|
||||
(void)entityID;
|
||||
|
||||
TU_LOG2(" Set Volume: %d dB of entity: %u\r\n", volume[index], entityID);
|
||||
return true;
|
||||
|
||||
@@ -102,7 +102,15 @@ tusb_desc_device_t desc_device =
|
||||
|
||||
.iManufacturer = 0x01,
|
||||
.iProduct = 0x02,
|
||||
.iSerialNumber = 0x00,
|
||||
// OLED Edition restores the per-board serial number that upstream stripped
|
||||
// in commit e79c762 ("remove usb serialnumber #32"). Without it, Windows
|
||||
// treats the dongle as a NEW device for each USB port (issue awalol#100):
|
||||
// users lose their per-device volume / app preferences when they move
|
||||
// ports. The trade-off (re-introducing SpecialK incompatibility from #32)
|
||||
// affects only Windows users with that specific tool; the broader Windows
|
||||
// population gets stable device identity back. The serial string itself
|
||||
// is produced by board_usb_get_serial() from the flash chip's unique ID.
|
||||
.iSerialNumber = STRID_SERIAL,
|
||||
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user