Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: ______
Expand Down
24 changes: 22 additions & 2 deletions src/ConsoleMode/Services/ControllerMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,29 @@ public static class ControllerMapping
/// <summary>DualSense and DualSense Edge; any other Sony pad is read with the DS4 layout.</summary>
public static bool IsDualSense(ushort productId) => productId is 0x0CE6 or 0x0DF2;

/// <summary>Input report size the DualSense declares over USB; over Bluetooth it declares 78 (report 0x31).</summary>
public const int DualSenseUsbReportLength = 64;

/// <summary>
/// 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.
/// </summary>
public static ushort SonyButtons(ReadOnlySpan<byte> report, bool dualSense)
/// <param name="inputReportLength">
/// 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.
/// </param>
public static ushort SonyButtons(ReadOnlySpan<byte> 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.
var (start, stick) = report[0] switch
{
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)
};
Expand Down Expand Up @@ -58,6 +67,17 @@ public static ushort SonyButtons(ReadOnlySpan<byte> report, bool dualSense)
return bits;
}

/// <summary>Report IDs <see cref="SonyButtons"/> reads: basic/USB (0x01), DS4 Bluetooth (0x11), DualSense Bluetooth (0x31).</summary>
public static bool IsKnownSonyReport(byte reportId) => reportId is 0x01 or 0x11 or 0x31;

/// <summary>ID 0x01 on a DualSense is the USB report only when the pad's reports are USB-sized (64 bytes).</summary>
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;
}

/// <summary>HID button indexes (confirm, back, option, alt, menu) for a vendor's layout.</summary>
/// <remarks>
/// Sony (DS4/DualSense): Square 0, Cross 1, Circle 2, Triangle 3, L1 4, R1 5, L2 6, R2 7,
Expand Down
15 changes: 14 additions & 1 deletion src/ConsoleMode/Services/SonyHidReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
Expand Down
32 changes: 32 additions & 0 deletions tests/ConsoleMode.Tests/ControllerMappingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Loading