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