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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
da63e2bb72
commit
1b1944c006
@@ -8,6 +8,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Button remapping.** Any of the 16 digital controls (face buttons, D-pad, shoulders/triggers, stick clicks, Create/Options) can be remapped to any other — stored on the dongle, applied transparently before the host sees the report, so it works on every OS and every game with no host-side software. The remap table lives in its own dedicated flash sector (`PICO_FLASH_SIZE_BYTES - 3·FLASH_SECTOR_SIZE`, magic `DS5\x03`, below the slots sector) and survives reboot; identity (no remap) is the default. Multiple sources mapping to one target OR together (analog L2/R2 take the max); a source can be set to *disabled* (`0xFF`). The remap acts on the **outgoing host report copy only** — the raw input the OLED screens and the PS+Mute reboot combo read is untouched. Edited over the existing `0xF6`/`0xF7` vendor reports with a hardened `RM`+version frame (**no HID-descriptor change**, so Windows enumeration is unaffected) and a revision counter the host polls to confirm a write landed. New `src/remap.{h,cpp}`; apply logic + button set ported from [SundayMoments/DS5_Bridge](https://github.com/SundayMoments/DS5_Bridge) (credit). Dev helper `scripts/remap_test.py` exercises the path over `/dev/hidraw` without the web tool.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- **BT microphone now has packet-loss concealment (PLC).** The mic decode path gained a small decoded-frame jitter buffer (8 frames) drained at a steady 10 ms playout cadence: bursty BT delivery is smoothed, and a dropped mic frame during an active session is concealed with an Opus PLC frame (`opus_decode(decoder, NULL, 0, …)`) instead of leaving a hole the host hears as a click/dropout. Playout pre-buffers 3 frames and stops after 300 ms of no real frames (so it never emits comfort noise when the mic is idle). A new **`Mic PLC:`** counter on the Diagnostics screen climbs only when concealment fires — effectively a live BT link-quality gauge. Verified: forced BT loss kept the captured audio gap-free (longest zero-run ~0 ms) while the counter climbed. Design ported from [SundayMoments/DS5_Bridge](https://github.com/SundayMoments/DS5_Bridge) (credit). Adds ~30 ms mic latency (the pre-buffer).
|
- **BT microphone now has packet-loss concealment (PLC).** The mic decode path gained a small decoded-frame jitter buffer (8 frames) drained at a steady 10 ms playout cadence: bursty BT delivery is smoothed, and a dropped mic frame during an active session is concealed with an Opus PLC frame (`opus_decode(decoder, NULL, 0, …)`) instead of leaving a hole the host hears as a click/dropout. Playout pre-buffers 3 frames and stops after 300 ms of no real frames (so it never emits comfort noise when the mic is idle). A new **`Mic PLC:`** counter on the Diagnostics screen climbs only when concealment fires — effectively a live BT link-quality gauge. Verified: forced BT loss kept the captured audio gap-free (longest zero-run ~0 ms) while the counter climbed. Design ported from [SundayMoments/DS5_Bridge](https://github.com/SundayMoments/DS5_Bridge) (credit). Adds ~30 ms mic latency (the pre-buffer).
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ add_executable(ds5-bridge
|
|||||||
src/state_mgr.cpp
|
src/state_mgr.cpp
|
||||||
src/oled.cpp
|
src/oled.cpp
|
||||||
src/slots.cpp
|
src/slots.cpp
|
||||||
|
src/remap.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
if (ENABLE_BATT_LED)
|
if (ENABLE_BATT_LED)
|
||||||
|
|||||||
Executable
+139
@@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Host-side dev helper to exercise the firmware button-remap path over the
|
||||||
|
already-declared 0xF6 (SET) / 0xF7 (GET) vendor feature reports — no web tool
|
||||||
|
needed. Linux only (reads /dev/hidraw directly), same role as mic_diag.sh.
|
||||||
|
|
||||||
|
The remap rides 0xF6/0xF7 with a magic+version frame (see src/cmd.cpp):
|
||||||
|
SET 0xF6: [func=0x10]['R']['M'][ver][table[16]]
|
||||||
|
GET 0xF7: <Config_body(35)> ['R']['M'][ver][rev_lo][rev_hi][table[16]]
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
remap_test.py get # show revision + current table
|
||||||
|
remap_test.py swap CIRCLE CROSS # swap two buttons (and persist)
|
||||||
|
remap_test.py set SRC TGT # route SRC -> TGT (one-way)
|
||||||
|
remap_test.py off SRC # disable SRC (0xFF)
|
||||||
|
remap_test.py reset # identity (no remap)
|
||||||
|
|
||||||
|
Run with sudo if /dev/hidraw needs root.
|
||||||
|
"""
|
||||||
|
import fcntl, glob, struct, sys
|
||||||
|
|
||||||
|
VID, PID = 0x054c, 0x0ce6
|
||||||
|
CONFIG_LEN = 35 # sizeof(Config_body), see src/config.h
|
||||||
|
PROTO_VER = 1 # kRemapProtoVer
|
||||||
|
COUNT = 16 # kRemapCount
|
||||||
|
|
||||||
|
# Must match RemapButton order in src/remap.cpp.
|
||||||
|
NAMES = ["L2", "L1", "CREATE", "DPAD_UP", "DPAD_LEFT", "DPAD_DOWN",
|
||||||
|
"DPAD_RIGHT", "L3", "R2", "R1", "OPTIONS", "TRIANGLE",
|
||||||
|
"CIRCLE", "CROSS", "SQUARE", "R3"]
|
||||||
|
IDX = {n: i for i, n in enumerate(NAMES)}
|
||||||
|
|
||||||
|
|
||||||
|
def hidiocg(size): # HIDIOCGFEATURE(size)
|
||||||
|
return (3 << 30) | (size << 16) | (ord('H') << 8) | 0x07
|
||||||
|
|
||||||
|
|
||||||
|
def hidiocs(size): # HIDIOCSFEATURE(size)
|
||||||
|
return (3 << 30) | (size << 16) | (ord('H') << 8) | 0x06
|
||||||
|
|
||||||
|
|
||||||
|
def read_f7(f):
|
||||||
|
"""Return (rev, table[16]) or None if the response lacks the remap block."""
|
||||||
|
size = 64 # 1 report-id byte + up to 63 payload
|
||||||
|
buf = bytearray(size)
|
||||||
|
buf[0] = 0xF7
|
||||||
|
fcntl.ioctl(f, hidiocg(size), buf)
|
||||||
|
payload = bytes(buf[1:]) # kernel prepends report id at byte 0
|
||||||
|
blk = payload[CONFIG_LEN:CONFIG_LEN + 5 + COUNT]
|
||||||
|
if len(blk) < 5 + COUNT or blk[0] != ord('R') or blk[1] != ord('M'):
|
||||||
|
return None
|
||||||
|
ver = blk[2]
|
||||||
|
rev = blk[3] | (blk[4] << 8)
|
||||||
|
table = list(blk[5:5 + COUNT])
|
||||||
|
return ver, rev, table
|
||||||
|
|
||||||
|
|
||||||
|
def find_dongle():
|
||||||
|
for path in sorted(glob.glob('/dev/hidraw*')):
|
||||||
|
try:
|
||||||
|
f = open(path, 'rb+', buffering=0)
|
||||||
|
except (OSError, PermissionError):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if read_f7(f) is not None:
|
||||||
|
return f, path
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
f.close()
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def write_table(f, table):
|
||||||
|
assert len(table) == COUNT
|
||||||
|
payload = bytes([0xF6, 0x10, ord('R'), ord('M'), PROTO_VER]) + bytes(table)
|
||||||
|
buf = bytearray(payload)
|
||||||
|
fcntl.ioctl(f, hidiocs(len(buf)), buf)
|
||||||
|
|
||||||
|
|
||||||
|
def show(label, ver, rev, table):
|
||||||
|
print(f"{label}: proto v{ver}, revision {rev}")
|
||||||
|
active = [(s, t) for s, t in enumerate(table) if t != s]
|
||||||
|
if not active:
|
||||||
|
print(" identity (no remap active)")
|
||||||
|
return
|
||||||
|
for s, t in active:
|
||||||
|
tn = "DISABLED" if t == 0xFF else NAMES[t]
|
||||||
|
print(f" {NAMES[s]:<10} -> {tn}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_button(arg):
|
||||||
|
key = arg.upper()
|
||||||
|
if key not in IDX:
|
||||||
|
sys.exit(f"unknown button '{arg}'. choices: {', '.join(NAMES)}")
|
||||||
|
return IDX[key]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(__doc__)
|
||||||
|
sys.exit(2)
|
||||||
|
cmd = sys.argv[1].lower()
|
||||||
|
|
||||||
|
f, path = find_dongle()
|
||||||
|
if f is None:
|
||||||
|
sys.exit("no DS5 dongle with remap support found "
|
||||||
|
"(check /dev/hidraw permissions and firmware version)")
|
||||||
|
print(f"dongle: {path}")
|
||||||
|
ver, rev, table = read_f7(f)
|
||||||
|
|
||||||
|
if cmd == "get":
|
||||||
|
show("current", ver, rev, table)
|
||||||
|
return
|
||||||
|
|
||||||
|
if cmd == "reset":
|
||||||
|
table = list(range(COUNT))
|
||||||
|
elif cmd == "off" and len(sys.argv) == 3:
|
||||||
|
table[parse_button(sys.argv[2])] = 0xFF
|
||||||
|
elif cmd == "set" and len(sys.argv) == 4:
|
||||||
|
table[parse_button(sys.argv[2])] = parse_button(sys.argv[3])
|
||||||
|
elif cmd == "swap" and len(sys.argv) == 4:
|
||||||
|
a, b = parse_button(sys.argv[2]), parse_button(sys.argv[3])
|
||||||
|
table[a], table[b] = b, a
|
||||||
|
else:
|
||||||
|
print(__doc__)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
show("writing", ver, rev, table)
|
||||||
|
write_table(f, table)
|
||||||
|
nver, nrev, ntable = read_f7(f)
|
||||||
|
show("read-back", nver, nrev, ntable)
|
||||||
|
if ntable == table and nrev != rev:
|
||||||
|
print("OK: table applied and revision bumped")
|
||||||
|
else:
|
||||||
|
print("WARNING: read-back mismatch or revision did not change")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+48
-3
@@ -14,6 +14,7 @@
|
|||||||
#include "device/usbd.h"
|
#include "device/usbd.h"
|
||||||
#include "pico/time.h"
|
#include "pico/time.h"
|
||||||
#include "slots.h"
|
#include "slots.h"
|
||||||
|
#include "remap.h"
|
||||||
#include "hardware/clocks.h"
|
#include "hardware/clocks.h"
|
||||||
#include "hardware/adc.h"
|
#include "hardware/adc.h"
|
||||||
#include "hardware/vreg.h"
|
#include "hardware/vreg.h"
|
||||||
@@ -86,11 +87,34 @@ bool is_pico_cmd(uint8_t report_id) {
|
|||||||
uint16_t pico_cmd_get(uint8_t report_id, uint8_t *buffer, uint16_t reqlen) {
|
uint16_t pico_cmd_get(uint8_t report_id, uint8_t *buffer, uint16_t reqlen) {
|
||||||
if (report_id == 0xf7) {
|
if (report_id == 0xf7) {
|
||||||
printf("[HID] Receive 0xf7 getting config\n");
|
printf("[HID] Receive 0xf7 getting config\n");
|
||||||
if (sizeof(Config_body) > reqlen) {
|
const size_t cfg_len = sizeof(Config_body);
|
||||||
|
if (cfg_len > reqlen) {
|
||||||
printf("[Config] Warning: Config_body overflow\n");
|
printf("[Config] Warning: Config_body overflow\n");
|
||||||
}
|
}
|
||||||
const auto len = std::min(sizeof(Config_body),static_cast<size_t>(reqlen));
|
const auto len = std::min(cfg_len, static_cast<size_t>(reqlen));
|
||||||
memcpy(buffer,&get_config(),len);
|
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;
|
return len;
|
||||||
}
|
}
|
||||||
if (report_id == 0xf8) {
|
if (report_id == 0xf8) {
|
||||||
@@ -268,4 +292,25 @@ void pico_cmd_set(uint8_t report_id, uint8_t const *buffer, uint16_t bufsize) {
|
|||||||
sleep_ms(150);
|
sleep_ms(150);
|
||||||
tud_connect();
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -22,6 +22,7 @@
|
|||||||
#include "battery_led.h"
|
#include "battery_led.h"
|
||||||
#endif
|
#endif
|
||||||
#include "oled.h"
|
#include "oled.h"
|
||||||
|
#include "remap.h"
|
||||||
|
|
||||||
// Pico SDK speciifically for waiting on conditions
|
// Pico SDK speciifically for waiting on conditions
|
||||||
#include "pico/critical_section.h"
|
#include "pico/critical_section.h"
|
||||||
@@ -118,7 +119,12 @@ void interrupt_loop() {
|
|||||||
|
|
||||||
// TODO: Refactor for better code reuse
|
// TODO: Refactor for better code reuse
|
||||||
if (get_config().polling_rate_mode != 2) {
|
if (get_config().polling_rate_mode != 2) {
|
||||||
if (!tud_hid_report(0x01, interrupt_in_data, 63)) {
|
// Remap acts on the OUTGOING copy only — interrupt_in_data stays raw so
|
||||||
|
// the reboot combo above and every OLED screen keep seeing physical input.
|
||||||
|
uint8_t out[63];
|
||||||
|
memcpy(out, interrupt_in_data, 63);
|
||||||
|
remap_apply(out);
|
||||||
|
if (!tud_hid_report(0x01, out, 63)) {
|
||||||
printf("[USBHID] tud_hid_report error\n");
|
printf("[USBHID] tud_hid_report error\n");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -137,6 +143,9 @@ void interrupt_loop() {
|
|||||||
}
|
}
|
||||||
critical_section_exit(&report_cs);
|
critical_section_exit(&report_cs);
|
||||||
|
|
||||||
|
// Remap the snapshot, not interrupt_in_data (outgoing copy only — see above).
|
||||||
|
if (should_send) remap_apply(safe_report);
|
||||||
|
|
||||||
// Only send to TinyUSB if we actually grabbed fresh data
|
// Only send to TinyUSB if we actually grabbed fresh data
|
||||||
if (should_send) {
|
if (should_send) {
|
||||||
if (!tud_hid_report(0x01, safe_report, 63)) {
|
if (!tud_hid_report(0x01, safe_report, 63)) {
|
||||||
@@ -377,6 +386,7 @@ int main() {
|
|||||||
critical_section_init(&report_cs);
|
critical_section_init(&report_cs);
|
||||||
|
|
||||||
config_load();
|
config_load();
|
||||||
|
remap_load();
|
||||||
|
|
||||||
bt_init();
|
bt_init();
|
||||||
bt_register_data_callback(on_bt_data);
|
bt_register_data_callback(on_bt_data);
|
||||||
|
|||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
// Button remapping. See remap.h. Flash-sector pattern mirrors slots.cpp;
|
||||||
|
// the apply logic + button set are ported from SundayMoments/DS5_Bridge.
|
||||||
|
|
||||||
|
#include "remap.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "hardware/flash.h"
|
||||||
|
#include "hardware/sync.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Source/target button indices. Order is arbitrary but MUST match the web
|
||||||
|
// editor's expectation (DS5Dongle-OLED-Config-Web src/protocol/remap.ts).
|
||||||
|
enum RemapButton : uint8_t {
|
||||||
|
RemapL2, RemapL1, RemapCreate, RemapDpadUp, RemapDpadLeft, RemapDpadDown,
|
||||||
|
RemapDpadRight, RemapL3, RemapR2, RemapR1, RemapOptions, RemapTriangle,
|
||||||
|
RemapCircle, RemapCross, RemapSquare, RemapR3, RemapButtonCount,
|
||||||
|
};
|
||||||
|
static_assert(RemapButtonCount == kRemapCount, "kRemapCount must match RemapButton");
|
||||||
|
|
||||||
|
// interrupt_in_data[8]: shoulders / sticks / system bits.
|
||||||
|
constexpr uint8_t kL1Bit = 0x01, kR1Bit = 0x02, kL2Bit = 0x04, kR2Bit = 0x08,
|
||||||
|
kCreateBit = 0x10, kOptionsBit = 0x20, kL3Bit = 0x40, kR3Bit = 0x80;
|
||||||
|
// interrupt_in_data[7]: D-pad hat (low nibble) + face buttons (high nibble).
|
||||||
|
constexpr uint8_t kSquareBit = 0x10, kCrossBit = 0x20, kCircleBit = 0x40,
|
||||||
|
kTriangleBit = 0x80, kDpadMask = 0x0F;
|
||||||
|
// D-pad hat values.
|
||||||
|
constexpr uint8_t kUp = 0, kUpRight = 1, kRight = 2, kDownRight = 3, kDown = 4,
|
||||||
|
kDownLeft = 5, kLeft = 6, kUpLeft = 7, kNeutral = 8;
|
||||||
|
|
||||||
|
constexpr uint32_t REMAP_MAGIC = 0x44533503u; // "DS5\x03"
|
||||||
|
constexpr uint32_t REMAP_FLASH_OFFSET = PICO_FLASH_SIZE_BYTES - 3u * FLASH_SECTOR_SIZE;
|
||||||
|
|
||||||
|
struct __attribute__((packed)) RemapData {
|
||||||
|
uint32_t magic;
|
||||||
|
uint8_t table[kRemapCount];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(RemapData) <= FLASH_PAGE_SIZE);
|
||||||
|
static_assert(REMAP_FLASH_OFFSET % FLASH_SECTOR_SIZE == 0);
|
||||||
|
|
||||||
|
RemapData g_remap{};
|
||||||
|
bool g_active = false; // false = identity → remap_apply() fast-returns
|
||||||
|
uint16_t g_revision = 0;
|
||||||
|
|
||||||
|
const RemapData *flash_remap() {
|
||||||
|
return reinterpret_cast<const RemapData *>(XIP_BASE + REMAP_FLASH_OFFSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_identity() {
|
||||||
|
for (int i = 0; i < kRemapCount; i++) g_remap.table[i] = (uint8_t) i;
|
||||||
|
}
|
||||||
|
|
||||||
|
void recompute_active() {
|
||||||
|
g_active = false;
|
||||||
|
for (int i = 0; i < kRemapCount; i++) {
|
||||||
|
if (g_remap.table[i] != i) { g_active = true; return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool valid_table(const uint8_t *t) {
|
||||||
|
for (int i = 0; i < kRemapCount; i++) {
|
||||||
|
if (t[i] >= kRemapCount && t[i] != 0xFF) return false; // <16 or disabled
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool save_to_flash() {
|
||||||
|
alignas(4) uint8_t page[FLASH_PAGE_SIZE];
|
||||||
|
memset(page, 0xff, sizeof(page));
|
||||||
|
memcpy(page, &g_remap, sizeof(g_remap));
|
||||||
|
|
||||||
|
const uint32_t interrupts = save_and_disable_interrupts();
|
||||||
|
flash_range_erase(REMAP_FLASH_OFFSET, FLASH_SECTOR_SIZE);
|
||||||
|
flash_range_program(REMAP_FLASH_OFFSET, page, sizeof(page));
|
||||||
|
restore_interrupts(interrupts);
|
||||||
|
|
||||||
|
RemapData verify{};
|
||||||
|
memcpy(&verify, flash_remap(), sizeof(verify));
|
||||||
|
if (memcmp(&verify, &g_remap, sizeof(g_remap)) == 0) {
|
||||||
|
printf("[Remap] flash write verified\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
printf("[Remap] flash write VERIFY FAILED\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool dpad_has(uint8_t dir, RemapButton b) {
|
||||||
|
switch (b) {
|
||||||
|
case RemapDpadUp: return dir == kUp || dir == kUpRight || dir == kUpLeft;
|
||||||
|
case RemapDpadRight: return dir == kRight || dir == kUpRight || dir == kDownRight;
|
||||||
|
case RemapDpadDown: return dir == kDown || dir == kDownRight || dir == kDownLeft;
|
||||||
|
case RemapDpadLeft: return dir == kLeft || dir == kUpLeft || dir == kDownLeft;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t dpad_from(bool up, bool right, bool down, bool left) {
|
||||||
|
if (up && right && !down && !left) return kUpRight;
|
||||||
|
if (right && down && !up && !left) return kDownRight;
|
||||||
|
if (down && left && !up && !right) return kDownLeft;
|
||||||
|
if (left && up && !right && !down) return kUpLeft;
|
||||||
|
if (up && !down) return kUp;
|
||||||
|
if (right && !left) return kRight;
|
||||||
|
if (down && !up) return kDown;
|
||||||
|
if (left && !right) return kLeft;
|
||||||
|
return kNeutral;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void remap_load() {
|
||||||
|
memcpy(&g_remap, flash_remap(), sizeof(g_remap));
|
||||||
|
if (g_remap.magic != REMAP_MAGIC || !valid_table(g_remap.table)) {
|
||||||
|
printf("[Remap] flash sector empty/invalid, defaulting to identity\n");
|
||||||
|
g_remap.magic = REMAP_MAGIC;
|
||||||
|
set_identity();
|
||||||
|
save_to_flash();
|
||||||
|
}
|
||||||
|
recompute_active();
|
||||||
|
printf("[Remap] loaded (active=%d)\n", g_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
void remap_get(uint8_t out[kRemapCount]) { memcpy(out, g_remap.table, kRemapCount); }
|
||||||
|
|
||||||
|
uint16_t remap_revision() { return g_revision; }
|
||||||
|
|
||||||
|
bool remap_set(const uint8_t *table) {
|
||||||
|
if (!valid_table(table)) return false;
|
||||||
|
memcpy(g_remap.table, table, kRemapCount);
|
||||||
|
g_remap.magic = REMAP_MAGIC;
|
||||||
|
recompute_active();
|
||||||
|
g_revision++;
|
||||||
|
return save_to_flash();
|
||||||
|
}
|
||||||
|
|
||||||
|
void remap_apply(uint8_t *report) {
|
||||||
|
if (!g_active) return; // identity → no-op (hot-path fast return)
|
||||||
|
|
||||||
|
bool src[kRemapCount]{};
|
||||||
|
uint8_t src_analog[kRemapCount]{};
|
||||||
|
const uint8_t dir = report[7] & kDpadMask;
|
||||||
|
|
||||||
|
src[RemapL2] = (report[8] & kL2Bit) != 0;
|
||||||
|
src[RemapL1] = (report[8] & kL1Bit) != 0;
|
||||||
|
src[RemapCreate] = (report[8] & kCreateBit) != 0;
|
||||||
|
src[RemapDpadUp] = dpad_has(dir, RemapDpadUp);
|
||||||
|
src[RemapDpadLeft] = dpad_has(dir, RemapDpadLeft);
|
||||||
|
src[RemapDpadDown] = dpad_has(dir, RemapDpadDown);
|
||||||
|
src[RemapDpadRight] = dpad_has(dir, RemapDpadRight);
|
||||||
|
src[RemapL3] = (report[8] & kL3Bit) != 0;
|
||||||
|
src[RemapR2] = (report[8] & kR2Bit) != 0;
|
||||||
|
src[RemapR1] = (report[8] & kR1Bit) != 0;
|
||||||
|
src[RemapOptions] = (report[8] & kOptionsBit) != 0;
|
||||||
|
src[RemapTriangle] = (report[7] & kTriangleBit) != 0;
|
||||||
|
src[RemapCircle] = (report[7] & kCircleBit) != 0;
|
||||||
|
src[RemapCross] = (report[7] & kCrossBit) != 0;
|
||||||
|
src[RemapSquare] = (report[7] & kSquareBit) != 0;
|
||||||
|
src[RemapR3] = (report[8] & kR3Bit) != 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < kRemapCount; i++) src_analog[i] = src[i] ? 0xFF : 0;
|
||||||
|
src_analog[RemapL2] = report[4]; // L2 analog
|
||||||
|
src_analog[RemapR2] = report[5]; // R2 analog
|
||||||
|
|
||||||
|
bool tgt[kRemapCount]{};
|
||||||
|
uint8_t tgt_analog[kRemapCount]{};
|
||||||
|
for (int s = 0; s < kRemapCount; s++) {
|
||||||
|
const uint8_t t = g_remap.table[s];
|
||||||
|
if (t >= kRemapCount) continue; // 0xFF = disabled (source produces nothing)
|
||||||
|
if (src[s]) tgt[t] = true;
|
||||||
|
tgt_analog[t] = std::max(tgt_analog[t], src_analog[s]); // OR digital / max analog
|
||||||
|
}
|
||||||
|
|
||||||
|
report[4] = tgt_analog[RemapL2];
|
||||||
|
report[5] = tgt_analog[RemapR2];
|
||||||
|
|
||||||
|
// Byte 7 is entirely D-pad + face (all 8 bits) — rebuild it.
|
||||||
|
report[7] = dpad_from(tgt[RemapDpadUp], tgt[RemapDpadRight],
|
||||||
|
tgt[RemapDpadDown], tgt[RemapDpadLeft]);
|
||||||
|
if (tgt[RemapSquare]) report[7] |= kSquareBit;
|
||||||
|
if (tgt[RemapCross]) report[7] |= kCrossBit;
|
||||||
|
if (tgt[RemapCircle]) report[7] |= kCircleBit;
|
||||||
|
if (tgt[RemapTriangle]) report[7] |= kTriangleBit;
|
||||||
|
|
||||||
|
// Byte 8 is entirely shoulders/sticks/Create/Options (all 8 bits) — rebuild it.
|
||||||
|
report[8] = 0;
|
||||||
|
if (tgt[RemapL1]) report[8] |= kL1Bit;
|
||||||
|
if (tgt[RemapR1]) report[8] |= kR1Bit;
|
||||||
|
if (tgt[RemapL2]) report[8] |= kL2Bit;
|
||||||
|
if (tgt[RemapR2]) report[8] |= kR2Bit;
|
||||||
|
if (tgt[RemapCreate]) report[8] |= kCreateBit;
|
||||||
|
if (tgt[RemapOptions]) report[8] |= kOptionsBit;
|
||||||
|
if (tgt[RemapL3]) report[8] |= kL3Bit;
|
||||||
|
if (tgt[RemapR3]) report[8] |= kR3Bit;
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// Button remapping — a 16-entry table persisted in its own flash sector,
|
||||||
|
// applied to the OUTGOING host HID report only (never the raw interrupt_in_data
|
||||||
|
// the OLED / reboot-combo logic reads). Edited from the web config tool over the
|
||||||
|
// already-declared 0xF6/0xF7 vendor reports. Apply logic + button set ported
|
||||||
|
// from SundayMoments/DS5_Bridge (credit).
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef DS5_BRIDGE_REMAP_H
|
||||||
|
#define DS5_BRIDGE_REMAP_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Number of remappable buttons (see RemapButton in remap.cpp). The table maps
|
||||||
|
// source index -> target index; 0xFF means "disabled" (source does nothing).
|
||||||
|
constexpr int kRemapCount = 16;
|
||||||
|
|
||||||
|
// Wire-protocol version for the remap get/set framing carried over the existing
|
||||||
|
// 0xF6/0xF7 vendor reports (see cmd.cpp). Bump only on incompatible layout
|
||||||
|
// changes so the web tool can refuse a mismatched firmware.
|
||||||
|
constexpr uint8_t kRemapProtoVer = 1;
|
||||||
|
|
||||||
|
// Load the table from its dedicated flash sector. Call once at boot, after
|
||||||
|
// config_load(). A fresh/invalid sector defaults to identity (no remap).
|
||||||
|
void remap_load();
|
||||||
|
|
||||||
|
// Remap a 63-byte DS5 input-report COPY in place (buttons live in report[4,5,7,8]).
|
||||||
|
// No-op fast path when the table is identity. Must only ever touch the outgoing
|
||||||
|
// host report, not the raw interrupt_in_data.
|
||||||
|
void remap_apply(uint8_t *report);
|
||||||
|
|
||||||
|
// Validate + store + persist a new 16-entry table (each entry < 16, or 0xFF =
|
||||||
|
// disabled). Bumps the revision on success. Returns false if the table is invalid.
|
||||||
|
bool remap_set(const uint8_t *table);
|
||||||
|
|
||||||
|
// Copy the current 16-entry table out.
|
||||||
|
void remap_get(uint8_t out[kRemapCount]);
|
||||||
|
|
||||||
|
// Monotonic counter bumped on each successful remap_set — the web polls it to
|
||||||
|
// confirm a write landed. Runtime only (not persisted).
|
||||||
|
uint16_t remap_revision();
|
||||||
|
|
||||||
|
#endif // DS5_BRIDGE_REMAP_H
|
||||||
Reference in New Issue
Block a user