Skip to content
Open
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Description
==============
Control your ESP32 projects with a PS3 controller!

Supports **DualShock 3 / SIXAXIS** and **PS Move Navigation Controller** (CECH-ZCS1). Both use the same pairing and connection flow; controller type is detected automatically from the input report.

This library is meant to be used with Espressif's [ESP-IDF](https://github.com/espressif/esp-idf) IoT Development Framework.

However, Arduino is supported too!
Expand All @@ -13,7 +15,7 @@ When a PS3 controller is 'paired' to a PS3 console, it just means that it has st

Therefore, if you want to connect your PS3 controller to the ESP32, you either need to figure out what the Bluetooth MAC address of your PS3 console is and set the ESP32's address to it, or change the MAC address stored in the PS3 controller.

Whichever path you choose, you're going to need a tool to read and/or write the currently paired MAC address from the PS3 controller. I used [SixaxisPairTool](https://dancingpixelstudios.com/sixaxis-controller/sixaxispairtool/) for this, but you can try using [sixaxispairer](https://github.com/user-none/sixaxispairer) as well, if open source is important to you.
Whichever path you choose, you're going to need a tool to read and/or write the currently paired MAC address from the PS3 controller. I used [SixaxisPairTool](https://dancingpixelstudios.com/sixaxis-controller/sixaxispairtool/) for this, but you can try using [sixaxispairer](https://github.com/user-none/sixaxispairer) as well, if open source is important to you. The **PS Move Navigation Controller** uses the same pairing mechanism; tools like [8BitDo RR Tools](https://www.8bitdo.com/) or `sixpair` work for both.

**Note for ESP-IDF:** If you opted to change the ESP32's MAC address, you'll need to include this snippet in your code **before** calling ```ps3Init()```, where the MAC address should match with the one stored on the PS3 controller:
```c
Expand Down Expand Up @@ -90,6 +92,10 @@ In order to learn more about how to use this library, please refer to the exampl

- Finally, `Ps3Accelerometer` allows you to draw live graphs of the accelerometer data inside the PS3 controller by using `Tools -> Serial Plotter`.

### Navigation Controller support ###

The library auto-detects the **PS Move Navigation Controller**. Use `Ps3.isNavigationController()` or `Ps3.getControllerType()` to branch your logic. The Nav Controller has no right stick, no accelerometer/gyro, no rumble, and only one LED; the library handles these differences automatically.


Getting Started with ESP-IDF
==============
Expand Down
2 changes: 1 addition & 1 deletion library.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name=PS3 Controller Host
version=1.1.0
version=1.2.0
author=Jeffrey van Pernis
maintainer=Jeffrey van Pernis
sentence=Control your ESP32 projects with a PS3 controller!
Expand Down
95 changes: 95 additions & 0 deletions psNavResearch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Adding PS Move Navigation Controller to your DualShock 3 driver

The PS Move Navigation Controller (CECH-ZCS1) is, at the protocol level, **a stripped-down DualShock 3**. It uses the identical Bluetooth connection flow, identical L2CAP channel setup, identical pairing mechanism, identical activation command, and identical 49-byte input report format. If your driver already handles the DS3, the minimum change required is adding a single Product ID (`0x042F`) to your device recognition logic. Everything else—from the L2CAP PSM values to the Feature Report activation sequence—is byte-for-byte the same. The rest of this report details the nuances that matter for a robust implementation.

## One PID stands between you and full support

Both controllers share Sony's Vendor ID **`0x054C`** but use different Product IDs:

| Controller | VID | PID | Bluetooth device name |
|---|---|---|---|
| DualShock 3 / SIXAXIS | `0x054C` | **`0x0268`** | "PLAYSTATION(R)3 Controller" |
| Navigation Controller | `0x054C` | **`0x042F`** | "Navigation Controller" |

The Bluetooth Class of Device is identical: **`0x002504`** (Major: Peripheral, Minor: Joystick, Service Class: none). Both controllers advertise the same broken HID service via SDP, and both present 148-byte native HID report descriptors that are structurally identical and equally malformed (Collection types swapped between Physical and Application). The PS3 console never reads SDP or the HID descriptor from either controller—it uses hardcoded protocol knowledge—which is why Sony never bothered fixing the descriptors.

**The single most common reason** a system supporting DS3 fails to recognize the Navigation Controller is a PID whitelist that only includes `0x0268`. Adding `0x042F` and routing it through the same code path is the critical first step.

## Bluetooth connection and pairing are identical

The Navigation Controller uses the exact same non-standard Bluetooth pairing and connection sequence as the DS3:

**Pairing (USB-based, no Bluetooth PIN):** Connect the controller via USB, then write the host's Bluetooth MAC address to **Feature Report `0xF5`** (8 bytes: `[0xF5, 0x01, MAC[0..5]]`). Read the controller's own MAC via **Feature Report `0xF2`** (17 bytes; MAC at bytes 4–9, big-endian). No Bluetooth-level PIN, SSP, or link key exchange occurs. The `sixpair` tool and BlueZ `sixaxis` plugin work for both controllers without modification.

**Bluetooth connection sequence (controller-initiated):** After pressing the PS button, the controller initiates an ACL connection to the stored host MAC, then opens two L2CAP channels:

1. **HID Control** — PSM `0x0011`
2. **HID Interrupt** — PSM `0x0013`

The host must accept incoming connections (page scan mode), perform a role switch to become piconet master, and then send the **activation command** on the HID Control channel: a SET_REPORT (Feature) with HIDP header `0x53`, Report ID `0xF4`, and payload **`0x42, 0x03, 0x00, 0x00`**. Without this activation, neither controller transmits input reports. This sequence is byte-identical for both devices.

There is **no cryptographic authentication** or challenge-response for either controller. Security relies entirely on physical USB access for initial pairing. Both controllers connect without Bluetooth bonding, which means hosts enforcing `ClassicBondedOnly` (e.g., for CVE-2023-45866 mitigation) will reject both devices equally.

## The 49-byte input report is the same structure, different contents

Both controllers send **Report ID `0x01`**, **49 bytes**, at approximately **100 Hz** over the HID Interrupt channel (prefixed with `0xA1` over Bluetooth). The byte layout is identical; the Navigation Controller simply reports neutral/zero values for hardware it lacks:

| Offset | Field | DS3 | Nav Controller |
|--------|-------|-----|----------------|
| 0 | Report ID | `0x01` | `0x01` |
| 1 | Reserved | `0x00`/`0x01` | Same |
| 2 | Buttons byte 1: Select(0), L3(1), R3(2), Start(3), D-Up(4), D-Right(5), D-Down(6), D-Left(7) | All active | L3, D-pad active; Select/R3/Start = 0 |
| 3 | Buttons byte 2: L2(0), R2(1), L1(2), R1(3), △(4), ○(5), ✕(6), □(7) | All active | L2, L1, ○, ✕ active; R2/R1/△/□ = 0 |
| 4 | PS button (bit 0) | Active | Active |
| 6–7 | Left stick X, Y | `0x00`–`0xFF` | `0x00`–`0xFF` (functional) |
| 8–9 | Right stick X, Y | `0x00`–`0xFF` | **Fixed ~`0x80`** (no hardware) |
| 14–17 | D-pad pressure (Up/Right/Down/Left) | `0x00`–`0xFF` | Functional |
| 18 | L2 analog pressure | `0x00`–`0xFF` | Functional |
| 19–21 | R2/L1/R1 pressure | Active | R2/R1 = `0x00`; L1 functional |
| 22–25 | △/○/✕/□ pressure | Active | ○/✕ functional; △/□ = `0x00` |
| 29 | Battery level | Available | Available |
| 41–42 | Accelerometer X (big-endian) | Live data | **`0x00`** (no hardware) |
| 43–44 | Accelerometer Y | Live data | `0x00` |
| 45–46 | Accelerometer Z | Live data | `0x00` (some revisions send non-zero) |
| 47–48 | Gyroscope Z (big-endian) | Live data | `0x00` (some revisions send non-zero) |

The **physical buttons** on the Navigation Controller are: left analog stick (with L3 click), D-pad, L1, L2 (analog trigger), Cross (✕), Circle (○), and PS button. That's 9 inputs plus one analog stick. Every other field in the report exists but carries no meaningful data.

One parsing subtlety: the Linux kernel byte-swaps the accelerometer/gyro values at offsets 41–48 from big-endian to little-endian for the DS3, but **skips this swap for the Navigation Controller** since those bytes are zero/undefined. If your driver applies the swap unconditionally, it's harmless on zeroes but technically incorrect—a clean implementation should gate the swap on controller type.

**Host-specific packet format:** The HID transport layer may add a header before the 49-byte report. On ESP32/Bluedroid, L2CAP delivers a 50-byte payload: `[0x02][49-byte report]`. Other stacks (e.g. Linux HIDP) may use `[0xA1][49-byte report]`. The report structure (offsets 0–48) is identical; only the transport prefix differs.

**Sensor bytes vary by revision:** Some Navigation Controller revisions send non-zero values in Accel Z and Gyroscope Z (report bytes 45–48). Accel X and Y (bytes 41–44) appear reliably zero across tested units. For packet-based controller detection (when PID is unavailable at connection time): check Accel X/Y for all-zero, then inspect Accel Z high byte—DS3 real sensor readings use high byte ≥ 0x30 (e.g. 0x33FC), while Nav sends 0 or garbage (e.g. 0x23FB).

## Output reports, LEDs, and rumble: same format, no effect

The output report is **Report ID `0x01`, 48 bytes**, sent via SET_REPORT on the HID Control channel (header `0x52`). The format is identical for both controllers:

| Offset | Purpose | DS3 | Nav Controller |
|--------|---------|-----|----------------|
| 2–3 | Right (weak) motor: duration, on/off | Functional | **No motor** |
| 4–5 | Left (strong) motor: duration, force (`0x00`–`0xFF`) | Functional | **No motor** |
| 10 | LED bitmask (bit1=LED1 … bit4=LED4) | 4 LEDs | **1 LED** (bit 0 = `0x01` only) |
| 11–30 | LED blink parameters (5 bytes × 4 LEDs) | All functional | Only LED 1 |

The Navigation Controller **accepts** output reports silently but has no rumble motors and only one status LED. Sending rumble commands is harmless. The Linux kernel registers only 1 LED for the Navigation Controller versus 4 for the DS3.

## What to change in a working DS3 driver

For a developer with existing DS3 Bluetooth support, here is a prioritized checklist of required and recommended changes:

- **Required: Add PID `0x042F`** to your device recognition logic alongside `0x0268`. Route it through the same connection, pairing, and activation code paths—they are identical. On hosts where PID is not available at connection time (e.g. some embedded stacks), use packet-based detection: if Accel X/Y (report bytes 41–44) are all zero, treat as Navigation Controller.

- **Required: Accept the Navigation Controller's SDP/HID descriptor** without errors. If your driver validates the HID descriptor, either skip validation (as the PS3 does) or replace the descriptor entirely. Both controllers ship broken descriptors that no compliant HID parser handles correctly.

- **Recommended: Gate the accelerometer/gyro byte-swap** on controller type. For PID `0x042F`, skip the `swap(rd[41], rd[42])` through `swap(rd[47], rd[48])` operations since those bytes contain no sensor data.

- **Recommended: Adjust input mapping** to expose only the Navigation Controller's physical inputs (left stick X/Y, D-pad, L1, L2, L3, Cross, Circle, PS button). Map absent buttons to nothing rather than exposing phantom zero-state inputs.

- **Recommended: Reduce LED count** to 1 and disable rumble feedback paths for PID `0x042F` to avoid wasted output report traffic.

- **Optional: Handle hardware revisions gracefully**. Sony shipped multiple Navigation Controller revisions with slightly different native report descriptors. Complete descriptor replacement (rather than patching) is the robust approach, as the Linux kernel learned.

## Conclusion

The Navigation Controller is architecturally a DualShock 3 with half the hardware removed. At the Bluetooth protocol level—connection initiation, L2CAP channels, pairing mechanism, activation sequence, report structure, and authentication—the two devices are **indistinguishable** except for the Product ID. The 49-byte input report uses the same byte layout; absent hardware simply reports neutral values. No changes to your Bluetooth stack, L2CAP handling, pairing flow, or HID transport are needed. The entire integration reduces to PID recognition, input mapping adjustments, and minor output report optimizations. The Linux kernel's `hid-sony.c` driver, which has maintained both controllers for over a decade, confirms this with shared code paths gated almost exclusively on quirk flags that differ only in which PID triggered them.
16 changes: 16 additions & 0 deletions src/Ps3Controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,18 @@ String Ps3Controller::getAddress() {
bool Ps3Controller::isConnected()
{
return ps3IsConnected();
}


ps3_controller_type_t Ps3Controller::getControllerType()
{
return ps3GetControllerType();
}


bool Ps3Controller::isNavigationController()
{
return ps3GetControllerType() == ps3_controller_nav;
}


Expand Down Expand Up @@ -149,7 +160,12 @@ void Ps3Controller::attachOnConnect(callback_t callback)
void Ps3Controller::attachOnDisconnect(callback_t callback)
{
_callback_disconnect = callback;
}


void Ps3Controller::setDebugCallback(void (*cb)(const char *))
{
ps3SetDebugPrintCallback(cb);
}


Expand Down
4 changes: 4 additions & 0 deletions src/Ps3Controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class Ps3Controller
String getAddress();

bool isConnected();
bool isNavigationController();
ps3_controller_type_t getControllerType();

void setPlayer(int player);
void setRumble(float intensity, int duration = -1);
Expand All @@ -35,6 +37,8 @@ class Ps3Controller
void attachOnConnect(callback_t callback);
void attachOnDisconnect(callback_t callback);

void setDebugCallback(void (*cb)(const char *));

private:
static void _event_callback(void *object, ps3_t data, ps3_event_t event);
static void _connection_callback(void *object, uint8_t is_connected);
Expand Down
12 changes: 12 additions & 0 deletions src/include/ps3.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
/* T Y P E S */
/********************************************************************************/

/**********************************/
/* C O N T R O L L E R T Y P E */
/**********************************/

typedef enum {
ps3_controller_ds3 = 0,
ps3_controller_nav = 1
} ps3_controller_type_t;

/********************/
/* A N A L O G */
/********************/
Expand Down Expand Up @@ -144,6 +153,7 @@ typedef struct {
ps3_button_t button;
ps3_status_t status;
ps3_sensor_t sensor;
ps3_controller_type_t controller_type;

} ps3_t;

Expand All @@ -164,6 +174,7 @@ typedef void(*ps3_event_object_callback_t)( void *object, ps3_t ps3, ps3_event_t
/********************************************************************************/

bool ps3IsConnected();
ps3_controller_type_t ps3GetControllerType();
void ps3Init();
void ps3Deinit();
void ps3Enable();
Expand All @@ -175,6 +186,7 @@ void ps3SetEventObjectCallback( void *object, ps3_event_object_callback_t cb );
void ps3SetLed( uint8_t player );
void ps3SetLedCmd( ps3_cmd_t *cmd, uint8_t player );
void ps3SetBluetoothMacAddress( const uint8_t *mac );
void ps3SetDebugPrintCallback( void (*cb)(const char *) );


#endif
6 changes: 5 additions & 1 deletion src/include/ps3_int.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,11 @@ void ps3_packet_event( ps3_t ps3, ps3_event_t event );
/* P A R S E R F U N C T I O N S */
/********************************************************************************/

void ps3_parse_packet( uint8_t *packet );
void ps3_parse_packet( uint8_t *packet, uint16_t len );
ps3_controller_type_t ps3_parse_controller_type( uint8_t *packet, uint16_t len );

/* Optional debug output - set via ps3SetDebugPrintCallback() */
extern void (*ps3_debug_print)(const char *);


/********************************************************************************/
Expand Down
Loading