From 44d126fbf7878e5bca4d90ce2762d5e26f0fa6bb Mon Sep 17 00:00:00 2001 From: nextestudios Date: Wed, 30 Sep 2026 23:19:29 -0300 Subject: [PATCH 1/3] fix: read a DualSense over Bluetooth whatever its report padding The DualSense's basic Bluetooth report has the same ID (0x01) as the USB report but a different layout; the reader told them apart by the size of the bytes read (>= 64 = USB). When Windows hands the report back padded to the declared Bluetooth size (78), the basic report was parsed with the USB offsets and every button read as released. Decide by the size the HID descriptor declares instead, and log the first report of each pad (ID, size, declared size) and any unknown report ID, so a pad that reads wrong can be diagnosed from consolemode.log. Co-Authored-By: Claude Sonnet 5.5 --- src/ConsoleMode/Services/ControllerMapping.cs | 24 ++++++++++++-- src/ConsoleMode/Services/SonyHidReader.cs | 15 ++++++++- .../ControllerMappingTests.cs | 32 +++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/ConsoleMode/Services/ControllerMapping.cs b/src/ConsoleMode/Services/ControllerMapping.cs index 77bd24d..d5185a7 100644 --- a/src/ConsoleMode/Services/ControllerMapping.cs +++ b/src/ConsoleMode/Services/ControllerMapping.cs @@ -13,12 +13,21 @@ public static class ControllerMapping /// DualSense and DualSense Edge; any other Sony pad is read with the DS4 layout. public static bool IsDualSense(ushort productId) => productId is 0x0CE6 or 0x0DF2; + /// Input report size the DualSense declares over USB; over Bluetooth it declares 78 (report 0x31). + public const int DualSenseUsbReportLength = 64; + /// /// XInput-style button bits from a raw DS4/DualSense input report (first byte = report ID): /// Cross = A, Circle = B, Square = X, Triangle = Y, L1/R1 = LB/RB, Options = Start, Create/Share = Back, /// PS = Guide; the hat and the left stick set the D-pad bits. Unknown reports give 0. /// - public static ushort SonyButtons(ReadOnlySpan report, bool dualSense) + /// + /// The report size the HID descriptor declares (HIDP_CAPS.InputReportByteLength), 0 when unknown. + /// ReadFile may hand back every report padded to that size, so a DualSense's basic Bluetooth report + /// (ID 0x01, 10 meaningful bytes) can arrive 78 bytes long: its size alone can't tell it from the + /// 64-byte USB report that has the same ID but a different layout; the declared size can. + /// + public static ushort SonyButtons(ReadOnlySpan report, bool dualSense, int inputReportLength = 0) { if (report.Length == 0) return 0; // Offset of the buttons byte (hat + face) and of the left stick X, per report layout. @@ -26,7 +35,7 @@ public static ushort SonyButtons(ReadOnlySpan report, bool dualSense) { 0x31 => (9, 2), // DualSense, Bluetooth 0x11 => (7, 3), // DS4, Bluetooth - 0x01 when dualSense && report.Length >= 64 => (8, 1), // DualSense, USB + 0x01 when IsDualSenseUsbReport(report.Length, dualSense, inputReportLength) => (8, 1), // DualSense, USB 0x01 => (5, 1), // DS4 USB, or either pad's basic Bluetooth report _ => (-1, -1) }; @@ -58,6 +67,17 @@ public static ushort SonyButtons(ReadOnlySpan report, bool dualSense) return bits; } + /// Report IDs reads: basic/USB (0x01), DS4 Bluetooth (0x11), DualSense Bluetooth (0x31). + public static bool IsKnownSonyReport(byte reportId) => reportId is 0x01 or 0x11 or 0x31; + + /// ID 0x01 on a DualSense is the USB report only when the pad's reports are USB-sized (64 bytes). + private static bool IsDualSenseUsbReport(int reportLength, bool dualSense, int inputReportLength) + { + if (!dualSense || reportLength < DualSenseUsbReportLength) return false; + // Padded to the Bluetooth size (78): this is the basic Bluetooth report. + return inputReportLength is 0 or DualSenseUsbReportLength; + } + /// HID button indexes (confirm, back, option, alt, menu) for a vendor's layout. /// /// Sony (DS4/DualSense): Square 0, Cross 1, Circle 2, Triangle 3, L1 4, R1 5, L2 6, R2 7, diff --git a/src/ConsoleMode/Services/SonyHidReader.cs b/src/ConsoleMode/Services/SonyHidReader.cs index 066377f..2afa4c7 100644 --- a/src/ConsoleMode/Services/SonyHidReader.cs +++ b/src/ConsoleMode/Services/SonyHidReader.cs @@ -96,11 +96,24 @@ private static async Task ReadLoopAsync(string path, Pad pad, CancellationToken { await using var stream = new FileStream(pad.Handle, FileAccess.Read, 0, isAsync: true); var buffer = new byte[Math.Max(pad.ReportLength, 64)]; + var loggedFirst = false; + var loggedUnknown = false; while (!ct.IsCancellationRequested) { var read = await stream.ReadAsync(buffer.AsMemory(0, pad.ReportLength), ct); if (read <= 0) break; - var bits = ControllerMapping.SonyButtons(buffer.AsSpan(0, read), pad.DualSense); + // Once per pad, so a pad that reads wrong can be diagnosed from the log alone. + if (!loggedFirst) + { + loggedFirst = true; + AppLog.Write($"Controle: HID {(pad.DualSense ? "DualSense" : "DualShock 4")}: relatório 0x{buffer[0]:X2} com {read} bytes (descritor declara {pad.ReportLength})"); + } + if (!loggedUnknown && !ControllerMapping.IsKnownSonyReport(buffer[0])) + { + loggedUnknown = true; + AppLog.Write($"Controle: HID: relatório 0x{buffer[0]:X2} não reconhecido; os botões desse formato não são lidos"); + } + var bits = ControllerMapping.SonyButtons(buffer.AsSpan(0, read), pad.DualSense, pad.ReportLength); lock (Gate) { Buttons[path] = bits; } } } diff --git a/tests/ConsoleMode.Tests/ControllerMappingTests.cs b/tests/ConsoleMode.Tests/ControllerMappingTests.cs index 61bd7ee..248da6f 100644 --- a/tests/ConsoleMode.Tests/ControllerMappingTests.cs +++ b/tests/ConsoleMode.Tests/ControllerMappingTests.cs @@ -131,4 +131,36 @@ public void Sony_options_and_ps_map_to_start_and_guide() [InlineData(new byte[] { 0x31, 0, 0 })] // too short public void Unknown_or_short_sony_reports_give_nothing(byte[] report) => Assert.Equal(0, ControllerMapping.SonyButtons(report, dualSense: true)); + + // A DualSense over Bluetooth in its basic mode (ID 0x01, 10 meaningful bytes). Windows may hand every + // report back padded to the declared Bluetooth size (78), which must not be read as the 64-byte USB layout. + [Theory] + [InlineData(10)] // exactly the report + [InlineData(78)] // padded to the declared size + public void DualSense_basic_bluetooth_report_is_read_whatever_its_padding(int length) + { + var report = new byte[length]; + report[0] = 0x01; + report[1] = report[2] = 128; // left stick centred + report[5] = (byte)(0x08 | 0x20); // hat released + Cross + report[6] = 0x20; // Options + Assert.Equal(0x1000 | 0x0010, ControllerMapping.SonyButtons(report, dualSense: true, inputReportLength: 78)); + } + + [Fact] + public void DualSense_usb_report_keeps_its_own_layout_when_the_declared_size_is_64() + { + var report = NeutralReport(0x01, 64, 8, 1); + report[8] |= 0x20; // Cross + Assert.Equal(0x1000, ControllerMapping.SonyButtons(report, dualSense: true, inputReportLength: 64)); + } + + [Theory] + [InlineData(0x01, true)] + [InlineData(0x11, true)] + [InlineData(0x31, true)] + [InlineData(0x05, false)] + [InlineData(0x00, false)] + public void Only_the_documented_sony_report_ids_are_known(byte id, bool known) => + Assert.Equal(known, ControllerMapping.IsKnownSonyReport(id)); } From 17e471c5441f63e8b72d51cd0cbfe9444b684c29 Mon Sep 17 00:00:00 2001 From: nextestudios Date: Wed, 30 Sep 2026 23:19:45 -0300 Subject: [PATCH 2/3] docs: manual checks for the DualSense over Bluetooth Co-Authored-By: Claude Sonnet 5.5 --- docs/TESTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/TESTING.md b/docs/TESTING.md index 6918d61..d4f393f 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -328,6 +328,8 @@ Pré-condições: 1.6 instalada por cima da 1.5, com a configuração da 1.5 (te - [ ] Repetir por Bluetooth e com Steam Input/DS4Windows ligado: cada toque produz uma única ação, sem movimento em repouso. Resultado: ______ - [ ] Com um Xbox também conectado, repetir a navegação com o DualShock. Resultado: ______ - [ ] Reconectar o DualShock com a tela Console aberta: a navegação volta a responder. Resultado: ______ +- [ ] **DualSense (PS5) por Bluetooth**, sem Steam Input/DS4Windows e com o jogo/Big Picture em foco: Select + Y (Create + △) abre o overlay e, nele, D-pad, ✕, ○ e □ respondem. Repetir por USB. Resultado: ______ +- [ ] O `consolemode.log` tem, por controle Sony, uma linha `Controle: HID DualSense: relatório 0x.. com N bytes (descritor declara M)`. Se o menu não responder, essa linha (e `relatório 0x.. não reconhecido`, se houver) diz o motivo. Resultado: ______ - [ ] Durante o jogo, Share + △ (Select + Y) abre o overlay; → em uma opção lateral leva ao primeiro cartão de janela e ✕ ativa a janela escolhida. Resultado: ______ - [ ] No primeiro cartão de uma coluna, ← retorna à opção lateral de origem; ↑/↓ e ←/→ navegam pelos demais cartões, inclusive com rolagem. Resultado: ______ - [ ] Com seletor de sessão ou confirmação de fechamento aberto, as direções ficam dentro do diálogo. Sem janelas abertas, → não perde o foco. Resultado: ______ From 6f13a9d9cc8d59a5ce6f604ca4d7fafe69cae3bc Mon Sep 17 00:00:00 2001 From: nextestudios Date: Thu, 1 Oct 2026 00:08:45 -0300 Subject: [PATCH 3/3] docs(changelog): reference #132 Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.en-US.md | 3 +++ CHANGELOG.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 4a7cc71..0d5925c 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -4,6 +4,9 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Befor ## [Unreleased] +### Fixes +- Controllers: a DualSense (PS5) over Bluetooth is read again by the shortcuts and the session menu; the log records the first report format of each Sony pad. (#132) + ## [1.6.0-alpha.5] ### Fixes diff --git a/CHANGELOG.md b/CHANGELOG.md index 5074c8d..e390e49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CH ## [Unreleased] +### Correções +- Controles: o DualSense (PS5) por Bluetooth voltou a ser lido pelas teclas de atalho e pelo menu da sessão; o log registra o formato do primeiro relatório de cada controle Sony. (#132) + ## [1.6.0-alpha.5] ### Correções