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 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: ______ 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)); }