Skip to content
Draft
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
59 changes: 59 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## ninjaUSB – AI Coding Assistant Instructions

Goal: Firmware for an nRF52840 dongle acting as a BLE peripheral that receives commands/keystrokes and emits USB HID keyboard reports (future BadUSB extensions). Keep changes minimal, Zephyr‑aligned, and reproducible.

### Architecture & Flow
1. Entry: `app/src/main.c` – initializes GPIO LED, USB HID device, Bluetooth stack, advertising, event loop.
2. USB: `app/src/usb_init.c` builds descriptors & config via Zephyr NEXT USB stack (`CONFIG_USB_DEVICE_STACK_NEXT`, HID enabled). Placeholders (VID/PID, manufacturer) still marked TODO.
3. HID Device Node: `app/app.overlay` defines `hid_dev_0` (keyboard protocol, 64‑byte IN report, 1 ms poll). Retrieved with `DEVICE_DT_GET_ONE(zephyr_hid_device)`.
4. Reports: Static buffer `report[]` sized by `KB_REPORT_COUNT` indexes (mod key + 6 keycodes). Submission via `hid_device_submit_report(hid_dev, KB_REPORT_COUNT, report)` after readiness flag set in `.iface_ready` callback.
5. Input Path: Zephyr input subsystem callback (`input_cb`) enqueues events into `kb_msgq`; main loop dequeues and mutates `report` based on `INPUT_KEY_*` codes.
6. BLE: Custom 128‑bit service + writable characteristic (`write_command`) in `main.c`; writes copied to `command_buf`. Example: first byte == 1 triggers NumLock key press.
7. Event Loop: Blocks on `k_msgq_get`, updates report, checks `kb_ready`, then sends HID report.

### Build / Flash Workflow
Build (from repo root inside Zephyr workspace):
```
west build -b nrf52840dongle/nrf52840 app
```
Artifacts: `build/zephyr/zephyr.hex`, signed package generated manually.
Flash via serial DFU (adjust `/dev/ttyACM0` as needed):
```
nrfutil pkg generate --hw-version 52 --sd-req=0x00 \
--application build/zephyr/zephyr.hex \
--application-version 1 build/firmware.zip
nrfutil dfu usb-serial -pkg build/firmware.zip -p /dev/ttyACM0
```
If USB VBUS detection unsupported, device enabling is forced (`!usbd_can_detect_vbus`).

### Conventions & Patterns
- CMake: `app/CMakeLists.txt` glob‑adds `src/*.c`; keep new sources in `app/src/` or include headers via `app/inc/` + `zephyr_include_directories(inc)`.
- Configuration split: Feature toggles & stack settings in `app/prj.conf`; only add Zephyr Kconfig symbols actually used.
- Versioning: `app/VERSION` (Zephyr format) – bump when changing externally observable behavior.
- Logging: Use `LOG_MODULE_REGISTER(<module>, LOG_LEVEL_*)`; prefer existing modules (`main`, `usbd_app_config`); avoid printk except for very early BLE messages (some still present).
- HID report editing: Mutate indices defined by `enum kb_report_idx`; always clear released keys (set to 0) to avoid stuck modifiers.
- BLE characteristic writes: Validate length (`len <= sizeof(command_buf)`), return proper ATT error on overflow. Extend action dispatch using first command byte (add `switch` instead of chained `if`).
- USB descriptors: Update constants in `usb_init.c` (`USB_DEVICE_MANUFACTURER`, VID/PID) before distributing hardware; ensure uniqueness.
- Avoid blocking outside of main loop; use Zephyr work (`k_work`) for deferred operations like restarting advertising (`adv_work`).

### Extending Functionality (Examples)
- Add a new command (e.g., send string): extend `write_command` with command byte map; enqueue synthesized `kb_event`s instead of directly editing `report` for uniformity.
- Add more input sources: register additional `INPUT_CALLBACK_DEFINE` handlers; ensure they produce `INPUT_KEY_*` codes the switch handles.
- Additional keys: Expand switch in main loop; maintain mutual exclusivity if needed (clear prior codes).

### Gotchas
- Do not exceed `KB_REPORT_COUNT`; HID keyboard standard supports 6 simultaneous keys + modifiers.
- `hid_device_submit_report` requires device readiness (`kb_ready` flag set in `.iface_ready`). Submissions earlier silently fail or log errors.
- VID/PID placeholders must not ship; Nordic examples’ values are not production legal.
- Message queue depth is 2 (`K_MSGQ_DEFINE(kb_msgq, ..., 2, ...)`); burst inputs beyond this are dropped; enlarge if adding rapid input sources.

### Safe Change Checklist
1. Build succeeds (`west build ...`).
2. USB enumeration intact (keyboard recognized) – changing descriptors can break host detection.
3. BLE advertising still starts after disconnect (`recycled_cb` triggers `advertising_start`).
4. No stuck keys (press/release path clears report indices).

### Areas Lacking Automation
No unit / integration tests present; validation is manual (USB enumeration + BLE write -> HID action). Keep additions deterministic and log‑rich.

Feedback Wanted: Clarify more on BLE command protocol structure? Add guidance for future BadUSB scripting layer? Indicate and I’ll refine.
2 changes: 1 addition & 1 deletion app/VERSION
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
VERSION_MAJOR = 0
VERSION_MINOR = 1
VERSION_MINOR = 2
PATCHLEVEL = 0
VERSION_TWEAK = 0
EXTRAVERSION =
9 changes: 9 additions & 0 deletions app/app.overlay
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
/ {
chosen {
/* Route Zephyr console/shell over physical UART0 instead of USB CDC ACM */
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
zephyr,uart-mcumgr = &uart0;
zephyr,bt-mon-uart = &uart0;
zephyr,bt-c2h-uart = &uart0;
};

hid_dev_0: hid_dev_0 {
compatible = "zephyr,hid-device";
label = "HID0";
Expand Down
16 changes: 16 additions & 0 deletions app/prj.conf
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,19 @@ CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="NinjaUSB"

# Enable SMP (Security Manager Protocol) for bonding and pairing
CONFIG_BT_SMP=y
CONFIG_BT_BONDABLE=y
CONFIG_BT_KEYS_OVERWRITE_OLDEST=y

# Enable settings subsystem for persistent bonding storage
CONFIG_SETTINGS=y
CONFIG_BT_SETTINGS=y

# Route console and logging to UART
CONFIG_SERIAL=y
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_LOG_BACKEND_UART=y
CONFIG_LOG_MODE_IMMEDIATE=n

82 changes: 64 additions & 18 deletions app/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/uuid.h>

#include <zephyr/settings/settings.h>

#include <zephyr/usb/class/usbd_hid.h>
#include <zephyr/usb/usbd.h>

Expand Down Expand Up @@ -156,43 +158,52 @@ static void msg_cb(struct usbd_context *const usbd_ctx,
/* BLE
*/
/************************************************************************/
#define BT_UUID_CUSTOM_SERVICE_VAL \
BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef0)

/* Switch to standard HID Service UUID (0x1812) for the primary service. */
#define BT_UUID_CMD_CHAR_VAL \
BT_UUID_128_ENCODE(0xabcdef01, 0x2345, 0x6789, 0x2345, 0x6789abcdef01)

static struct bt_uuid_128 custom_service_uuid =
BT_UUID_INIT_128(BT_UUID_CUSTOM_SERVICE_VAL);
static struct bt_uuid_128 cmd_char_uuid =
BT_UUID_INIT_128(BT_UUID_CMD_CHAR_VAL);

static uint8_t command_buf[20];
/* BLE write payload will directly overwrite the HID report buffer ("raw pass-through"). */

ssize_t write_command(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset,
uint8_t flags) {
if (len > sizeof(command_buf)) {
ARG_UNUSED(conn);
ARG_UNUSED(attr);
ARG_UNUSED(offset);
ARG_UNUSED(flags);

/* Expect raw HID keyboard report bytes from BLE. The standard keyboard
* report is KB_REPORT_COUNT bytes (modifier, reserved, 6 keycodes). If
* fewer bytes are provided we zero-fill the remainder. If more are sent
* we reject to avoid accidental overflow / unintended state.
*/
if (len > KB_REPORT_COUNT) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
}

memcpy(command_buf, buf, len);
printk("Received command: ");
for (int i = 0; i < len; i++) {
printk("%02x ", command_buf[i]);
}
printk("\n");
/* Clear existing report then copy provided bytes */
memset(report, 0, KB_REPORT_COUNT);
memcpy(report, buf, len);

LOG_INF("BLE raw HID report write (%u bytes)", len);
LOG_HEXDUMP_DBG(report, KB_REPORT_COUNT, "hid-rx");

if (command_buf[0] == 1) {
report[KB_KEY_CODE1] = HID_KEY_NUMLOCK;
hid_device_submit_report(hid_dev, KB_REPORT_COUNT, report);
if (!kb_ready) {
LOG_WRN("HID interface not ready; dropping report");
return len; /* Return len so GATT write appears successful */
}

int ret = hid_device_submit_report(hid_dev, KB_REPORT_COUNT, report);
if (ret) {
LOG_ERR("Failed to submit HID report (%d)", ret);
}
return len;
}

BT_GATT_SERVICE_DEFINE(
custom_svc, BT_GATT_PRIMARY_SERVICE(&custom_service_uuid),
custom_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_HIDS),
BT_GATT_CHARACTERISTIC(&cmd_char_uuid.uuid, BT_GATT_CHRC_WRITE,
BT_GATT_PERM_WRITE, NULL, write_command, NULL), );

Expand All @@ -206,6 +217,7 @@ static const struct bt_data ad[] = {
BT_DATA_BYTES(
BT_DATA_FLAGS,
(BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), /* Set the advertising flags */
BT_DATA_BYTES(BT_DATA_UUID16_ALL, 0x12, 0x18), /* Advertise HID Service UUID */
BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME,
sizeof(CONFIG_BT_DEVICE_NAME) - 1),
/* Set the advertising packet data */};
Expand Down Expand Up @@ -260,6 +272,28 @@ BT_CONN_CB_DEFINE(connection_callbacks) = {
.recycled = recycled_cb,
};

/* Authentication callbacks for bonding support */
static void auth_cancel(struct bt_conn *conn)
{
LOG_INF("Pairing cancelled");
}

static void auth_pairing_complete(struct bt_conn *conn, bool bonded)
{
LOG_INF("Pairing completed %s", bonded ? "and bonded" : "but not bonded");
}

static void auth_pairing_failed(struct bt_conn *conn, enum bt_security_err reason)
{
LOG_ERR("Pairing failed (reason %d)", reason);
}

static struct bt_conn_auth_cb auth_cb = {
.cancel = auth_cancel,
.pairing_complete = auth_pairing_complete,
.pairing_failed = auth_pairing_failed,
};

int main(void) {
struct usbd_context *app_usbd;
int ret;
Expand Down Expand Up @@ -326,6 +360,18 @@ int main(void) {
return -1;
}

/* Load stored settings for bonding */
ret = settings_load();
if (ret) {
LOG_WRN("Settings load failed (err %d)", ret);
}

/* Register authentication callbacks for bonding support */
ret = bt_conn_auth_cb_register(&auth_cb);
if (ret) {
LOG_ERR("Failed to register auth callbacks (err %d)", ret);
}

/* Start connectable advertising */
k_work_init(&adv_work, adv_work_handler);
advertising_start();
Expand Down