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:
MarcelineVPQ
2026-05-17 07:53:41 -06:00
co-authored by Claude Opus 4.7
parent 0ed05d3f56
commit 4b6a72b70a
22 changed files with 2087 additions and 17 deletions
+27 -2
View File
@@ -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];
+6
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+9 -1
View File
@@ -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
};