Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4fa743654 | ||
|
|
44372c0108 | ||
|
|
5c214f3614 | ||
|
|
f4413b7969 | ||
|
|
cca1254740 | ||
|
|
5bb59c1422 | ||
|
|
cc5638b6e7 | ||
|
|
cc797a16df |
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: "Brief description of what this command does"
|
||||
description: "This command increments the version number and then adds, commits and pushes to the repo."
|
||||
---
|
||||
|
||||
Run increment_and_push.sh, and supply a good git commit message. For example:
|
||||
|
||||
@@ -55,6 +55,7 @@ RUN if [ "$(uname -m)" = "aarch64" ] && ! command -v aarch64-linux-gnu-gcc >/dev
|
||||
|
||||
# Copy source files
|
||||
COPY src/ /build/src/
|
||||
COPY resources/tui_continuous/ /build/resources/tui_continuous/
|
||||
|
||||
# Build nsigner as a fully static binary
|
||||
RUN ARCH="$(uname -m)"; \
|
||||
@@ -68,6 +69,7 @@ RUN ARCH="$(uname -m)"; \
|
||||
-I/build/nostr_core_lib \
|
||||
-I/build/nostr_core_lib/nostr_core \
|
||||
-I/build/nostr_core_lib/cjson \
|
||||
-I/build/resources/tui_continuous \
|
||||
/build/src/main.c \
|
||||
/build/src/secure_mem.c \
|
||||
/build/src/mnemonic.c \
|
||||
@@ -80,6 +82,8 @@ RUN ARCH="$(uname -m)"; \
|
||||
/build/src/transport_frame.c \
|
||||
/build/src/key_store.c \
|
||||
/build/src/socket_name.c \
|
||||
/build/src/auth_envelope.c \
|
||||
/build/resources/tui_continuous/tui_continuous.c \
|
||||
"$NOSTR_LIB" \
|
||||
-o /build/nsigner_static \
|
||||
$(pkg-config --static --libs libcurl openssl) \
|
||||
|
||||
57
Makefile
57
Makefile
@@ -1,10 +1,12 @@
|
||||
CC := gcc
|
||||
CFLAGS := -Wall -Wextra -std=c99 -Os -ffunction-sections -fdata-sections -Isrc -Iresources/nostr_core_lib -Iresources/nostr_core_lib/nostr_core -Iresources/nostr_core_lib/cjson
|
||||
CFLAGS := -Wall -Wextra -std=c99 -Os -ffunction-sections -fdata-sections -Isrc -Iresources/nostr_core_lib -Iresources/nostr_core_lib/nostr_core -Iresources/nostr_core_lib/cjson -Iresources/tui_continuous
|
||||
LDFLAGS := -Wl,--gc-sections resources/nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
|
||||
|
||||
SRC_DIR := src
|
||||
BUILD_DIR := build
|
||||
TEST_DIR := tests
|
||||
CLIENT_DIR := client
|
||||
EXAMPLES_DIR := examples
|
||||
|
||||
TARGET_DEV := $(BUILD_DIR)/nsigner
|
||||
|
||||
@@ -20,7 +22,9 @@ SOURCES := \
|
||||
$(SRC_DIR)/server.c \
|
||||
$(SRC_DIR)/transport_frame.c \
|
||||
$(SRC_DIR)/key_store.c \
|
||||
$(SRC_DIR)/socket_name.c
|
||||
$(SRC_DIR)/socket_name.c \
|
||||
$(SRC_DIR)/auth_envelope.c \
|
||||
resources/tui_continuous/tui_continuous.c
|
||||
|
||||
HEADERS :=
|
||||
|
||||
@@ -33,8 +37,13 @@ TEST_DISPATCHER_TARGET := $(BUILD_DIR)/test_dispatcher
|
||||
TEST_POLICY_TARGET := $(BUILD_DIR)/test_policy
|
||||
TEST_INTEGRATION_TARGET := $(BUILD_DIR)/test_integration
|
||||
TEST_SOCKET_NAME_TARGET := $(BUILD_DIR)/test_socket_name
|
||||
TEST_AUTH_ENVELOPE_TARGET := $(BUILD_DIR)/test_auth_envelope
|
||||
TEST_QREXEC_AUTH_TARGET := $(BUILD_DIR)/test_qrexec_auth
|
||||
TEST_MNEMONIC_INPUT_TARGET := $(BUILD_DIR)/test_mnemonic_input
|
||||
EXAMPLE_GET_PUBLIC_KEY_TARGET := $(BUILD_DIR)/example_get_public_key_client
|
||||
EXAMPLE_SIGN_EVENT_TARGET := $(BUILD_DIR)/example_sign_event_client
|
||||
|
||||
.PHONY: all lib dev static static-debug static-arm64 test test-integration test-mnemonic test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name clean
|
||||
.PHONY: all lib dev static static-debug static-arm64 firmware-feather test test-integration test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth examples test-client clean
|
||||
|
||||
all: dev
|
||||
|
||||
@@ -59,7 +68,10 @@ static-arm64:
|
||||
chmod +x ./build_static.sh
|
||||
./build_static.sh --arch arm64
|
||||
|
||||
test: lib test-mnemonic test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name
|
||||
firmware-feather:
|
||||
cd firmware/feather_s3_tft && idf.py build
|
||||
|
||||
test: lib test-mnemonic test-mnemonic-input test-role test-selector test-enforcement test-dispatcher test-policy test-socket-name test-auth-envelope test-qrexec-auth test-client
|
||||
|
||||
test-integration: $(TEST_INTEGRATION_TARGET) $(TARGET_DEV)
|
||||
./$(TEST_INTEGRATION_TARGET)
|
||||
@@ -67,6 +79,9 @@ test-integration: $(TEST_INTEGRATION_TARGET) $(TARGET_DEV)
|
||||
test-mnemonic: $(TEST_MNEMONIC_TARGET)
|
||||
./$(TEST_MNEMONIC_TARGET)
|
||||
|
||||
test-mnemonic-input: $(TEST_MNEMONIC_INPUT_TARGET)
|
||||
./$(TEST_MNEMONIC_INPUT_TARGET)
|
||||
|
||||
test-role: $(TEST_ROLE_TARGET)
|
||||
./$(TEST_ROLE_TARGET)
|
||||
|
||||
@@ -85,10 +100,24 @@ test-policy: $(TEST_POLICY_TARGET)
|
||||
test-socket-name: $(TEST_SOCKET_NAME_TARGET)
|
||||
./$(TEST_SOCKET_NAME_TARGET)
|
||||
|
||||
test-auth-envelope: $(TEST_AUTH_ENVELOPE_TARGET)
|
||||
./$(TEST_AUTH_ENVELOPE_TARGET)
|
||||
|
||||
test-qrexec-auth: $(TEST_QREXEC_AUTH_TARGET) $(TARGET_DEV)
|
||||
./$(TEST_QREXEC_AUTH_TARGET)
|
||||
|
||||
test-client: examples
|
||||
|
||||
examples: $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(EXAMPLE_SIGN_EVENT_TARGET)
|
||||
|
||||
$(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_MNEMONIC_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_MNEMONIC_INPUT_TARGET): $(TEST_DIR)/test_mnemonic_input.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) -I$(CLIENT_DIR) $(TEST_DIR)/test_mnemonic_input.c -o $(TEST_MNEMONIC_INPUT_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_ROLE_TARGET): $(TEST_DIR)/test_role_table.c $(SRC_DIR)/role_table.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_role_table.c $(SRC_DIR)/role_table.c -o $(TEST_ROLE_TARGET) $(LDFLAGS)
|
||||
@@ -109,13 +138,29 @@ $(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c -o $(TEST_POLICY_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_INTEGRATION_TARGET): $(TEST_DIR)/test_integration.c
|
||||
$(TEST_INTEGRATION_TARGET): $(TEST_DIR)/test_integration.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_integration.c -o $(TEST_INTEGRATION_TARGET) $(LDFLAGS)
|
||||
$(CC) $(CFLAGS) -I$(CLIENT_DIR) $(TEST_DIR)/test_integration.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c -o $(TEST_INTEGRATION_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_SOCKET_NAME_TARGET): $(TEST_DIR)/test_socket_name.c $(SRC_DIR)/socket_name.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_socket_name.c $(SRC_DIR)/socket_name.c -o $(TEST_SOCKET_NAME_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_AUTH_ENVELOPE_TARGET): $(TEST_DIR)/test_auth_envelope.c $(SRC_DIR)/auth_envelope.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_auth_envelope.c $(SRC_DIR)/auth_envelope.c -o $(TEST_AUTH_ENVELOPE_TARGET) $(LDFLAGS)
|
||||
|
||||
$(TEST_QREXEC_AUTH_TARGET): $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envelope.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) $(TEST_DIR)/test_qrexec_auth.c $(SRC_DIR)/auth_envelope.c -o $(TEST_QREXEC_AUTH_TARGET) $(LDFLAGS)
|
||||
|
||||
$(EXAMPLE_GET_PUBLIC_KEY_TARGET): $(EXAMPLES_DIR)/get_public_key_client.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) -I$(CLIENT_DIR) $(EXAMPLES_DIR)/get_public_key_client.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c -o $(EXAMPLE_GET_PUBLIC_KEY_TARGET) $(LDFLAGS)
|
||||
|
||||
$(EXAMPLE_SIGN_EVENT_TARGET): $(EXAMPLES_DIR)/sign_event_client.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) -I$(CLIENT_DIR) $(EXAMPLES_DIR)/sign_event_client.c $(CLIENT_DIR)/nsigner_client.c $(SRC_DIR)/auth_envelope.c -o $(EXAMPLE_SIGN_EVENT_TARGET) $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
||||
39
README.md
39
README.md
@@ -80,6 +80,13 @@ When started, `n_signer` immediately enters terminal input mode:
|
||||
|
||||
No startup files are read or written. The mnemonic — typed or generated — lives only in `mlock`'d memory and is zeroized on shutdown or crash.
|
||||
|
||||
For parent-process launchers, startup can also be non-interactive:
|
||||
|
||||
- `--mnemonic-stdin`: read one mnemonic line from stdin at startup, then continue normally.
|
||||
- `--mnemonic-fd N`: read one mnemonic line from inherited file descriptor `N` at startup.
|
||||
|
||||
These modes avoid putting mnemonic material in argv/environment and are designed for supervised spawners. See [`plans/mnemonic_startup_input.md`](plans/mnemonic_startup_input.md) for the full behavior contract.
|
||||
|
||||
### 3.2 Running phase (status display + signer)
|
||||
|
||||
After unlock, terminal becomes a live status and control console. Example layout:
|
||||
@@ -241,11 +248,14 @@ Discovery:
|
||||
- `nsigner --listen qrexec` is the same stdio framing mode, but caller identity can be derived from `QREXEC_REMOTE_DOMAIN` (displayed as `qubes:<source-vm>`).
|
||||
- `nsigner --listen tcp:IPv4:PORT` or `tcp:[IPv6]:PORT` enables TCP listening for non-AF_UNIX clients (for example `tcp:127.0.0.1:8080`, `tcp:[::]:8080`, or `tcp:[fd00::1234]:8080`).
|
||||
|
||||
### 7.2 ESP32 MCU: USB-CDC serial
|
||||
### 7.2 ESP32 MCU: TinyUSB composite (CDC + WebUSB)
|
||||
|
||||
On MCU targets, the same core modules run with a different transport adapter: USB-CDC serial framing instead of AF_UNIX.
|
||||
On Feather S3 TFT, MCU targets run the same core signer modules behind a TinyUSB composite transport:
|
||||
|
||||
Core logic stays the same; only transport/UI bindings change.
|
||||
- CDC-ACM framed JSON-RPC for serial tooling (`/dev/ttyACM*`)
|
||||
- Vendor/WebUSB framed JSON-RPC for browser tooling
|
||||
|
||||
Core signer logic stays the same; only transport/UI bindings change.
|
||||
|
||||
### 7.3 NIP-46 relay flow (both platforms)
|
||||
|
||||
@@ -269,7 +279,7 @@ Primary deployment is a local, foreground terminal program with abstract namespa
|
||||
|
||||
### 8.2 ESP32 / MCU
|
||||
|
||||
MCU target reuses mnemonic/role/selector/enforcement/dispatcher core and swaps transport/UI for constrained hardware (for example UART/OLED/buttons or USB-CDC host interaction).
|
||||
MCU target reuses mnemonic/role/selector/enforcement/dispatcher core and swaps transport/UI for constrained hardware. The Feather path currently uses TinyUSB composite USB (CDC + WebUSB) plus TFT/buttons for attended approvals.
|
||||
|
||||
### 8.3 Qubes OS qube
|
||||
|
||||
@@ -366,6 +376,7 @@ Build outputs a single executable artifact.
|
||||
- [`build_static.sh`](build_static.sh): musl-static build path
|
||||
- [`Makefile`](Makefile): local build targets
|
||||
- [`Dockerfile.alpine-musl`](Dockerfile.alpine-musl): reproducible Alpine musl toolchain
|
||||
- [`resources/tui_continuous/tui_continuous.h`](resources/tui_continuous/tui_continuous.h) + [`resources/tui_continuous/tui_continuous.c`](resources/tui_continuous/tui_continuous.c): vendored continuous-TUI component (from `~/lt/aesthetics`, API baseline `TUI_CONTINUOUS_VERSION 0.0.9`)
|
||||
- [`increment_and_push.sh`](increment_and_push.sh): version/tag/release workflow
|
||||
- [`src/main.c`](src/main.c): version macros (`NSIGNER_VERSION*`)
|
||||
|
||||
@@ -383,7 +394,21 @@ Static build:
|
||||
./build/nsigner_static_x86_64 --version
|
||||
```
|
||||
|
||||
## 11. Document map
|
||||
## 11. Implemented adjuncts and future work
|
||||
|
||||
### Implemented PoC
|
||||
|
||||
- **MCU / USB signer (Feather ESP32-S3 Reverse TFT).** Working PoC in [`firmware/feather_s3_tft`](firmware/feather_s3_tft). Single TinyUSB composite USB device exposes both CDC-ACM and WebUSB Vendor interfaces. Same dispatcher serves both transports with auth envelope verification, on-device TFT prompts, and physical button approval. See [`firmware/README.md`](firmware/README.md) and [`plans/feather_tinyusb_composite.md`](plans/feather_tinyusb_composite.md).
|
||||
|
||||
### Future work (deferred)
|
||||
|
||||
These items are designed and worth doing, but are not in the current implementation scope. They are listed here so they are not lost.
|
||||
|
||||
- **`--listen http:[addr]:port` mode.** Today the TCP listener speaks 4-byte big-endian length-prefixed framed JSON-RPC, which is correct for low-overhead local IPC but is not directly reachable from web browsers (`fetch`, `XMLHttpRequest`, `curl`). A small additional listener that wraps the same dispatcher in minimal HTTP/1.1 (`POST /rpc`, `Content-Type: application/json`, `Content-Length`-framed body, JSON response) would let standard HTTP clients talk to `nsigner` without any custom framing code. The existing auth envelope (`kind:27235`) and JSON-RPC contract are unchanged; only the outer framing differs. CORS allow on the response would let browser extensions and (with TLS) HTTPS pages reach a remote `nsigner` over FIPS or any other carrier. See the discussion in [`documents/FIPS_DEPLOYMENT.md`](documents/FIPS_DEPLOYMENT.md) section 9 ("Next hardening steps").
|
||||
- **NIP-46 bunker / relay transport.** Tracked in [`plans/nip46_bunker_mode.md`](plans/nip46_bunker_mode.md).
|
||||
- **Browser extension.** Tracked in [`plans/nsigner_browser_extension.md`](plans/nsigner_browser_extension.md). NIP-07 surface forwarding to a running `nsigner` instance.
|
||||
|
||||
## 12. Document map
|
||||
|
||||
- [`README.md`](README.md): authoritative behavior specification for the foreground single-program model
|
||||
- [`documents/CLIENT_IMPLEMENTATION.md`](documents/CLIENT_IMPLEMENTATION.md): client integration contract and framing behavior
|
||||
@@ -391,4 +416,8 @@ Static build:
|
||||
- [`documents/FIPS_DEPLOYMENT.md`](documents/FIPS_DEPLOYMENT.md): Tier-1 FIPS deployment runbook using loopback TCP listener
|
||||
- [`plans/nsigner.md`](plans/nsigner.md): implementation plan and sequencing
|
||||
- [`plans/seed_phrase_uses.md`](plans/seed_phrase_uses.md): seed phrase domain/use catalog and caveats
|
||||
- [`firmware/feather_s3_tft`](firmware/feather_s3_tft): Feather ESP32-S3 Reverse TFT firmware (TinyUSB composite CDC + WebUSB signer PoC)
|
||||
- [`plans/feather_tinyusb_composite.md`](plans/feather_tinyusb_composite.md): firmware Phase 7b plan and outcome (TinyUSB composite USB transport)
|
||||
- [`plans/nsigner_browser_extension.md`](plans/nsigner_browser_extension.md): browser extension exposing NIP-07 over `nsigner`
|
||||
- [`plans/nip46_bunker_mode.md`](plans/nip46_bunker_mode.md): deferred NIP-46 relay-mode signer transport
|
||||
- [`firmware/README.md`](firmware/README.md): firmware-side notes for MCU transport/UI integration
|
||||
|
||||
47
client/README.md
Normal file
47
client/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# n_signer C Reference Client
|
||||
|
||||
This directory provides a minimal copy/paste-friendly C client for `n_signer`.
|
||||
|
||||
## Files
|
||||
|
||||
- `nsigner_client.h` / `nsigner_client.c`:
|
||||
- Unix abstract socket connect helper
|
||||
- length-prefixed frame send/receive
|
||||
- generic request API
|
||||
- helpers for `get_public_key` and `sign_event`
|
||||
- optional TCP auth envelope attachment via `auth_envelope_build_for_request()`
|
||||
|
||||
## API at a glance
|
||||
|
||||
```c
|
||||
nsigner_client_t client;
|
||||
nsigner_client_init(&client);
|
||||
|
||||
nsigner_client_connect_unix(&client, "nsigner", 5000);
|
||||
|
||||
char *resp = NULL;
|
||||
nsigner_client_get_public_key(&client, "1", "", &resp);
|
||||
free(resp);
|
||||
|
||||
nsigner_client_close(&client);
|
||||
```
|
||||
|
||||
## Auth envelope usage
|
||||
|
||||
If transport requires auth envelopes (for TCP mode), set a private key once:
|
||||
|
||||
```c
|
||||
unsigned char privkey[32] = { /* caller key */ };
|
||||
nsigner_client_set_auth(&client, privkey, "example-client");
|
||||
```
|
||||
|
||||
Subsequent requests automatically include an `auth` object.
|
||||
|
||||
## Ownership rules
|
||||
|
||||
- Any `out_response_json` returned by the API must be freed by caller using `free()`.
|
||||
- `nsigner_client_close()` only closes the socket; it does not free the client struct itself.
|
||||
|
||||
## License
|
||||
|
||||
Source files in this directory use SPDX `0BSD` headers for permissive reuse in external projects.
|
||||
369
client/nsigner_client.c
Normal file
369
client/nsigner_client.c
Normal file
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: 0BSD
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include "nsigner_client.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cJSON.h>
|
||||
|
||||
#include "auth_envelope.h"
|
||||
|
||||
#define NSIGNER_CLIENT_MAX_FRAME (65536U)
|
||||
|
||||
static void client_set_error(nsigner_client_t *client, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(client->last_error, sizeof(client->last_error), fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
static int write_full(int fd, const void *buf, size_t len) {
|
||||
const unsigned char *p = (const unsigned char *)buf;
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = write(fd, p + off, len - off);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int read_full(int fd, void *buf, size_t len) {
|
||||
unsigned char *p = (unsigned char *)buf;
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = read(fd, p + off, len - off);
|
||||
if (n == 0) {
|
||||
return -1;
|
||||
}
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_framed(int fd, const char *payload) {
|
||||
uint32_t len;
|
||||
uint32_t be_len;
|
||||
|
||||
if (payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = (uint32_t)strlen(payload);
|
||||
be_len = htonl(len);
|
||||
|
||||
if (write_full(fd, &be_len, sizeof(be_len)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (write_full(fd, payload, len) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int recv_framed(int fd, char **out_payload) {
|
||||
uint32_t be_len;
|
||||
uint32_t len;
|
||||
char *payload;
|
||||
|
||||
if (out_payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_payload = NULL;
|
||||
|
||||
if (read_full(fd, &be_len, sizeof(be_len)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = ntohl(be_len);
|
||||
if (len == 0 || len > NSIGNER_CLIENT_MAX_FRAME) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload = (char *)malloc((size_t)len + 1U);
|
||||
if (payload == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (read_full(fd, payload, len) != 0) {
|
||||
free(payload);
|
||||
return -1;
|
||||
}
|
||||
|
||||
payload[len] = '\0';
|
||||
*out_payload = payload;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sleep_ms(int ms) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (long)(ms % 1000) * 1000000L;
|
||||
return nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
void nsigner_client_init(nsigner_client_t *client) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(client, 0, sizeof(*client));
|
||||
client->fd = -1;
|
||||
client->timeout_ms = 5000;
|
||||
}
|
||||
|
||||
void nsigner_client_close(nsigner_client_t *client) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->fd >= 0) {
|
||||
close(client->fd);
|
||||
client->fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
int nsigner_client_connect_unix(nsigner_client_t *client,
|
||||
const char *socket_name,
|
||||
int timeout_ms) {
|
||||
struct sockaddr_un addr;
|
||||
socklen_t addr_len;
|
||||
int elapsed = 0;
|
||||
|
||||
if (client == NULL || socket_name == NULL || socket_name[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
timeout_ms = (timeout_ms > 0) ? timeout_ms : 5000;
|
||||
nsigner_client_close(client);
|
||||
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sun_family = AF_UNIX;
|
||||
addr.sun_path[0] = '\0';
|
||||
strncpy(&addr.sun_path[1], socket_name, sizeof(addr.sun_path) - 2);
|
||||
addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
|
||||
addr_len = (socklen_t)(sizeof(sa_family_t) + 1 + strlen(socket_name));
|
||||
|
||||
while (elapsed < timeout_ms) {
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
client_set_error(client, "socket failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (connect(fd, (struct sockaddr *)&addr, addr_len) == 0) {
|
||||
client->fd = fd;
|
||||
client->timeout_ms = timeout_ms;
|
||||
client_set_error(client, "ok");
|
||||
return 0;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
sleep_ms(100);
|
||||
elapsed += 100;
|
||||
}
|
||||
|
||||
client_set_error(client, "connect timeout: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int nsigner_client_set_auth(nsigner_client_t *client,
|
||||
const unsigned char privkey[32],
|
||||
const char *label) {
|
||||
if (client == NULL || privkey == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(client->auth_privkey, privkey, 32);
|
||||
client->auth_enabled = 1;
|
||||
|
||||
if (label == NULL) {
|
||||
label = "";
|
||||
}
|
||||
strncpy(client->auth_label, label, sizeof(client->auth_label) - 1);
|
||||
client->auth_label[sizeof(client->auth_label) - 1] = '\0';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *nsigner_client_last_error(const nsigner_client_t *client) {
|
||||
if (client == NULL) {
|
||||
return "invalid_client";
|
||||
}
|
||||
return client->last_error;
|
||||
}
|
||||
|
||||
int nsigner_client_request_raw(nsigner_client_t *client,
|
||||
const char *request_json,
|
||||
char **out_response_json) {
|
||||
char *response_json = NULL;
|
||||
|
||||
if (client == NULL || request_json == NULL || out_response_json == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (client->fd < 0) {
|
||||
client_set_error(client, "not connected");
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_response_json = NULL;
|
||||
|
||||
if (send_framed(client->fd, request_json) != 0) {
|
||||
client_set_error(client, "send failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (recv_framed(client->fd, &response_json) != 0) {
|
||||
client_set_error(client, "receive failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_response_json = response_json;
|
||||
client_set_error(client, "ok");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int nsigner_client_request(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *method,
|
||||
const cJSON *params,
|
||||
char **out_response_json) {
|
||||
cJSON *root = NULL;
|
||||
cJSON *params_copy = NULL;
|
||||
cJSON *auth = NULL;
|
||||
char *request_json = NULL;
|
||||
int rc = -1;
|
||||
|
||||
if (client == NULL || id == NULL || method == NULL || out_response_json == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
root = cJSON_CreateObject();
|
||||
if (root == NULL) {
|
||||
client_set_error(client, "out of memory");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(root, "id", id);
|
||||
cJSON_AddStringToObject(root, "method", method);
|
||||
|
||||
if (params != NULL) {
|
||||
params_copy = cJSON_Duplicate((cJSON *)params, 1);
|
||||
} else {
|
||||
params_copy = cJSON_CreateArray();
|
||||
}
|
||||
|
||||
if (params_copy == NULL) {
|
||||
client_set_error(client, "failed to create params");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddItemToObject(root, "params", params_copy);
|
||||
|
||||
if (client->auth_enabled) {
|
||||
if (auth_envelope_build_for_request(id,
|
||||
method,
|
||||
params_copy,
|
||||
client->auth_privkey,
|
||||
client->auth_label,
|
||||
time(NULL),
|
||||
&auth) != 0) {
|
||||
client_set_error(client, "failed to build auth envelope");
|
||||
goto cleanup;
|
||||
}
|
||||
cJSON_AddItemToObject(root, "auth", auth);
|
||||
auth = NULL;
|
||||
}
|
||||
|
||||
request_json = cJSON_PrintUnformatted(root);
|
||||
if (request_json == NULL) {
|
||||
client_set_error(client, "failed to serialize request");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = nsigner_client_request_raw(client, request_json, out_response_json);
|
||||
|
||||
cleanup:
|
||||
cJSON_Delete(root);
|
||||
cJSON_Delete(auth);
|
||||
free(request_json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nsigner_client_get_public_key(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *selector,
|
||||
char **out_response_json) {
|
||||
cJSON *params = cJSON_CreateArray();
|
||||
int rc;
|
||||
|
||||
if (params == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString((selector != NULL) ? selector : ""));
|
||||
rc = nsigner_client_request(client, id, "get_public_key", params, out_response_json);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nsigner_client_sign_event(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *event_json,
|
||||
const char *role,
|
||||
char **out_response_json) {
|
||||
cJSON *params = cJSON_CreateArray();
|
||||
cJSON *opts = cJSON_CreateObject();
|
||||
int rc;
|
||||
|
||||
if (params == NULL || opts == NULL || event_json == NULL) {
|
||||
cJSON_Delete(params);
|
||||
cJSON_Delete(opts);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
|
||||
if (role != NULL && role[0] != '\0') {
|
||||
cJSON_AddStringToObject(opts, "role", role);
|
||||
}
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
|
||||
rc = nsigner_client_request(client, id, "sign_event", params, out_response_json);
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
56
client/nsigner_client.h
Normal file
56
client/nsigner_client.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: 0BSD
|
||||
*/
|
||||
|
||||
#ifndef NSIGNER_CLIENT_H
|
||||
#define NSIGNER_CLIENT_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cJSON.h>
|
||||
|
||||
typedef struct {
|
||||
int fd;
|
||||
int timeout_ms;
|
||||
unsigned char auth_privkey[32];
|
||||
int auth_enabled;
|
||||
char auth_label[64];
|
||||
char last_error[128];
|
||||
} nsigner_client_t;
|
||||
|
||||
void nsigner_client_init(nsigner_client_t *client);
|
||||
void nsigner_client_close(nsigner_client_t *client);
|
||||
|
||||
int nsigner_client_connect_unix(nsigner_client_t *client,
|
||||
const char *socket_name,
|
||||
int timeout_ms);
|
||||
|
||||
int nsigner_client_set_auth(nsigner_client_t *client,
|
||||
const unsigned char privkey[32],
|
||||
const char *label);
|
||||
|
||||
const char *nsigner_client_last_error(const nsigner_client_t *client);
|
||||
|
||||
int nsigner_client_request_raw(nsigner_client_t *client,
|
||||
const char *request_json,
|
||||
char **out_response_json);
|
||||
|
||||
int nsigner_client_request(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *method,
|
||||
const cJSON *params,
|
||||
char **out_response_json);
|
||||
|
||||
int nsigner_client_get_public_key(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *selector,
|
||||
char **out_response_json);
|
||||
|
||||
int nsigner_client_sign_event(nsigner_client_t *client,
|
||||
const char *id,
|
||||
const char *event_json,
|
||||
const char *role,
|
||||
char **out_response_json);
|
||||
|
||||
#endif
|
||||
@@ -6,14 +6,21 @@ This document is the client-integration spec for `nsigner`.
|
||||
|
||||
It is written for agent/tool authors implementing robust request flows against the local signer process.
|
||||
|
||||
Reference implementation in this repository:
|
||||
|
||||
- Reusable C client library: `client/nsigner_client.h` + `client/nsigner_client.c`
|
||||
- Minimal usage examples: `examples/get_public_key_client.c` and `examples/sign_event_client.c`
|
||||
- Auth envelope builder used by the client: `src/auth_envelope.h` (`auth_envelope_build_for_request`)
|
||||
|
||||
---
|
||||
|
||||
## 2. Discovery and socket targeting
|
||||
|
||||
`nsigner` currently supports two transport families:
|
||||
`nsigner` currently supports three transport families:
|
||||
|
||||
- Linux AF_UNIX **abstract namespace** sockets.
|
||||
- Stdio framed mode (`--listen stdio` and `--listen qrexec`) for one request/response exchange.
|
||||
- TCP framed mode (`--listen tcp:IPv4:PORT` or `--listen tcp:[IPv6]:PORT`).
|
||||
|
||||
For AF_UNIX:
|
||||
|
||||
@@ -54,6 +61,24 @@ In server mode:
|
||||
|
||||
This mode is server-side only in the current CLI (the `client` subcommand still targets AF_UNIX).
|
||||
|
||||
### 2.4 qrexec authentication posture (`--auth`)
|
||||
|
||||
qrexec now supports configurable auth-envelope handling:
|
||||
|
||||
- `--listen qrexec --auth off` (default): legacy behavior. Requests are authorized as `qubes:<vm-name>` only.
|
||||
- `--listen qrexec --auth optional`: if the request includes an `auth` field, it is verified using the same auth envelope rules as TCP. On success, caller identity is upgraded to `qubes:<vm-name>+pubkey:<hex>`.
|
||||
- `--listen qrexec --auth required`: every request must carry a valid `auth` envelope, and caller identity is `qubes:<vm-name>+pubkey:<hex>`.
|
||||
|
||||
When `--auth optional` is used, a malformed or invalid `auth` object is rejected with auth-layer errors (`2010..2017`) rather than silently falling back to `qubes:<vm-name>`.
|
||||
|
||||
### 2.5 TCP mode authentication (required)
|
||||
|
||||
For TCP transport, requests MUST include an `auth` object containing a signed Nostr-style event envelope.
|
||||
|
||||
- Missing `auth` returns `{"error":{"code":2014,"message":"auth_envelope_required"}}`.
|
||||
- Signature verification, method/id/body binding, timestamp skew checks, and replay checks are enforced before policy lookup.
|
||||
- On success, caller identity is normalized to `pubkey:<hex>` for policy checks.
|
||||
|
||||
---
|
||||
|
||||
## 3. Transport framing
|
||||
@@ -147,6 +172,14 @@ Representative error names clients must handle:
|
||||
- `unauthorized`
|
||||
- `approval_denied`
|
||||
- `internal_error`
|
||||
- `auth_envelope_malformed` (2010)
|
||||
- `auth_body_mismatch` (2011)
|
||||
- `auth_signature_invalid` (2012)
|
||||
- `auth_kind_invalid` (2013)
|
||||
- `auth_envelope_required` (2014)
|
||||
- `auth_envelope_mismatch` (2015)
|
||||
- `auth_envelope_stale` (2016)
|
||||
- `auth_replay_detected` (2017)
|
||||
|
||||
### 5.1 Recovery guidance
|
||||
|
||||
@@ -158,6 +191,7 @@ Representative error names clients must handle:
|
||||
- `unauthorized`: caller identity disallowed by policy; do not blind-retry.
|
||||
- `approval_denied`: user rejected prompt; treat as final unless user initiates retry.
|
||||
- `internal_error`: bounded retry with backoff; surface diagnostics.
|
||||
- `auth_envelope_*` / `auth_*` (2010-2017): fix request signing/auth envelope generation; do not blind-retry unchanged payloads.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -49,6 +49,16 @@ Still missing for complete Qubes integration:
|
||||
- qrexec policy in dom0 remains first enforcement boundary.
|
||||
- `n_signer` policy/approval remains second boundary.
|
||||
|
||||
### 3.4 Optional per-program identity inside one qube (`--auth optional`)
|
||||
|
||||
qrexec now supports an optional auth-envelope layer:
|
||||
|
||||
- `nsigner --listen qrexec --auth off` (default): identity remains `qubes:<source-vm>`.
|
||||
- `nsigner --listen qrexec --auth optional`: if a caller includes a valid auth envelope, identity becomes `qubes:<source-vm>+pubkey:<hex>`.
|
||||
- `nsigner --listen qrexec --auth required`: all requests must carry valid envelopes and identity is always `qubes:<source-vm>+pubkey:<hex>`.
|
||||
|
||||
This enables per-program approvals inside a single caller qube while preserving existing behavior for legacy callers that do not emit auth envelopes.
|
||||
|
||||
---
|
||||
|
||||
## 4. Required implementation tasks
|
||||
@@ -102,6 +112,12 @@ In qrexec mode, default prompt behavior is now:
|
||||
|
||||
This replaces the previous permissive `PROMPT_NEVER` temporary setting.
|
||||
|
||||
If qrexec auth envelopes are enabled (`--auth optional` or `--auth required`), approved callers can also be recorded as composite identities:
|
||||
|
||||
- `qubes:<vm>+pubkey:<hex>`
|
||||
|
||||
This gives independent approval rows for distinct programs running in the same source qube.
|
||||
|
||||
## 4.4 Client helper examples ✅ Implemented
|
||||
|
||||
Added:
|
||||
@@ -135,6 +151,7 @@ Includes:
|
||||
- from caller qube, invoke test request (`get_public_key`)
|
||||
- confirm signer qube receives request
|
||||
- confirm activity log displays `qubes:<source-vm>` caller prefix
|
||||
- with `--auth optional`, confirm envelope-enabled clients show `qubes:<source-vm>+pubkey:<hex>`
|
||||
- validate deny behavior from unauthorized qube
|
||||
|
||||
## 5.4 Failure checks
|
||||
|
||||
@@ -21,6 +21,8 @@ The security posture is intentionally minimalist:
|
||||
|
||||
Authoritative behavior reference: [`README.md`](../README.md). Implementation roadmap: [`plans/nsigner.md`](../plans/nsigner.md). Approval model plan: [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md). Wire contract for clients: [`documents/CLIENT_IMPLEMENTATION.md`](CLIENT_IMPLEMENTATION.md).
|
||||
|
||||
Interactive terminal presentation now follows the continuous-TUI conventions from `~/lt/aesthetics/TUI.md`, implemented via vendored [`resources/tui_continuous/tui_continuous.h`](../resources/tui_continuous/tui_continuous.h) / [`resources/tui_continuous/tui_continuous.c`](../resources/tui_continuous/tui_continuous.c) (baseline `TUI_CONTINUOUS_VERSION 0.0.9`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Threat model
|
||||
@@ -49,6 +51,7 @@ Authoritative behavior reference: [`README.md`](../README.md). Implementation ro
|
||||
| The kernel and `mlock` / `getrandom` syscalls | Used for in-memory protection and entropy. |
|
||||
| The static binary itself | Built reproducibly via [`build_static.sh`](../build_static.sh) and shipped as one musl-static artifact. |
|
||||
| The launcher that started `n_signer` | Whoever ran the process chose the `--preapprove` flags. In interactive use that's the human; in `n_OS_tr` boot it's the OS init system. The launcher's integrity is part of the trust chain. |
|
||||
| Parent process (when using `--mnemonic-stdin` / `--mnemonic-fd`) | In non-interactive startup mode, the parent provides mnemonic bytes over stdin/inherited FD. That parent is now explicitly in the trust chain for mnemonic handling correctness and secrecy. |
|
||||
|
||||
Everything else — clients, other processes, other users, remote callers — is **untrusted by default** and must clear an approval check.
|
||||
|
||||
@@ -94,12 +97,13 @@ An identity is a tagged caller record built from the transport. Different transp
|
||||
|---|---|---|
|
||||
| `unix_peer` | `uid`, `pid` from `SO_PEERCRED` on an abstract Unix socket. | Kernel — the client cannot forge it. |
|
||||
| `qubes` | source qube name from `QREXEC_REMOTE_DOMAIN`. | Qubes RPC framework. |
|
||||
| `qubes_pubkey` | source qube + verified auth-envelope pubkey (`qubes:<vm>+pubkey:<hex>`). | Qubes RPC framework + application-layer signature verification. |
|
||||
| `tcp_local` | Loopback address of caller. | TCP socket peer address — opt-in transport. |
|
||||
| `tcp_remote` | Address plus an authenticated pubkey (planned). | Application-layer authentication. |
|
||||
| `tcp_remote` | Verified auth-envelope pubkey (`pubkey:<hex>`). | Application-layer authentication. |
|
||||
| `fips` | Peer npub from a NIP-46 style flow (planned). | Application-layer authentication. |
|
||||
| `usb_serial` | Device path plus an asserted caller (planned). | Trust-on-first-use. |
|
||||
|
||||
Identity quality varies. `unix_peer` and `qubes` are vouched for by the kernel or hypervisor — strong. `tcp_local` and `tcp_remote` are transport-asserted only — only as good as the human at the terminal who chose to approve them.
|
||||
Identity quality varies. `unix_peer` and `qubes` are vouched for by the kernel or hypervisor — strong. `qubes_pubkey` is stronger still for multi-program qubes because it combines hypervisor-vouched source-qube identity with a per-program cryptographic identity. `tcp_local` and `tcp_remote` rely on transport/app-layer checks and are only as strong as deployment choices.
|
||||
|
||||
### Index (or path) — *which key*
|
||||
|
||||
@@ -423,14 +427,16 @@ Different transports give different identity quality. The wire format (4-byte le
|
||||
### 11.2 Stdio / qrexec mode
|
||||
|
||||
- One framed request, one framed response, then exit.
|
||||
- Identity: `qubes:<vm-name>` from `QREXEC_REMOTE_DOMAIN`, vouched for by Qubes.
|
||||
- Identity defaults to `qubes:<vm-name>` from `QREXEC_REMOTE_DOMAIN`, vouched for by Qubes.
|
||||
- With `--listen qrexec --auth optional|required`, requests carrying a valid auth envelope are upgraded to `qubes:<vm-name>+pubkey:<hex>`.
|
||||
- Risk surface: only what Qubes RPC policy permits. See [`packaging/qubes/`](../packaging/qubes/).
|
||||
|
||||
### 11.3 TCP (advanced / opt-in)
|
||||
|
||||
- Loopback addresses produce `tcp_local` identities. Remote addresses produce `tcp_remote` identities.
|
||||
- Identity is **transport-asserted only**. The TCP peer address proves a route, not a principal.
|
||||
- For TCP transports the **interactive approval prompt is the real authentication mechanism**. Do not enable TCP transports without a trustworthy local terminal.
|
||||
- Loopback and remote TCP callers must present a valid signed auth envelope.
|
||||
- Identity is normalized to caller `pubkey:<hex>` after signature verification; TCP peer address is context only.
|
||||
- The auth gate runs before policy: missing/invalid envelopes are rejected with `2010..2017` auth errors.
|
||||
- Prompt-driven approval still applies after auth; auth answers "who is calling", policy answers "is this caller allowed".
|
||||
|
||||
### 11.4 Future transports
|
||||
|
||||
|
||||
BIN
examples/__pycache__/cardputer_sign_event.cpython-313.pyc
Normal file
BIN
examples/__pycache__/cardputer_sign_event.cpython-313.pyc
Normal file
Binary file not shown.
BIN
examples/__pycache__/feather_get_public_key.cpython-313.pyc
Normal file
BIN
examples/__pycache__/feather_get_public_key.cpython-313.pyc
Normal file
Binary file not shown.
136
examples/feather_get_public_key.py
Normal file
136
examples/feather_get_public_key.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
import serial
|
||||
from coincurve import PrivateKey
|
||||
|
||||
|
||||
def read_exact(ser: serial.Serial, n: int, timeout_s: float = 8.0) -> bytes:
|
||||
end = time.time() + timeout_s
|
||||
out = bytearray()
|
||||
while len(out) < n and time.time() < end:
|
||||
chunk = ser.read(n - len(out))
|
||||
if chunk:
|
||||
out.extend(chunk)
|
||||
continue
|
||||
time.sleep(0.005)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def recv_frame(ser: serial.Serial, timeout_s: float = 10.0) -> bytes:
|
||||
end = time.time() + timeout_s
|
||||
window = bytearray()
|
||||
|
||||
while time.time() < end:
|
||||
b = ser.read(1)
|
||||
if not b:
|
||||
time.sleep(0.005)
|
||||
continue
|
||||
window.extend(b)
|
||||
if len(window) > 4:
|
||||
window.pop(0)
|
||||
if len(window) < 4:
|
||||
continue
|
||||
(n,) = struct.unpack(">I", bytes(window))
|
||||
# Keep this cap small to avoid getting stuck on false headers in stream noise.
|
||||
if n == 0 or n > 64 * 1024:
|
||||
continue
|
||||
body = read_exact(ser, n, timeout_s=max(0.2, end - time.time()))
|
||||
if len(body) != n:
|
||||
continue
|
||||
if body[:1] not in (b"{", b"["):
|
||||
continue
|
||||
try:
|
||||
json.loads(body.decode("utf-8", errors="strict"))
|
||||
except Exception:
|
||||
continue
|
||||
return body
|
||||
|
||||
return b""
|
||||
|
||||
|
||||
def build_auth_envelope(method: str, params, caller_priv: bytes, label: str = "py-cli") -> dict:
|
||||
body_json = json.dumps(params, separators=(",", ":")).encode("utf-8")
|
||||
body_hash = hashlib.sha256(body_json).hexdigest()
|
||||
|
||||
sk = PrivateKey(caller_priv)
|
||||
caller_pub_x = sk.public_key.format(compressed=True)[1:].hex()
|
||||
|
||||
created_at = int(time.time())
|
||||
tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", body_hash],
|
||||
]
|
||||
serialized = json.dumps(
|
||||
[0, caller_pub_x, created_at, 27235, tags, label],
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
event_id = hashlib.sha256(serialized).hexdigest()
|
||||
sig = sk.sign_schnorr(bytes.fromhex(event_id), aux_randomness=b"\x00" * 32).hex()
|
||||
|
||||
return {
|
||||
"id": event_id,
|
||||
"pubkey": caller_pub_x,
|
||||
"created_at": created_at,
|
||||
"kind": 27235,
|
||||
"tags": tags,
|
||||
"content": label,
|
||||
"sig": sig,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyACM0"
|
||||
|
||||
caller_priv = bytes(range(1, 33))
|
||||
params = []
|
||||
|
||||
req = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"method": "get_public_key",
|
||||
"params": params,
|
||||
"auth": build_auth_envelope("get_public_key", params, caller_priv),
|
||||
}
|
||||
body = json.dumps(req, separators=(",", ":")).encode("utf-8")
|
||||
frame = struct.pack(">I", len(body)) + body
|
||||
|
||||
ser = serial.Serial()
|
||||
ser.port = port
|
||||
ser.baudrate = 115200
|
||||
ser.timeout = 0.2
|
||||
ser.rtscts = False
|
||||
ser.dsrdtr = False
|
||||
# Set modem-control lines before open to avoid auto-reset pulses.
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
|
||||
with ser:
|
||||
# Let the board settle if OS still caused a reconnect.
|
||||
time.sleep(3.0)
|
||||
|
||||
for attempt in range(1, 4):
|
||||
ser.reset_input_buffer()
|
||||
ser.write(frame)
|
||||
ser.flush()
|
||||
|
||||
resp_body = recv_frame(ser, timeout_s=10.0)
|
||||
if resp_body:
|
||||
print(resp_body.decode("utf-8", errors="replace"))
|
||||
return 0
|
||||
|
||||
print(f"No framed response received (attempt {attempt}/3)")
|
||||
time.sleep(0.5)
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
199
examples/feather_sign_event.py
Normal file
199
examples/feather_sign_event.py
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
import serial
|
||||
from coincurve import PrivateKey, PublicKeyXOnly
|
||||
|
||||
|
||||
def read_exact(ser: serial.Serial, n: int, timeout_s: float = 8.0) -> bytes:
|
||||
end = time.time() + timeout_s
|
||||
out = bytearray()
|
||||
while len(out) < n and time.time() < end:
|
||||
chunk = ser.read(n - len(out))
|
||||
if chunk:
|
||||
out.extend(chunk)
|
||||
continue
|
||||
time.sleep(0.005)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def recv_frame(ser: serial.Serial, timeout_s: float = 10.0) -> bytes:
|
||||
end = time.time() + timeout_s
|
||||
window = bytearray()
|
||||
|
||||
while time.time() < end:
|
||||
b = ser.read(1)
|
||||
if not b:
|
||||
time.sleep(0.005)
|
||||
continue
|
||||
|
||||
window.extend(b)
|
||||
if len(window) > 4:
|
||||
window.pop(0)
|
||||
|
||||
if len(window) < 4:
|
||||
continue
|
||||
|
||||
(n,) = struct.unpack(">I", bytes(window))
|
||||
if n == 0 or n > 1_000_000:
|
||||
continue
|
||||
|
||||
body = read_exact(ser, n, timeout_s=max(0.2, end - time.time()))
|
||||
if len(body) != n:
|
||||
return b""
|
||||
if body[:1] not in (b"{", b"["):
|
||||
continue
|
||||
|
||||
try:
|
||||
json.loads(body.decode("utf-8", errors="strict"))
|
||||
except Exception:
|
||||
# Probably matched a false header inside stream noise; keep scanning.
|
||||
continue
|
||||
|
||||
return body
|
||||
|
||||
return b""
|
||||
|
||||
|
||||
def nostr_serialize(pubkey: str, created_at: int, kind: int, tags: list, content: str) -> bytes:
|
||||
arr = [0, pubkey, created_at, kind, tags, content]
|
||||
return json.dumps(arr, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
|
||||
|
||||
def event_id_hex(event_obj: dict) -> str:
|
||||
ser = nostr_serialize(
|
||||
event_obj["pubkey"],
|
||||
int(event_obj["created_at"]),
|
||||
int(event_obj["kind"]),
|
||||
event_obj["tags"],
|
||||
event_obj["content"],
|
||||
)
|
||||
return hashlib.sha256(ser).hexdigest()
|
||||
|
||||
|
||||
def verify_event(event_obj: dict) -> bool:
|
||||
ev_id = event_id_hex(event_obj)
|
||||
if ev_id != event_obj.get("id"):
|
||||
return False
|
||||
|
||||
pk = PublicKeyXOnly(bytes.fromhex(event_obj["pubkey"]))
|
||||
sig = bytes.fromhex(event_obj["sig"])
|
||||
return pk.verify(sig, bytes.fromhex(ev_id))
|
||||
|
||||
|
||||
def build_auth_envelope(method: str, params, caller_priv: bytes, label: str = "py-cli") -> dict:
|
||||
body_json = json.dumps(params, separators=(",", ":")).encode("utf-8")
|
||||
body_hash = hashlib.sha256(body_json).hexdigest()
|
||||
|
||||
sk = PrivateKey(caller_priv)
|
||||
caller_pub_x = sk.public_key.format(compressed=True)[1:].hex()
|
||||
created_at = int(time.time())
|
||||
tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", body_hash],
|
||||
]
|
||||
serialized = json.dumps(
|
||||
[0, caller_pub_x, created_at, 27235, tags, label],
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8")
|
||||
event_id = hashlib.sha256(serialized).hexdigest()
|
||||
sig = sk.sign_schnorr(bytes.fromhex(event_id), aux_randomness=b"\x00" * 32).hex()
|
||||
|
||||
return {
|
||||
"id": event_id,
|
||||
"pubkey": caller_pub_x,
|
||||
"created_at": created_at,
|
||||
"kind": 27235,
|
||||
"tags": tags,
|
||||
"content": label,
|
||||
"sig": sig,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = [a for a in sys.argv[1:]]
|
||||
no_auth = False
|
||||
if "--no-auth" in args:
|
||||
no_auth = True
|
||||
args.remove("--no-auth")
|
||||
use_alt_caller = "--alt-caller" in args
|
||||
if use_alt_caller:
|
||||
args.remove("--alt-caller")
|
||||
port = args[0] if args else "/dev/ttyACM0"
|
||||
|
||||
caller_priv = bytes(range(1, 33)) if not use_alt_caller else bytes(range(33, 65))
|
||||
|
||||
unsigned_event = {
|
||||
"kind": 1,
|
||||
"created_at": int(time.time()),
|
||||
"tags": [],
|
||||
"content": "hello from feather phase4",
|
||||
}
|
||||
|
||||
params = [unsigned_event]
|
||||
req = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"method": "sign_event",
|
||||
"params": params,
|
||||
}
|
||||
if not no_auth:
|
||||
req["auth"] = build_auth_envelope("sign_event", params, caller_priv)
|
||||
|
||||
body = json.dumps(req, separators=(",", ":")).encode("utf-8")
|
||||
frame = struct.pack(">I", len(body)) + body
|
||||
|
||||
ser = serial.Serial()
|
||||
ser.port = port
|
||||
ser.baudrate = 115200
|
||||
ser.timeout = 0.2
|
||||
ser.rtscts = False
|
||||
ser.dsrdtr = False
|
||||
# Set modem-control lines before open to avoid auto-reset pulses.
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
|
||||
with ser:
|
||||
# Let board fully settle after USB open/reconnect.
|
||||
time.sleep(3.0)
|
||||
ser.reset_input_buffer()
|
||||
ser.write(frame)
|
||||
ser.flush()
|
||||
|
||||
resp_raw = recv_frame(ser, timeout_s=45.0)
|
||||
if not resp_raw:
|
||||
print("No framed response received")
|
||||
return 1
|
||||
|
||||
try:
|
||||
resp = json.loads(resp_raw.decode("utf-8", errors="strict"))
|
||||
except Exception:
|
||||
print("Invalid JSON response bytes:", resp_raw)
|
||||
return 1
|
||||
if "error" in resp:
|
||||
print("RPC error:", json.dumps(resp["error"], separators=(",", ":")))
|
||||
return 1
|
||||
|
||||
result = resp.get("result")
|
||||
if not isinstance(result, dict):
|
||||
print("Invalid RPC result")
|
||||
return 1
|
||||
|
||||
ok = verify_event(result)
|
||||
print(json.dumps(result, separators=(",", ":")))
|
||||
if not ok:
|
||||
print("signature invalid")
|
||||
return 1
|
||||
|
||||
print("✓ signature valid")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
162
examples/feather_webusb_demo.html
Normal file
162
examples/feather_webusb_demo.html
Normal file
@@ -0,0 +1,162 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>n_signer Feather WebUSB Demo</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 16px; max-width: 840px; }
|
||||
button { margin-right: 8px; margin-bottom: 8px; }
|
||||
pre { background: #111; color: #ddd; padding: 12px; border-radius: 6px; overflow: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>n_signer Feather WebUSB Demo</h1>
|
||||
<p>Connect to the ESP32-S3 WebUSB interface, send authenticated <code>get_public_key</code>, and display the result.</p>
|
||||
|
||||
<button id="connectBtn">Connect</button>
|
||||
<button id="pubkeyBtn" disabled>Get Public Key</button>
|
||||
|
||||
<pre id="log"></pre>
|
||||
|
||||
<script type="module">
|
||||
import { schnorr } from "https://esm.sh/@noble/curves@1.5.0/secp256k1?bundle";
|
||||
|
||||
const logEl = document.getElementById("log");
|
||||
const connectBtn = document.getElementById("connectBtn");
|
||||
const pubkeyBtn = document.getElementById("pubkeyBtn");
|
||||
|
||||
let dev = null;
|
||||
let iface = null;
|
||||
const EP_OUT = 1;
|
||||
const EP_IN = 1;
|
||||
|
||||
function log(...args) {
|
||||
logEl.textContent += args.join(" ") + "\n";
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function hex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function utf8(s) {
|
||||
return new TextEncoder().encode(s);
|
||||
}
|
||||
|
||||
async function sha256Hex(dataBytes) {
|
||||
const h = await crypto.subtle.digest("SHA-256", dataBytes);
|
||||
return hex(new Uint8Array(h));
|
||||
}
|
||||
|
||||
function be32(n) {
|
||||
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
|
||||
}
|
||||
|
||||
async function buildAuth(method, params) {
|
||||
const callerPriv = Uint8Array.from({ length: 32 }, (_, i) => i + 1);
|
||||
const callerPubX = hex(schnorr.getPublicKey(callerPriv));
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
const paramsJson = JSON.stringify(params);
|
||||
const bodyHash = await sha256Hex(utf8(paramsJson));
|
||||
const tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", method],
|
||||
["nsigner_body_hash", bodyHash],
|
||||
];
|
||||
const content = "webusb-demo";
|
||||
const ser = JSON.stringify([0, callerPubX, createdAt, 27235, tags, content]);
|
||||
const id = await sha256Hex(utf8(ser));
|
||||
const sigBytes = await schnorr.sign(id, callerPriv, new Uint8Array(32));
|
||||
const sigHex = typeof sigBytes === "string" ? sigBytes : hex(sigBytes);
|
||||
return { id, pubkey: callerPubX, created_at: createdAt, kind: 27235, tags, content, sig: sigHex };
|
||||
}
|
||||
|
||||
async function sendRpc(reqObj) {
|
||||
const body = utf8(JSON.stringify(reqObj));
|
||||
const frame = new Uint8Array(4 + body.length);
|
||||
frame.set(be32(body.length), 0);
|
||||
frame.set(body, 4);
|
||||
await dev.transferOut(EP_OUT, frame);
|
||||
|
||||
const deadline = Date.now() + 10000;
|
||||
let ring = new Uint8Array(0);
|
||||
while (Date.now() < deadline) {
|
||||
const r = await dev.transferIn(EP_IN, 512);
|
||||
if (!r.data || r.data.byteLength === 0) {
|
||||
continue;
|
||||
}
|
||||
const chunk = new Uint8Array(r.data.buffer, r.data.byteOffset, r.data.byteLength);
|
||||
const next = new Uint8Array(ring.length + chunk.length);
|
||||
next.set(ring, 0);
|
||||
next.set(chunk, ring.length);
|
||||
ring = next;
|
||||
|
||||
while (ring.length >= 4) {
|
||||
const n = (ring[0] << 24) | (ring[1] << 16) | (ring[2] << 8) | ring[3];
|
||||
if (n <= 0 || n > 1_000_000) {
|
||||
ring = ring.slice(1);
|
||||
continue;
|
||||
}
|
||||
if (ring.length < 4 + n) {
|
||||
break;
|
||||
}
|
||||
const payload = ring.slice(4, 4 + n);
|
||||
ring = ring.slice(4 + n);
|
||||
const txt = new TextDecoder().decode(payload);
|
||||
return JSON.parse(txt);
|
||||
}
|
||||
}
|
||||
throw new Error("Timed out waiting for framed response");
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
dev = await navigator.usb.requestDevice({ filters: [{ vendorId: 0x303a }] });
|
||||
await dev.open();
|
||||
if (dev.configuration === null) {
|
||||
await dev.selectConfiguration(1);
|
||||
}
|
||||
|
||||
const intf = dev.configuration.interfaces.find(i =>
|
||||
i.alternates.some(a => a.interfaceClass === 0xff)
|
||||
);
|
||||
if (!intf) throw new Error("No vendor WebUSB interface found");
|
||||
iface = intf.interfaceNumber;
|
||||
await dev.claimInterface(iface);
|
||||
const alt = intf.alternates.find(a => a.interfaceClass === 0xff);
|
||||
await dev.selectAlternateInterface(iface, alt.alternateSetting);
|
||||
|
||||
await dev.controlTransferOut({
|
||||
requestType: "class",
|
||||
recipient: "interface",
|
||||
request: 0x22,
|
||||
value: 1,
|
||||
index: iface,
|
||||
});
|
||||
|
||||
pubkeyBtn.disabled = false;
|
||||
log("Connected. Interface", String(iface));
|
||||
}
|
||||
|
||||
connectBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await connect();
|
||||
} catch (e) {
|
||||
log("Connect failed:", String(e));
|
||||
}
|
||||
});
|
||||
|
||||
pubkeyBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const params = [];
|
||||
const auth = await buildAuth("get_public_key", params);
|
||||
const req = { jsonrpc: "2.0", id: "web-1", method: "get_public_key", params, auth };
|
||||
const resp = await sendRpc(req);
|
||||
log("Response:", JSON.stringify(resp));
|
||||
} catch (e) {
|
||||
log("RPC failed:", String(e));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
64
examples/get_pubkey_fips.py
Normal file
64
examples/get_pubkey_fips.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
|
||||
from coincurve import PrivateKey
|
||||
|
||||
HOST = "npub15uqyclnr3er7r8uhka7f0ae2yt4gkjat8gxdan04q0e6xrnwmtjswcyla3.fips"
|
||||
PORT = 8080
|
||||
|
||||
# Demo caller key (32 bytes). Replace with your stable caller key in real use.
|
||||
PRIVKEY = bytes(range(1, 33))
|
||||
|
||||
params = []
|
||||
body_hash = hashlib.sha256(json.dumps(params, separators=(",", ":")).encode()).hexdigest()
|
||||
|
||||
sk = PrivateKey(PRIVKEY)
|
||||
pubkey_x = sk.public_key.format(compressed=False)[1:33].hex()
|
||||
created_at = int(time.time())
|
||||
|
||||
tags = [
|
||||
["nsigner_rpc", "1"],
|
||||
["nsigner_method", "get_public_key"],
|
||||
["nsigner_body_hash", body_hash],
|
||||
]
|
||||
|
||||
content = "py-min"
|
||||
serialized_event = json.dumps(
|
||||
[0, pubkey_x, created_at, 27235, tags, content], separators=(",", ":"), ensure_ascii=False
|
||||
).encode()
|
||||
event_id = hashlib.sha256(serialized_event).hexdigest()
|
||||
sig = sk.sign_schnorr(bytes.fromhex(event_id), aux_randomness=b"\x00" * 32).hex()
|
||||
|
||||
request = {
|
||||
"id": "1",
|
||||
"method": "get_public_key",
|
||||
"params": params,
|
||||
"auth": {
|
||||
"id": event_id,
|
||||
"pubkey": pubkey_x,
|
||||
"created_at": created_at,
|
||||
"kind": 27235,
|
||||
"tags": tags,
|
||||
"content": content,
|
||||
"sig": sig,
|
||||
},
|
||||
}
|
||||
|
||||
payload = json.dumps(request, separators=(",", ":")).encode()
|
||||
frame = struct.pack(">I", len(payload)) + payload
|
||||
|
||||
with socket.create_connection((HOST, PORT), timeout=10) as s:
|
||||
s.sendall(frame)
|
||||
hdr = b""
|
||||
while len(hdr) < 4:
|
||||
hdr += s.recv(4 - len(hdr))
|
||||
ln = struct.unpack(">I", hdr)[0]
|
||||
body = b""
|
||||
while len(body) < ln:
|
||||
body += s.recv(ln - len(body))
|
||||
|
||||
print(json.loads(body.decode())["result"])
|
||||
32
examples/get_public_key_client.c
Normal file
32
examples/get_public_key_client.c
Normal file
@@ -0,0 +1,32 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "../client/nsigner_client.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
nsigner_client_t client;
|
||||
char *response = NULL;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
|
||||
nsigner_client_init(&client);
|
||||
|
||||
if (nsigner_client_connect_unix(&client, socket_name, 5000) != 0) {
|
||||
fprintf(stderr, "connect failed: %s\n", nsigner_client_last_error(&client));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nsigner_client_get_public_key(&client, "example-1", "", &response) != 0) {
|
||||
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(&client));
|
||||
nsigner_client_close(&client);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("%s\n", response);
|
||||
free(response);
|
||||
nsigner_client_close(&client);
|
||||
return 0;
|
||||
}
|
||||
33
examples/sign_event_client.c
Normal file
33
examples/sign_event_client.c
Normal file
@@ -0,0 +1,33 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "../client/nsigner_client.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *socket_name = "nsigner";
|
||||
const char *event_json = "{\"kind\":1,\"content\":\"hello from client example\",\"tags\":[],\"created_at\":1700000000}";
|
||||
nsigner_client_t client;
|
||||
char *response = NULL;
|
||||
|
||||
if (argc > 1 && argv[1] != NULL && argv[1][0] != '\0') {
|
||||
socket_name = argv[1];
|
||||
}
|
||||
|
||||
nsigner_client_init(&client);
|
||||
|
||||
if (nsigner_client_connect_unix(&client, socket_name, 5000) != 0) {
|
||||
fprintf(stderr, "connect failed: %s\n", nsigner_client_last_error(&client));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nsigner_client_sign_event(&client, "example-2", event_json, "main", &response) != 0) {
|
||||
fprintf(stderr, "request failed: %s\n", nsigner_client_last_error(&client));
|
||||
nsigner_client_close(&client);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("%s\n", response);
|
||||
free(response);
|
||||
nsigner_client_close(&client);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,22 +1,75 @@
|
||||
# Firmware Scaffolding
|
||||
# Feather S3 TFT Firmware
|
||||
|
||||
This directory is the placeholder for the future microcontroller variant of `nsigner`.
|
||||
This directory contains the ESP-IDF firmware for the Feather S3 TFT signer target.
|
||||
|
||||
## Current direction
|
||||
## USB transport mode
|
||||
|
||||
Initial MCU work should reuse platform abstractions that already exist in [`nostr_core_lib`](../nostr_core_lib/README.md), especially the ESP32 platform files under [`nostr_core_lib/platform/esp32/`](../nostr_core_lib/platform/esp32):
|
||||
Firmware now runs in **TinyUSB composite mode** (single USB peripheral owned by TinyUSB), exposing:
|
||||
|
||||
- [`nostr_platform_esp32.c`](../nostr_core_lib/platform/esp32/nostr_platform_esp32.c)
|
||||
- [`nostr_http_esp32.c`](../nostr_core_lib/platform/esp32/nostr_http_esp32.c)
|
||||
- [`nostr_websocket_esp32.c`](../nostr_core_lib/platform/esp32/nostr_websocket_esp32.c)
|
||||
- CDC-ACM interface for Python/serial tooling (`/dev/ttyACM*`)
|
||||
- Vendor interface for WebUSB browser transport
|
||||
|
||||
## Intended purpose
|
||||
Expected runtime USB identity:
|
||||
|
||||
This firmware target is for a USB signer and constrained-device signer profile, with `nsigner` protocol compatibility and transport adaptation (USB-CDC first, then other embedded transport options as needed).
|
||||
- VID:PID `303a:4001`
|
||||
- Product string: `Feather S3 TFT n_signer`
|
||||
|
||||
## Next milestones
|
||||
## Flashing workflow (important)
|
||||
|
||||
1. Add MCU-specific build system (ESP-IDF/CMake or equivalent).
|
||||
2. Add a minimal signer main loop with version reporting compatible with desktop `nsigner` semantics.
|
||||
3. Implement transport bridge framing compatible with host-side `nsigner-bridge` plans.
|
||||
4. Add hardware confirmation UX hooks (button + tiny display flow).
|
||||
Because TinyUSB owns USB at runtime, normal app mode is not the ROM USB-Serial/JTAG flashing interface.
|
||||
|
||||
Use this workflow for flashing:
|
||||
|
||||
1. Enter bootloader mode: hold **BOOT**, tap **RESET**, release **BOOT**.
|
||||
2. Confirm bootloader USB device appears (typically `/dev/ttyACM0`).
|
||||
3. Flash:
|
||||
|
||||
```bash
|
||||
cd firmware/feather_s3_tft
|
||||
idf.py -p /dev/ttyACM0 flash
|
||||
```
|
||||
|
||||
4. Exit bootloader and run app: ensure BOOT is released, then tap **RESET** once.
|
||||
5. Confirm app USB identity:
|
||||
|
||||
```bash
|
||||
lsusb | grep -i 303a
|
||||
# expect: 303a:4001 n_signer Feather S3 TFT n_signer
|
||||
```
|
||||
|
||||
## Quick validation
|
||||
|
||||
CDC path:
|
||||
|
||||
```bash
|
||||
./.venv/bin/python ./examples/feather_get_public_key.py /dev/ttyACM0
|
||||
./.venv/bin/python ./examples/feather_sign_event.py /dev/ttyACM0
|
||||
```
|
||||
|
||||
WebUSB path:
|
||||
|
||||
- Open [`examples/feather_webusb_demo.html`](../examples/feather_webusb_demo.html)
|
||||
- Connect in Chrome/Edge
|
||||
- Run `get_public_key`
|
||||
- Confirm pubkey matches CDC result
|
||||
|
||||
## Linux WebUSB host setup (one-time)
|
||||
|
||||
Chrome and Edge need permission to open the device on Linux. Install a udev rule for the firmware VID:PID and reload rules:
|
||||
|
||||
```bash
|
||||
sudo tee /etc/udev/rules.d/99-nsigner-webusb.rules >/dev/null <<'EOF'
|
||||
SUBSYSTEM=="usb", ATTR{idVendor}=="303a", ATTR{idProduct}=="4001", MODE="0666", GROUP="plugdev", TAG+="uaccess"
|
||||
EOF
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger
|
||||
```
|
||||
|
||||
After installing the rule, unplug and replug the device once, then reload the demo page.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If `lsusb` shows `303a:1001`, the board is still in ROM bootloader mode. Tap RESET with BOOT released.
|
||||
- If CDC command hangs, verify app-mode VID:PID is `303a:4001`, not `303a:1001`.
|
||||
- If no `/dev/ttyACM*` appears, unplug/replug after reset and re-check `lsusb`.
|
||||
- If WebUSB connect throws `SecurityError: Access denied`, install the Linux udev rule above and replug.
|
||||
|
||||
Binary file not shown.
4
firmware/feather_s3_tft/CMakeLists.txt
Normal file
4
firmware/feather_s3_tft/CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(feather_s3_tft)
|
||||
1
firmware/feather_s3_tft/components/secp256k1
Submodule
1
firmware/feather_s3_tft/components/secp256k1
Submodule
Submodule firmware/feather_s3_tft/components/secp256k1 added at 7262adb4b4
41
firmware/feather_s3_tft/dependencies.lock
Normal file
41
firmware/feather_s3_tft/dependencies.lock
Normal file
@@ -0,0 +1,41 @@
|
||||
dependencies:
|
||||
espressif/esp_tinyusb:
|
||||
component_hash: 6a50305bc61c7a361da8c0833642be824e92dacb0a6001719a832a4e96e471bf
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
- name: espressif/tinyusb
|
||||
registry_url: https://components.espressif.com
|
||||
require: public
|
||||
version: '>=0.14.2'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.7.6~2
|
||||
espressif/tinyusb:
|
||||
component_hash: a40b7f67df6ac254a4afd96c6401cc56f2059ae747d6d7e66b568bca53ad0edc
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
source:
|
||||
registry_url: https://components.espressif.com
|
||||
type: service
|
||||
targets:
|
||||
- esp32s2
|
||||
- esp32s3
|
||||
- esp32p4
|
||||
- esp32h4
|
||||
- esp32s31
|
||||
version: 0.19.0~3
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 5.4.2
|
||||
direct_dependencies:
|
||||
- espressif/esp_tinyusb
|
||||
- idf
|
||||
manifest_hash: b4dd3f53f0094031d234a3c709d53fc67d628dda73c41ab165960c10a792a8a6
|
||||
target: esp32s3
|
||||
version: 2.0.0
|
||||
20
firmware/feather_s3_tft/main/CMakeLists.txt
Normal file
20
firmware/feather_s3_tft/main/CMakeLists.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"main.c"
|
||||
"display.c"
|
||||
"mnemonic.c"
|
||||
"key_derivation.c"
|
||||
"bech32.c"
|
||||
"usb_transport.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
REQUIRES
|
||||
mbedtls
|
||||
secp256k1
|
||||
json
|
||||
espressif__esp_tinyusb
|
||||
espressif__tinyusb
|
||||
PRIV_REQUIRES
|
||||
esp_driver_gpio
|
||||
esp_driver_spi
|
||||
)
|
||||
139
firmware/feather_s3_tft/main/bech32.c
Normal file
139
firmware/feather_s3_tft/main/bech32.c
Normal file
@@ -0,0 +1,139 @@
|
||||
#include "bech32.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char BECH32_CHARSET[] = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
static uint32_t bech32_polymod_step(uint32_t pre)
|
||||
{
|
||||
const uint8_t b = (uint8_t)(pre >> 25);
|
||||
return ((pre & 0x1FFFFFFu) << 5) ^
|
||||
((-(int32_t)((b >> 0) & 1u)) & 0x3b6a57b2u) ^
|
||||
((-(int32_t)((b >> 1) & 1u)) & 0x26508e6du) ^
|
||||
((-(int32_t)((b >> 2) & 1u)) & 0x1ea119fau) ^
|
||||
((-(int32_t)((b >> 3) & 1u)) & 0x3d4233ddu) ^
|
||||
((-(int32_t)((b >> 4) & 1u)) & 0x2a1462b3u);
|
||||
}
|
||||
|
||||
static int convert_bits(uint8_t *out,
|
||||
size_t *out_len,
|
||||
int out_bits,
|
||||
const uint8_t *in,
|
||||
size_t in_len,
|
||||
int in_bits,
|
||||
int pad)
|
||||
{
|
||||
uint32_t value = 0;
|
||||
int bits = 0;
|
||||
const uint32_t max_v = (((uint32_t)1) << out_bits) - 1;
|
||||
|
||||
*out_len = 0;
|
||||
|
||||
while (in_len-- > 0) {
|
||||
value = (value << in_bits) | *(in++);
|
||||
bits += in_bits;
|
||||
|
||||
while (bits >= out_bits) {
|
||||
bits -= out_bits;
|
||||
out[(*out_len)++] = (uint8_t)((value >> bits) & max_v);
|
||||
}
|
||||
}
|
||||
|
||||
if (pad) {
|
||||
if (bits > 0) {
|
||||
out[(*out_len)++] = (uint8_t)((value << (out_bits - bits)) & max_v);
|
||||
}
|
||||
} else if (((value << (out_bits - bits)) & max_v) != 0u || bits >= in_bits) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int bech32_encode(char *output, size_t output_len, const char *hrp, const uint8_t *data, size_t data_len)
|
||||
{
|
||||
uint32_t chk = 1;
|
||||
size_t hrp_len = strlen(hrp);
|
||||
size_t pos = 0;
|
||||
|
||||
if (output == NULL || hrp == NULL || data == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < hrp_len; ++i) {
|
||||
const unsigned char ch = (unsigned char)hrp[i];
|
||||
if (ch < 33 || ch > 126 || (ch >= 'A' && ch <= 'Z')) {
|
||||
return 0;
|
||||
}
|
||||
chk = bech32_polymod_step(chk) ^ (ch >> 5);
|
||||
}
|
||||
|
||||
chk = bech32_polymod_step(chk);
|
||||
|
||||
for (size_t i = 0; i < hrp_len; ++i) {
|
||||
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
|
||||
if (pos + 1 >= output_len) {
|
||||
return 0;
|
||||
}
|
||||
output[pos++] = hrp[i];
|
||||
}
|
||||
|
||||
if (pos + 1 >= output_len) {
|
||||
return 0;
|
||||
}
|
||||
output[pos++] = '1';
|
||||
|
||||
for (size_t i = 0; i < data_len; ++i) {
|
||||
if ((data[i] >> 5) != 0u) {
|
||||
return 0;
|
||||
}
|
||||
chk = bech32_polymod_step(chk) ^ data[i];
|
||||
if (pos + 1 >= output_len) {
|
||||
return 0;
|
||||
}
|
||||
output[pos++] = BECH32_CHARSET[data[i]];
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 6; ++i) {
|
||||
chk = bech32_polymod_step(chk);
|
||||
}
|
||||
|
||||
chk ^= 1;
|
||||
|
||||
for (size_t i = 0; i < 6; ++i) {
|
||||
if (pos + 1 >= output_len) {
|
||||
return 0;
|
||||
}
|
||||
output[pos++] = BECH32_CHARSET[(chk >> ((5 - i) * 5)) & 0x1f];
|
||||
}
|
||||
|
||||
if (pos >= output_len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
output[pos] = '\0';
|
||||
return 1;
|
||||
}
|
||||
|
||||
int npub_encode(const uint8_t pubkey32[32], char *out, size_t out_len)
|
||||
{
|
||||
uint8_t conv[64] = {0};
|
||||
size_t conv_len = 0;
|
||||
|
||||
if (pubkey32 == NULL || out == NULL || out_len == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!convert_bits(conv, &conv_len, 5, pubkey32, 32, 8, 1)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!bech32_encode(out, out_len, "npub", conv, conv_len)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(conv, 0, sizeof(conv));
|
||||
return 0;
|
||||
}
|
||||
6
firmware/feather_s3_tft/main/bech32.h
Normal file
6
firmware/feather_s3_tft/main/bech32.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
int npub_encode(const uint8_t pubkey32[32], char *out, size_t out_len);
|
||||
323
firmware/feather_s3_tft/main/display.c
Normal file
323
firmware/feather_s3_tft/main/display.c
Normal file
@@ -0,0 +1,323 @@
|
||||
#include "display.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_rom_sys.h"
|
||||
|
||||
#include "font5x7.h"
|
||||
|
||||
#define TAG "display"
|
||||
|
||||
#define PIN_NUM_MOSI 35
|
||||
#define PIN_NUM_CLK 36
|
||||
#define PIN_NUM_CS 42
|
||||
#define PIN_NUM_DC 40
|
||||
#define PIN_NUM_RST 41
|
||||
#define PIN_NUM_BL 45
|
||||
#define PIN_NUM_TFT_POWER 7
|
||||
|
||||
#define LCD_HOST SPI2_HOST
|
||||
|
||||
#define ST7789_SWRESET 0x01
|
||||
#define ST7789_SLPOUT 0x11
|
||||
#define ST7789_COLMOD 0x3A
|
||||
#define ST7789_MADCTL 0x36
|
||||
#define ST7789_CASET 0x2A
|
||||
#define ST7789_RASET 0x2B
|
||||
#define ST7789_RAMWR 0x2C
|
||||
#define ST7789_DISPON 0x29
|
||||
#define ST7789_INVON 0x21
|
||||
|
||||
#define X_OFFSET 40
|
||||
#define Y_OFFSET 52
|
||||
|
||||
#define CHAR_SCALE 2
|
||||
#define CHAR_W (6 * CHAR_SCALE)
|
||||
#define CHAR_H (8 * CHAR_SCALE)
|
||||
|
||||
static spi_device_handle_t s_lcd = NULL;
|
||||
static bool s_initialized = false;
|
||||
|
||||
static esp_err_t lcd_send_cmd(uint8_t cmd)
|
||||
{
|
||||
gpio_set_level(PIN_NUM_DC, 0);
|
||||
spi_transaction_t t = {
|
||||
.length = 8,
|
||||
.tx_buffer = &cmd,
|
||||
};
|
||||
return spi_device_transmit(s_lcd, &t);
|
||||
}
|
||||
|
||||
static esp_err_t lcd_send_data(const void *data, size_t len)
|
||||
{
|
||||
if (len == 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
gpio_set_level(PIN_NUM_DC, 1);
|
||||
spi_transaction_t t = {
|
||||
.length = len * 8,
|
||||
.tx_buffer = data,
|
||||
};
|
||||
return spi_device_transmit(s_lcd, &t);
|
||||
}
|
||||
|
||||
static esp_err_t lcd_set_window(int x, int y, int w, int h)
|
||||
{
|
||||
if (w <= 0 || h <= 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
uint16_t x0 = (uint16_t)(x + X_OFFSET);
|
||||
uint16_t x1 = (uint16_t)(x + w - 1 + X_OFFSET);
|
||||
uint16_t y0 = (uint16_t)(y + Y_OFFSET);
|
||||
uint16_t y1 = (uint16_t)(y + h - 1 + Y_OFFSET);
|
||||
|
||||
uint8_t col[4] = { x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF };
|
||||
uint8_t row[4] = { y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF };
|
||||
|
||||
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_CASET), TAG, "CASET failed");
|
||||
ESP_RETURN_ON_ERROR(lcd_send_data(col, sizeof(col)), TAG, "CASET data failed");
|
||||
|
||||
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_RASET), TAG, "RASET failed");
|
||||
ESP_RETURN_ON_ERROR(lcd_send_data(row, sizeof(row)), TAG, "RASET data failed");
|
||||
|
||||
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_RAMWR), TAG, "RAMWR failed");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t lcd_hw_reset(void)
|
||||
{
|
||||
gpio_set_level(PIN_NUM_RST, 1);
|
||||
esp_rom_delay_us(10000);
|
||||
gpio_set_level(PIN_NUM_RST, 0);
|
||||
esp_rom_delay_us(10000);
|
||||
gpio_set_level(PIN_NUM_RST, 1);
|
||||
esp_rom_delay_us(120000);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t lcd_panel_init(void)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(lcd_hw_reset(), TAG, "HW reset failed");
|
||||
|
||||
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_SWRESET), TAG, "SWRESET failed");
|
||||
esp_rom_delay_us(150000);
|
||||
|
||||
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_SLPOUT), TAG, "SLPOUT failed");
|
||||
esp_rom_delay_us(120000);
|
||||
|
||||
const uint8_t colmod = 0x55; // 16-bit
|
||||
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_COLMOD), TAG, "COLMOD cmd failed");
|
||||
ESP_RETURN_ON_ERROR(lcd_send_data(&colmod, 1), TAG, "COLMOD data failed");
|
||||
esp_rom_delay_us(10000);
|
||||
|
||||
const uint8_t madctl = 0xA0; // MV | MY, landscape for Reverse TFT Feather
|
||||
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_MADCTL), TAG, "MADCTL cmd failed");
|
||||
ESP_RETURN_ON_ERROR(lcd_send_data(&madctl, 1), TAG, "MADCTL data failed");
|
||||
|
||||
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_INVON), TAG, "INVON failed");
|
||||
esp_rom_delay_us(10000);
|
||||
|
||||
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_DISPON), TAG, "DISPON failed");
|
||||
esp_rom_delay_us(120000);
|
||||
|
||||
gpio_set_level(PIN_NUM_BL, 1);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static inline uint16_t clamp_u16_color(uint16_t c)
|
||||
{
|
||||
return (uint16_t)((c >> 8) | (c << 8)); // swap endian for wire order
|
||||
}
|
||||
|
||||
esp_err_t display_init(void)
|
||||
{
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = (1ULL << PIN_NUM_DC) | (1ULL << PIN_NUM_RST) | (1ULL << PIN_NUM_BL) |
|
||||
(1ULL << PIN_NUM_TFT_POWER),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(gpio_config(&io_conf), TAG, "gpio_config failed");
|
||||
|
||||
// Reverse TFT Feather requires TFT/I2C power gate enabled for panel logic.
|
||||
gpio_set_level(PIN_NUM_TFT_POWER, 1);
|
||||
esp_rom_delay_us(10000);
|
||||
|
||||
spi_bus_config_t buscfg = {
|
||||
.mosi_io_num = PIN_NUM_MOSI,
|
||||
.miso_io_num = -1,
|
||||
.sclk_io_num = PIN_NUM_CLK,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = DISPLAY_WIDTH * 20 * sizeof(uint16_t),
|
||||
};
|
||||
|
||||
ESP_RETURN_ON_ERROR(spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO), TAG, "spi_bus_initialize failed");
|
||||
|
||||
spi_device_interface_config_t devcfg = {
|
||||
.clock_speed_hz = 40 * 1000 * 1000,
|
||||
.mode = 0,
|
||||
.spics_io_num = PIN_NUM_CS,
|
||||
.queue_size = 4,
|
||||
};
|
||||
|
||||
ESP_RETURN_ON_ERROR(spi_bus_add_device(LCD_HOST, &devcfg, &s_lcd), TAG, "spi_bus_add_device failed");
|
||||
ESP_RETURN_ON_ERROR(lcd_panel_init(), TAG, "panel init failed");
|
||||
|
||||
s_initialized = true;
|
||||
ESP_RETURN_ON_ERROR(display_clear(RGB565_BLACK), TAG, "clear failed");
|
||||
|
||||
ESP_LOGI(TAG, "ST7789 initialized");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_fill_rect(int x, int y, int w, int h, uint16_t color)
|
||||
{
|
||||
if (!s_initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (x >= DISPLAY_WIDTH || y >= DISPLAY_HEIGHT || w <= 0 || h <= 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (x < 0) {
|
||||
w += x;
|
||||
x = 0;
|
||||
}
|
||||
if (y < 0) {
|
||||
h += y;
|
||||
y = 0;
|
||||
}
|
||||
if (x + w > DISPLAY_WIDTH) {
|
||||
w = DISPLAY_WIDTH - x;
|
||||
}
|
||||
if (y + h > DISPLAY_HEIGHT) {
|
||||
h = DISPLAY_HEIGHT - y;
|
||||
}
|
||||
if (w <= 0 || h <= 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
const uint16_t wire_color = clamp_u16_color(color);
|
||||
const int chunk_pixels = DISPLAY_WIDTH * 8;
|
||||
static uint16_t line_buf[DISPLAY_WIDTH * 8];
|
||||
|
||||
for (int i = 0; i < chunk_pixels; ++i) {
|
||||
line_buf[i] = wire_color;
|
||||
}
|
||||
|
||||
ESP_RETURN_ON_ERROR(lcd_set_window(x, y, w, h), TAG, "set window failed");
|
||||
|
||||
int total = w * h;
|
||||
while (total > 0) {
|
||||
int batch = total > chunk_pixels ? chunk_pixels : total;
|
||||
ESP_RETURN_ON_ERROR(lcd_send_data(line_buf, batch * sizeof(uint16_t)), TAG, "fill data failed");
|
||||
total -= batch;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_clear(uint16_t color)
|
||||
{
|
||||
return display_fill_rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, color);
|
||||
}
|
||||
|
||||
static esp_err_t draw_char(int x, int y, char c, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
if (c < 0x20 || c > 0x7F) {
|
||||
c = '?';
|
||||
}
|
||||
|
||||
const uint8_t *glyph = font5x7[(uint8_t)c - 0x20];
|
||||
|
||||
for (int col = 0; col < 5; ++col) {
|
||||
uint8_t bits = glyph[col];
|
||||
for (int row = 0; row < 7; ++row) {
|
||||
bool on = (bits >> row) & 0x1;
|
||||
ESP_RETURN_ON_ERROR(
|
||||
display_fill_rect(x + (col * CHAR_SCALE), y + (row * CHAR_SCALE), CHAR_SCALE, CHAR_SCALE, on ? fg : bg),
|
||||
TAG,
|
||||
"pixel draw failed");
|
||||
}
|
||||
}
|
||||
|
||||
// spacing column + bottom row
|
||||
ESP_RETURN_ON_ERROR(display_fill_rect(x + (5 * CHAR_SCALE), y, CHAR_SCALE, 7 * CHAR_SCALE, bg), TAG, "spacing failed");
|
||||
ESP_RETURN_ON_ERROR(display_fill_rect(x, y + (7 * CHAR_SCALE), 6 * CHAR_SCALE, CHAR_SCALE, bg), TAG, "baseline failed");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
if (!s_initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (!text) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
int cursor_x = x;
|
||||
int cursor_y = y;
|
||||
|
||||
for (size_t i = 0; text[i] != '\0'; ++i) {
|
||||
char c = text[i];
|
||||
if (c == '\n') {
|
||||
cursor_x = x;
|
||||
cursor_y += CHAR_H;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cursor_x + CHAR_W > DISPLAY_WIDTH) {
|
||||
cursor_x = x;
|
||||
cursor_y += CHAR_H;
|
||||
}
|
||||
if (cursor_y + CHAR_H > DISPLAY_HEIGHT) {
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_RETURN_ON_ERROR(draw_char(cursor_x, cursor_y, c, fg, bg), TAG, "draw_char failed");
|
||||
cursor_x += CHAR_W;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_draw_button_status(int btn_id, bool pressed)
|
||||
{
|
||||
if (btn_id < 0 || btn_id > 2) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const int x = 10;
|
||||
const int y = 30 + btn_id * 32;
|
||||
const int w = DISPLAY_WIDTH - 20;
|
||||
const int h = 24;
|
||||
|
||||
uint16_t box_color = pressed ? RGB565_GREEN : RGB565_RED;
|
||||
uint16_t text_color = RGB565_WHITE;
|
||||
|
||||
char line[32];
|
||||
snprintf(line, sizeof(line), "D%d: %s", btn_id, pressed ? "PRESSED" : "RELEASED");
|
||||
|
||||
ESP_RETURN_ON_ERROR(display_fill_rect(x, y, w, h, box_color), TAG, "status box failed");
|
||||
ESP_RETURN_ON_ERROR(display_draw_text(x + 6, y + 8, line, text_color, box_color), TAG, "status text failed");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
31
firmware/feather_s3_tft/main/display.h
Normal file
31
firmware/feather_s3_tft/main/display.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define DISPLAY_WIDTH 240
|
||||
#define DISPLAY_HEIGHT 135
|
||||
|
||||
#define RGB565_BLACK 0x0000
|
||||
#define RGB565_WHITE 0xFFFF
|
||||
#define RGB565_RED 0xF800
|
||||
#define RGB565_GREEN 0x07E0
|
||||
#define RGB565_BLUE 0x001F
|
||||
#define RGB565_YELLOW 0xFFE0
|
||||
#define RGB565_CYAN 0x07FF
|
||||
#define RGB565_MAGENTA 0xF81F
|
||||
|
||||
esp_err_t display_init(void);
|
||||
esp_err_t display_clear(uint16_t color);
|
||||
esp_err_t display_fill_rect(int x, int y, int w, int h, uint16_t color);
|
||||
esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_button_status(int btn_id, bool pressed);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
103
firmware/feather_s3_tft/main/font5x7.h
Normal file
103
firmware/feather_s3_tft/main/font5x7.h
Normal file
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// 5x7 font, ASCII 0x20..0x7F (printable chars + DEL placeholder)
|
||||
static const uint8_t font5x7[96][5] = {
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00}, // 0x20 ' '
|
||||
{0x00, 0x00, 0x5F, 0x00, 0x00}, // 0x21 '!'
|
||||
{0x00, 0x07, 0x00, 0x07, 0x00}, // 0x22 '"'
|
||||
{0x14, 0x7F, 0x14, 0x7F, 0x14}, // 0x23 '#'
|
||||
{0x24, 0x2A, 0x7F, 0x2A, 0x12}, // 0x24 '$'
|
||||
{0x23, 0x13, 0x08, 0x64, 0x62}, // 0x25 '%'
|
||||
{0x36, 0x49, 0x55, 0x22, 0x50}, // 0x26 '&'
|
||||
{0x00, 0x05, 0x03, 0x00, 0x00}, // 0x27 '\''
|
||||
{0x00, 0x1C, 0x22, 0x41, 0x00}, // 0x28 '('
|
||||
{0x00, 0x41, 0x22, 0x1C, 0x00}, // 0x29 ')'
|
||||
{0x14, 0x08, 0x3E, 0x08, 0x14}, // 0x2A '*'
|
||||
{0x08, 0x08, 0x3E, 0x08, 0x08}, // 0x2B '+'
|
||||
{0x00, 0x50, 0x30, 0x00, 0x00}, // 0x2C ','
|
||||
{0x08, 0x08, 0x08, 0x08, 0x08}, // 0x2D '-'
|
||||
{0x00, 0x60, 0x60, 0x00, 0x00}, // 0x2E '.'
|
||||
{0x20, 0x10, 0x08, 0x04, 0x02}, // 0x2F '/'
|
||||
{0x3E, 0x51, 0x49, 0x45, 0x3E}, // 0x30 '0'
|
||||
{0x00, 0x42, 0x7F, 0x40, 0x00}, // 0x31 '1'
|
||||
{0x42, 0x61, 0x51, 0x49, 0x46}, // 0x32 '2'
|
||||
{0x21, 0x41, 0x45, 0x4B, 0x31}, // 0x33 '3'
|
||||
{0x18, 0x14, 0x12, 0x7F, 0x10}, // 0x34 '4'
|
||||
{0x27, 0x45, 0x45, 0x45, 0x39}, // 0x35 '5'
|
||||
{0x3C, 0x4A, 0x49, 0x49, 0x30}, // 0x36 '6'
|
||||
{0x01, 0x71, 0x09, 0x05, 0x03}, // 0x37 '7'
|
||||
{0x36, 0x49, 0x49, 0x49, 0x36}, // 0x38 '8'
|
||||
{0x06, 0x49, 0x49, 0x29, 0x1E}, // 0x39 '9'
|
||||
{0x00, 0x36, 0x36, 0x00, 0x00}, // 0x3A ':'
|
||||
{0x00, 0x56, 0x36, 0x00, 0x00}, // 0x3B ';'
|
||||
{0x08, 0x14, 0x22, 0x41, 0x00}, // 0x3C '<'
|
||||
{0x14, 0x14, 0x14, 0x14, 0x14}, // 0x3D '='
|
||||
{0x00, 0x41, 0x22, 0x14, 0x08}, // 0x3E '>'
|
||||
{0x02, 0x01, 0x51, 0x09, 0x06}, // 0x3F '?'
|
||||
{0x32, 0x49, 0x79, 0x41, 0x3E}, // 0x40 '@'
|
||||
{0x7E, 0x11, 0x11, 0x11, 0x7E}, // 0x41 'A'
|
||||
{0x7F, 0x49, 0x49, 0x49, 0x36}, // 0x42 'B'
|
||||
{0x3E, 0x41, 0x41, 0x41, 0x22}, // 0x43 'C'
|
||||
{0x7F, 0x41, 0x41, 0x22, 0x1C}, // 0x44 'D'
|
||||
{0x7F, 0x49, 0x49, 0x49, 0x41}, // 0x45 'E'
|
||||
{0x7F, 0x09, 0x09, 0x09, 0x01}, // 0x46 'F'
|
||||
{0x3E, 0x41, 0x49, 0x49, 0x7A}, // 0x47 'G'
|
||||
{0x7F, 0x08, 0x08, 0x08, 0x7F}, // 0x48 'H'
|
||||
{0x00, 0x41, 0x7F, 0x41, 0x00}, // 0x49 'I'
|
||||
{0x20, 0x40, 0x41, 0x3F, 0x01}, // 0x4A 'J'
|
||||
{0x7F, 0x08, 0x14, 0x22, 0x41}, // 0x4B 'K'
|
||||
{0x7F, 0x40, 0x40, 0x40, 0x40}, // 0x4C 'L'
|
||||
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, // 0x4D 'M'
|
||||
{0x7F, 0x04, 0x08, 0x10, 0x7F}, // 0x4E 'N'
|
||||
{0x3E, 0x41, 0x41, 0x41, 0x3E}, // 0x4F 'O'
|
||||
{0x7F, 0x09, 0x09, 0x09, 0x06}, // 0x50 'P'
|
||||
{0x3E, 0x41, 0x51, 0x21, 0x5E}, // 0x51 'Q'
|
||||
{0x7F, 0x09, 0x19, 0x29, 0x46}, // 0x52 'R'
|
||||
{0x46, 0x49, 0x49, 0x49, 0x31}, // 0x53 'S'
|
||||
{0x01, 0x01, 0x7F, 0x01, 0x01}, // 0x54 'T'
|
||||
{0x3F, 0x40, 0x40, 0x40, 0x3F}, // 0x55 'U'
|
||||
{0x1F, 0x20, 0x40, 0x20, 0x1F}, // 0x56 'V'
|
||||
{0x3F, 0x40, 0x38, 0x40, 0x3F}, // 0x57 'W'
|
||||
{0x63, 0x14, 0x08, 0x14, 0x63}, // 0x58 'X'
|
||||
{0x07, 0x08, 0x70, 0x08, 0x07}, // 0x59 'Y'
|
||||
{0x61, 0x51, 0x49, 0x45, 0x43}, // 0x5A 'Z'
|
||||
{0x00, 0x7F, 0x41, 0x41, 0x00}, // 0x5B '['
|
||||
{0x02, 0x04, 0x08, 0x10, 0x20}, // 0x5C '\\'
|
||||
{0x00, 0x41, 0x41, 0x7F, 0x00}, // 0x5D ']'
|
||||
{0x04, 0x02, 0x01, 0x02, 0x04}, // 0x5E '^'
|
||||
{0x40, 0x40, 0x40, 0x40, 0x40}, // 0x5F '_'
|
||||
{0x00, 0x01, 0x02, 0x04, 0x00}, // 0x60 '`'
|
||||
{0x20, 0x54, 0x54, 0x54, 0x78}, // 0x61 'a'
|
||||
{0x7F, 0x48, 0x44, 0x44, 0x38}, // 0x62 'b'
|
||||
{0x38, 0x44, 0x44, 0x44, 0x20}, // 0x63 'c'
|
||||
{0x38, 0x44, 0x44, 0x48, 0x7F}, // 0x64 'd'
|
||||
{0x38, 0x54, 0x54, 0x54, 0x18}, // 0x65 'e'
|
||||
{0x08, 0x7E, 0x09, 0x01, 0x02}, // 0x66 'f'
|
||||
{0x0C, 0x52, 0x52, 0x52, 0x3E}, // 0x67 'g'
|
||||
{0x7F, 0x08, 0x04, 0x04, 0x78}, // 0x68 'h'
|
||||
{0x00, 0x44, 0x7D, 0x40, 0x00}, // 0x69 'i'
|
||||
{0x20, 0x40, 0x44, 0x3D, 0x00}, // 0x6A 'j'
|
||||
{0x7F, 0x10, 0x28, 0x44, 0x00}, // 0x6B 'k'
|
||||
{0x00, 0x41, 0x7F, 0x40, 0x00}, // 0x6C 'l'
|
||||
{0x7C, 0x04, 0x18, 0x04, 0x78}, // 0x6D 'm'
|
||||
{0x7C, 0x08, 0x04, 0x04, 0x78}, // 0x6E 'n'
|
||||
{0x38, 0x44, 0x44, 0x44, 0x38}, // 0x6F 'o'
|
||||
{0x7C, 0x14, 0x14, 0x14, 0x08}, // 0x70 'p'
|
||||
{0x08, 0x14, 0x14, 0x18, 0x7C}, // 0x71 'q'
|
||||
{0x7C, 0x08, 0x04, 0x04, 0x08}, // 0x72 'r'
|
||||
{0x48, 0x54, 0x54, 0x54, 0x20}, // 0x73 's'
|
||||
{0x04, 0x3F, 0x44, 0x40, 0x20}, // 0x74 't'
|
||||
{0x3C, 0x40, 0x40, 0x20, 0x7C}, // 0x75 'u'
|
||||
{0x1C, 0x20, 0x40, 0x20, 0x1C}, // 0x76 'v'
|
||||
{0x3C, 0x40, 0x30, 0x40, 0x3C}, // 0x77 'w'
|
||||
{0x44, 0x28, 0x10, 0x28, 0x44}, // 0x78 'x'
|
||||
{0x0C, 0x50, 0x50, 0x50, 0x3C}, // 0x79 'y'
|
||||
{0x44, 0x64, 0x54, 0x4C, 0x44}, // 0x7A 'z'
|
||||
{0x00, 0x08, 0x36, 0x41, 0x00}, // 0x7B '{'
|
||||
{0x00, 0x00, 0x7F, 0x00, 0x00}, // 0x7C '|'
|
||||
{0x00, 0x41, 0x36, 0x08, 0x00}, // 0x7D '}'
|
||||
{0x08, 0x04, 0x08, 0x10, 0x08}, // 0x7E '~'
|
||||
{0x7F, 0x41, 0x41, 0x41, 0x7F}, // 0x7F DEL placeholder
|
||||
};
|
||||
5
firmware/feather_s3_tft/main/idf_component.yml
Normal file
5
firmware/feather_s3_tft/main/idf_component.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
idf:
|
||||
version: ">=5.0.0"
|
||||
espressif/esp_tinyusb:
|
||||
version: "^1.4.4"
|
||||
245
firmware/feather_s3_tft/main/key_derivation.c
Normal file
245
firmware/feather_s3_tft/main/key_derivation.c
Normal file
@@ -0,0 +1,245 @@
|
||||
#include "key_derivation.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_random.h"
|
||||
|
||||
#include "mbedtls/md.h"
|
||||
|
||||
#include "secp256k1.h"
|
||||
#include "secp256k1_extrakeys.h"
|
||||
#include "secp256k1_schnorrsig.h"
|
||||
|
||||
#define BIP32_HARDENED_FLAG 0x80000000u
|
||||
|
||||
typedef struct {
|
||||
uint8_t priv[32];
|
||||
uint8_t chain[32];
|
||||
uint8_t pub[33];
|
||||
} hd_key_t;
|
||||
|
||||
static int hmac_sha512(const uint8_t *key,
|
||||
size_t key_len,
|
||||
const uint8_t *data,
|
||||
size_t data_len,
|
||||
uint8_t out[64])
|
||||
{
|
||||
const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA512);
|
||||
if (md_info == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mbedtls_md_hmac(md_info, key, key_len, data, data_len, out) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static secp256k1_context *create_context(void)
|
||||
{
|
||||
uint8_t rand32[32] = {0};
|
||||
secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
|
||||
|
||||
if (ctx == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
esp_fill_random(rand32, sizeof(rand32));
|
||||
if (!secp256k1_context_randomize(ctx, rand32)) {
|
||||
memset(rand32, 0, sizeof(rand32));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memset(rand32, 0, sizeof(rand32));
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static int compute_compressed_pubkey(const secp256k1_context *ctx, const uint8_t priv[32], uint8_t pub33[33])
|
||||
{
|
||||
secp256k1_pubkey pubkey;
|
||||
size_t out_len = 33;
|
||||
|
||||
if (!secp256k1_ec_pubkey_create(ctx, &pubkey, priv)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!secp256k1_ec_pubkey_serialize(ctx, pub33, &out_len, &pubkey, SECP256K1_EC_COMPRESSED)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (out_len != 33) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bip32_master_from_seed(const secp256k1_context *ctx, const uint8_t seed[64], hd_key_t *master)
|
||||
{
|
||||
static const uint8_t kBitcoinSeed[] = "Bitcoin seed";
|
||||
uint8_t i64[64] = {0};
|
||||
|
||||
if (hmac_sha512(kBitcoinSeed, sizeof(kBitcoinSeed) - 1, seed, 64, i64) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(master->priv, i64, 32);
|
||||
memcpy(master->chain, i64 + 32, 32);
|
||||
memset(i64, 0, sizeof(i64));
|
||||
|
||||
if (!secp256k1_ec_seckey_verify(ctx, master->priv)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return compute_compressed_pubkey(ctx, master->priv, master->pub);
|
||||
}
|
||||
|
||||
static int bip32_ckd_priv(const secp256k1_context *ctx,
|
||||
const hd_key_t *parent,
|
||||
uint32_t index,
|
||||
hd_key_t *child)
|
||||
{
|
||||
uint8_t data[37] = {0};
|
||||
uint8_t i64[64] = {0};
|
||||
|
||||
if (index & BIP32_HARDENED_FLAG) {
|
||||
data[0] = 0x00;
|
||||
memcpy(data + 1, parent->priv, 32);
|
||||
} else {
|
||||
memcpy(data, parent->pub, 33);
|
||||
}
|
||||
|
||||
data[33] = (uint8_t)((index >> 24) & 0xFF);
|
||||
data[34] = (uint8_t)((index >> 16) & 0xFF);
|
||||
data[35] = (uint8_t)((index >> 8) & 0xFF);
|
||||
data[36] = (uint8_t)(index & 0xFF);
|
||||
|
||||
if (hmac_sha512(parent->chain, 32, data, sizeof(data), i64) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(child->priv, parent->priv, 32);
|
||||
memcpy(child->chain, i64 + 32, 32);
|
||||
|
||||
if (!secp256k1_ec_seckey_tweak_add(ctx, child->priv, i64)) {
|
||||
memset(i64, 0, sizeof(i64));
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(i64, 0, sizeof(i64));
|
||||
|
||||
if (!secp256k1_ec_seckey_verify(ctx, child->priv)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return compute_compressed_pubkey(ctx, child->priv, child->pub);
|
||||
}
|
||||
|
||||
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32])
|
||||
{
|
||||
static const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
1237u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
|
||||
secp256k1_context *ctx = NULL;
|
||||
hd_key_t node;
|
||||
|
||||
if (seed == NULL || privkey == NULL || pubkey == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&node, 0, sizeof(node));
|
||||
|
||||
ctx = create_context();
|
||||
if (ctx == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (bip32_master_from_seed(ctx, seed, &node) != 0) {
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof(path) / sizeof(path[0]); ++i) {
|
||||
hd_key_t next;
|
||||
memset(&next, 0, sizeof(next));
|
||||
|
||||
if (bip32_ckd_priv(ctx, &node, path[i], &next) != 0) {
|
||||
memset(&node, 0, sizeof(node));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&node, 0, sizeof(node));
|
||||
node = next;
|
||||
}
|
||||
|
||||
memcpy(privkey, node.priv, 32);
|
||||
|
||||
{
|
||||
secp256k1_keypair kp;
|
||||
secp256k1_xonly_pubkey xonly;
|
||||
|
||||
if (!secp256k1_keypair_create(ctx, &kp, node.priv)) {
|
||||
memset(&node, 0, sizeof(node));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!secp256k1_keypair_xonly_pub(ctx, &xonly, NULL, &kp)) {
|
||||
memset(&node, 0, sizeof(node));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xonly)) {
|
||||
memset(&node, 0, sizeof(node));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
memset(&node, 0, sizeof(node));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64])
|
||||
{
|
||||
secp256k1_context *ctx = NULL;
|
||||
secp256k1_keypair kp;
|
||||
uint8_t aux[32] = {0};
|
||||
|
||||
if (privkey == NULL || msg32 == NULL || sig64 == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx = create_context();
|
||||
if (ctx == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!secp256k1_keypair_create(ctx, &kp, privkey)) {
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
esp_fill_random(aux, sizeof(aux));
|
||||
if (!secp256k1_schnorrsig_sign32(ctx, sig64, msg32, &kp, aux)) {
|
||||
memset(aux, 0, sizeof(aux));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(aux, 0, sizeof(aux));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return 0;
|
||||
}
|
||||
6
firmware/feather_s3_tft/main/key_derivation.h
Normal file
6
firmware/feather_s3_tft/main/key_derivation.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64]);
|
||||
1005
firmware/feather_s3_tft/main/main.c
Normal file
1005
firmware/feather_s3_tft/main/main.c
Normal file
File diff suppressed because it is too large
Load Diff
148
firmware/feather_s3_tft/main/mnemonic.c
Normal file
148
firmware/feather_s3_tft/main/mnemonic.c
Normal file
@@ -0,0 +1,148 @@
|
||||
#include "mnemonic.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_random.h"
|
||||
|
||||
#include "mbedtls/md.h"
|
||||
#include "mbedtls/pkcs5.h"
|
||||
#include "mbedtls/sha256.h"
|
||||
|
||||
#include "mnemonic_wordlist.h"
|
||||
|
||||
#define MNEMONIC_12_WORDS 12
|
||||
#define ENTROPY_12_BYTES 16
|
||||
#define CHECKSUM_12_BITS 4
|
||||
#define TOTAL_12_BITS (ENTROPY_12_BYTES * 8 + CHECKSUM_12_BITS)
|
||||
|
||||
static bool append_word(char *out, size_t out_len, size_t *cursor, const char *word)
|
||||
{
|
||||
const size_t word_len = strlen(word);
|
||||
|
||||
if (*cursor >= out_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (*cursor != 0) {
|
||||
if (*cursor + 1 >= out_len) {
|
||||
return false;
|
||||
}
|
||||
out[*cursor] = ' ';
|
||||
(*cursor)++;
|
||||
}
|
||||
|
||||
if (*cursor + word_len >= out_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(out + *cursor, word, word_len);
|
||||
*cursor += word_len;
|
||||
out[*cursor] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint8_t get_bit_from_entropy_checksum(const uint8_t entropy[ENTROPY_12_BYTES],
|
||||
const uint8_t checksum[32],
|
||||
size_t bit_index)
|
||||
{
|
||||
if (bit_index < ENTROPY_12_BYTES * 8) {
|
||||
const size_t byte_index = bit_index / 8;
|
||||
const size_t bit_in_byte = 7 - (bit_index % 8);
|
||||
return (uint8_t)((entropy[byte_index] >> bit_in_byte) & 0x01);
|
||||
}
|
||||
|
||||
const size_t checksum_bit_index = bit_index - (ENTROPY_12_BYTES * 8);
|
||||
const size_t bit_in_byte = 7 - checksum_bit_index;
|
||||
return (uint8_t)((checksum[0] >> bit_in_byte) & 0x01);
|
||||
}
|
||||
|
||||
int generate_mnemonic_12(char *out, size_t out_len)
|
||||
{
|
||||
uint8_t entropy[ENTROPY_12_BYTES] = {0};
|
||||
uint8_t hash[32] = {0};
|
||||
size_t cursor = 0;
|
||||
|
||||
if (out == NULL || out_len == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out[0] = '\0';
|
||||
|
||||
esp_fill_random(entropy, sizeof(entropy));
|
||||
mbedtls_sha256(entropy, sizeof(entropy), hash, 0);
|
||||
|
||||
for (size_t w = 0; w < MNEMONIC_12_WORDS; ++w) {
|
||||
const size_t start_bit = w * 11;
|
||||
uint16_t index = 0;
|
||||
|
||||
for (size_t b = 0; b < 11; ++b) {
|
||||
index = (uint16_t)((index << 1) |
|
||||
get_bit_from_entropy_checksum(entropy, hash, start_bit + b));
|
||||
}
|
||||
|
||||
if (index >= 2048) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!append_word(out, out_len, &cursor, BIP39_WORDLIST[index])) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
out[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (TOTAL_12_BITS != 132) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
out[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mnemonic_to_seed(const char *mnemonic, uint8_t seed[64])
|
||||
{
|
||||
const mbedtls_md_info_t *md_info;
|
||||
mbedtls_md_context_t ctx;
|
||||
uint8_t salt[8] = {'m', 'n', 'e', 'm', 'o', 'n', 'i', 'c'};
|
||||
|
||||
if (mnemonic == NULL || seed == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA512);
|
||||
if (md_info == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
mbedtls_md_init(&ctx);
|
||||
if (mbedtls_md_setup(&ctx, md_info, 1) != 0) {
|
||||
mbedtls_md_free(&ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)ctx;
|
||||
if (mbedtls_pkcs5_pbkdf2_hmac_ext(MBEDTLS_MD_SHA512,
|
||||
(const uint8_t *)mnemonic,
|
||||
strlen(mnemonic),
|
||||
salt,
|
||||
sizeof(salt),
|
||||
2048,
|
||||
64,
|
||||
seed) != 0) {
|
||||
mbedtls_md_free(&ctx);
|
||||
return -1;
|
||||
}
|
||||
|
||||
mbedtls_md_free(&ctx);
|
||||
return 0;
|
||||
}
|
||||
7
firmware/feather_s3_tft/main/mnemonic.h
Normal file
7
firmware/feather_s3_tft/main/mnemonic.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
int generate_mnemonic_12(char *out, size_t out_len);
|
||||
int mnemonic_to_seed(const char *mnemonic, uint8_t seed[64]);
|
||||
261
firmware/feather_s3_tft/main/mnemonic_wordlist.h
Normal file
261
firmware/feather_s3_tft/main/mnemonic_wordlist.h
Normal file
@@ -0,0 +1,261 @@
|
||||
#pragma once
|
||||
|
||||
/* BIP-39 English wordlist (2048 words) */
|
||||
static const char *BIP39_WORDLIST[2048] = {
|
||||
"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract",
|
||||
"absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid",
|
||||
"acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual",
|
||||
"adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance",
|
||||
"advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent",
|
||||
"agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album",
|
||||
"alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone",
|
||||
"alpha", "already", "also", "alter", "always", "amateur", "amazing", "among",
|
||||
"amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry",
|
||||
"animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique",
|
||||
"anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april",
|
||||
"arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor",
|
||||
"army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact",
|
||||
"artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume",
|
||||
"asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction",
|
||||
"audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado",
|
||||
"avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis",
|
||||
"baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball",
|
||||
"bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base",
|
||||
"basic", "basket", "battle", "beach", "bean", "beauty", "because", "become",
|
||||
"beef", "before", "begin", "behave", "behind", "believe", "below", "belt",
|
||||
"bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle",
|
||||
"bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black",
|
||||
"blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood",
|
||||
"blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body",
|
||||
"boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring",
|
||||
"borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain",
|
||||
"brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief",
|
||||
"bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother",
|
||||
"brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb",
|
||||
"bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus",
|
||||
"business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable",
|
||||
"cactus", "cage", "cake", "call", "calm", "camera", "camp", "can",
|
||||
"canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable",
|
||||
"capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry",
|
||||
"cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog",
|
||||
"catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling",
|
||||
"celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk",
|
||||
"champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap",
|
||||
"check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child",
|
||||
"chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar",
|
||||
"cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify",
|
||||
"claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff",
|
||||
"climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud",
|
||||
"clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut",
|
||||
"code", "coffee", "coil", "coin", "collect", "color", "column", "combine",
|
||||
"come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm",
|
||||
"congress", "connect", "consider", "control", "convince", "cook", "cool", "copper",
|
||||
"copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch",
|
||||
"country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle",
|
||||
"craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream",
|
||||
"credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop",
|
||||
"cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch",
|
||||
"crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious",
|
||||
"current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad",
|
||||
"damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn",
|
||||
"day", "deal", "debate", "debris", "decade", "december", "decide", "decline",
|
||||
"decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay",
|
||||
"deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend",
|
||||
"deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk",
|
||||
"despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram",
|
||||
"dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital",
|
||||
"dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover",
|
||||
"disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide",
|
||||
"divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain",
|
||||
"donate", "donkey", "donor", "door", "dose", "double", "dove", "draft",
|
||||
"dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill",
|
||||
"drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb",
|
||||
"dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager",
|
||||
"eagle", "early", "earn", "earth", "easily", "east", "easy", "echo",
|
||||
"ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight",
|
||||
"either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator",
|
||||
"elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ",
|
||||
"empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy",
|
||||
"energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough",
|
||||
"enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode",
|
||||
"equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt",
|
||||
"escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil",
|
||||
"evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude",
|
||||
"excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit",
|
||||
"exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend",
|
||||
"extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint",
|
||||
"faith", "fall", "false", "fame", "family", "famous", "fan", "fancy",
|
||||
"fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault",
|
||||
"favorite", "feature", "february", "federal", "fee", "feed", "feel", "female",
|
||||
"fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field",
|
||||
"figure", "file", "film", "filter", "final", "find", "fine", "finger",
|
||||
"finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness",
|
||||
"fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight",
|
||||
"flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly",
|
||||
"foam", "focus", "fog", "foil", "fold", "follow", "food", "foot",
|
||||
"force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil",
|
||||
"foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend",
|
||||
"fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel",
|
||||
"fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy",
|
||||
"gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment",
|
||||
"gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius",
|
||||
"genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle",
|
||||
"ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass",
|
||||
"glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue",
|
||||
"goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip",
|
||||
"govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass",
|
||||
"gravity", "great", "green", "grid", "grief", "grit", "grocery", "group",
|
||||
"grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun",
|
||||
"gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy",
|
||||
"harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard",
|
||||
"head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet",
|
||||
"help", "hen", "hero", "hidden", "high", "hill", "hint", "hip",
|
||||
"hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow",
|
||||
"home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital",
|
||||
"host", "hotel", "hour", "hover", "hub", "huge", "human", "humble",
|
||||
"humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband",
|
||||
"hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill",
|
||||
"illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose",
|
||||
"improve", "impulse", "inch", "include", "income", "increase", "index", "indicate",
|
||||
"indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial",
|
||||
"inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane",
|
||||
"insect", "inside", "inspire", "install", "intact", "interest", "into", "invest",
|
||||
"invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory",
|
||||
"jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel",
|
||||
"job", "join", "joke", "journey", "joy", "judge", "juice", "jump",
|
||||
"jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup",
|
||||
"key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit",
|
||||
"kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know",
|
||||
"lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language",
|
||||
"laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law",
|
||||
"lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave",
|
||||
"lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend",
|
||||
"length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty",
|
||||
"library", "license", "life", "lift", "light", "like", "limb", "limit",
|
||||
"link", "lion", "liquid", "list", "little", "live", "lizard", "load",
|
||||
"loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop",
|
||||
"lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber",
|
||||
"lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet",
|
||||
"maid", "mail", "main", "major", "make", "mammal", "man", "manage",
|
||||
"mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin",
|
||||
"marine", "market", "marriage", "mask", "mass", "master", "match", "material",
|
||||
"math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure",
|
||||
"meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory",
|
||||
"mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message",
|
||||
"metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind",
|
||||
"minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake",
|
||||
"mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment",
|
||||
"monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning",
|
||||
"mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie",
|
||||
"much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music",
|
||||
"must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin",
|
||||
"narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative",
|
||||
"neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral",
|
||||
"never", "news", "next", "nice", "night", "noble", "noise", "nominee",
|
||||
"noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice",
|
||||
"novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey",
|
||||
"object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean",
|
||||
"october", "odor", "off", "offer", "office", "often", "oil", "okay",
|
||||
"old", "olive", "olympic", "omit", "once", "one", "onion", "online",
|
||||
"only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit",
|
||||
"orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich",
|
||||
"other", "outdoor", "outer", "output", "outside", "oval", "oven", "over",
|
||||
"own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page",
|
||||
"pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper",
|
||||
"parade", "parent", "park", "parrot", "party", "pass", "patch", "path",
|
||||
"patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut",
|
||||
"pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper",
|
||||
"perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical",
|
||||
"piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot",
|
||||
"pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet",
|
||||
"plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge",
|
||||
"poem", "poet", "point", "polar", "pole", "police", "pond", "pony",
|
||||
"pool", "popular", "portion", "position", "possible", "post", "potato", "pottery",
|
||||
"poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare",
|
||||
"present", "pretty", "prevent", "price", "pride", "primary", "print", "priority",
|
||||
"prison", "private", "prize", "problem", "process", "produce", "profit", "program",
|
||||
"project", "promote", "proof", "property", "prosper", "protect", "proud", "provide",
|
||||
"public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil",
|
||||
"puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle",
|
||||
"pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz",
|
||||
"quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail",
|
||||
"rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid",
|
||||
"rare", "rate", "rather", "raven", "raw", "razor", "ready", "real",
|
||||
"reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle",
|
||||
"reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject",
|
||||
"relax", "release", "relief", "rely", "remain", "remember", "remind", "remove",
|
||||
"render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report",
|
||||
"require", "rescue", "resemble", "resist", "resource", "response", "result", "retire",
|
||||
"retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib",
|
||||
"ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid",
|
||||
"ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road",
|
||||
"roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room",
|
||||
"rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude",
|
||||
"rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness",
|
||||
"safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same",
|
||||
"sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say",
|
||||
"scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science",
|
||||
"scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea",
|
||||
"search", "season", "seat", "second", "secret", "section", "security", "seed",
|
||||
"seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence",
|
||||
"series", "service", "session", "settle", "setup", "seven", "shadow", "shaft",
|
||||
"shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine",
|
||||
"ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder",
|
||||
"shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side",
|
||||
"siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar",
|
||||
"simple", "since", "sing", "siren", "sister", "situate", "six", "size",
|
||||
"skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab",
|
||||
"slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan",
|
||||
"slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth",
|
||||
"snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social",
|
||||
"sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve",
|
||||
"someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup",
|
||||
"source", "south", "space", "spare", "spatial", "spawn", "speak", "special",
|
||||
"speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin",
|
||||
"spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray",
|
||||
"spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium",
|
||||
"staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay",
|
||||
"steak", "steel", "stem", "step", "stereo", "stick", "still", "sting",
|
||||
"stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street",
|
||||
"strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject",
|
||||
"submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest",
|
||||
"suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme",
|
||||
"sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain",
|
||||
"swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim",
|
||||
"swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table",
|
||||
"tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target",
|
||||
"task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten",
|
||||
"tenant", "tennis", "tent", "term", "test", "text", "thank", "that",
|
||||
"theme", "then", "theory", "there", "they", "thing", "this", "thought",
|
||||
"three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger",
|
||||
"tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title",
|
||||
"toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token",
|
||||
"tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top",
|
||||
"topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist",
|
||||
"toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic",
|
||||
"train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree",
|
||||
"trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy",
|
||||
"trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try",
|
||||
"tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle",
|
||||
"twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical",
|
||||
"ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo",
|
||||
"unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown",
|
||||
"unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon",
|
||||
"upper", "upset", "urban", "urge", "usage", "use", "used", "useful",
|
||||
"useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley",
|
||||
"valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle",
|
||||
"velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very",
|
||||
"vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view",
|
||||
"village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual",
|
||||
"vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote",
|
||||
"voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want",
|
||||
"warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave",
|
||||
"way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding",
|
||||
"weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat",
|
||||
"wheel", "when", "where", "whip", "whisper", "wide", "width", "wife",
|
||||
"wild", "will", "win", "window", "wine", "wing", "wink", "winner",
|
||||
"winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman",
|
||||
"wonder", "wood", "wool", "word", "work", "world", "worry", "worth",
|
||||
"wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year",
|
||||
"yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo"
|
||||
};
|
||||
436
firmware/feather_s3_tft/main/usb_transport.c
Normal file
436
firmware/feather_s3_tft/main/usb_transport.c
Normal file
@@ -0,0 +1,436 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "tinyusb.h"
|
||||
#include "tusb.h"
|
||||
#include "class/vendor/vendor_device.h"
|
||||
|
||||
#include "usb_transport.h"
|
||||
|
||||
#define WEBUSB_VENDOR_REQUEST_WEBUSB 1
|
||||
#define WEBUSB_VENDOR_REQUEST_MICROSOFT 2
|
||||
|
||||
#define CDC_ITF_NUM 0
|
||||
|
||||
#define USB_EP_CDC_OUT 0x02
|
||||
#define USB_EP_CDC_IN 0x82
|
||||
#define USB_EP_CDC_NOTIF 0x83
|
||||
|
||||
#define USB_EP_VENDOR_OUT 0x01
|
||||
#define USB_EP_VENDOR_IN 0x81
|
||||
|
||||
#define USB_MAX_FRAME 4096
|
||||
#define USB_RX_RING_CAP (USB_MAX_FRAME + 32)
|
||||
|
||||
#define USB_BOS_TOTAL_LEN (TUD_BOS_DESC_LEN + TUD_BOS_WEBUSB_DESC_LEN + TUD_BOS_MICROSOFT_OS_DESC_LEN)
|
||||
#define USB_MS_OS_20_DESC_LEN 0xB2
|
||||
|
||||
#define USB_URL "example.com/nsigner/feather"
|
||||
|
||||
enum {
|
||||
ITF_NUM_CDC = 0,
|
||||
ITF_NUM_CDC_DATA,
|
||||
ITF_NUM_VENDOR,
|
||||
ITF_NUM_TOTAL,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint8_t buf[USB_RX_RING_CAP];
|
||||
size_t have;
|
||||
} rx_ring_t;
|
||||
|
||||
static const char *TAG = "usb_transport";
|
||||
|
||||
static bool s_inited = false;
|
||||
static bool s_web_serial_connected = false;
|
||||
|
||||
static rx_ring_t s_cdc_rx;
|
||||
static rx_ring_t s_web_rx;
|
||||
|
||||
static const tusb_desc_device_t s_desc_device = {
|
||||
.bLength = sizeof(tusb_desc_device_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
.bcdUSB = 0x0210,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.idVendor = 0x303A,
|
||||
.idProduct = 0x4001,
|
||||
.bcdDevice = 0x0100,
|
||||
.iManufacturer = 0x01,
|
||||
.iProduct = 0x02,
|
||||
.iSerialNumber = 0x03,
|
||||
.bNumConfigurations = 0x01,
|
||||
};
|
||||
|
||||
static const uint8_t s_desc_configuration[] = {
|
||||
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0,
|
||||
TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_VENDOR_DESC_LEN,
|
||||
0x00, 100),
|
||||
TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, USB_EP_CDC_NOTIF, 16, USB_EP_CDC_OUT, USB_EP_CDC_IN,
|
||||
TUD_OPT_HIGH_SPEED ? 512 : 64),
|
||||
TUD_VENDOR_DESCRIPTOR(ITF_NUM_VENDOR, 5, USB_EP_VENDOR_OUT, USB_EP_VENDOR_IN,
|
||||
TUD_OPT_HIGH_SPEED ? 512 : 64),
|
||||
};
|
||||
|
||||
static const uint8_t s_desc_bos[] = {
|
||||
TUD_BOS_DESCRIPTOR(USB_BOS_TOTAL_LEN, 2),
|
||||
TUD_BOS_WEBUSB_DESCRIPTOR(WEBUSB_VENDOR_REQUEST_WEBUSB, 1),
|
||||
TUD_BOS_MS_OS_20_DESCRIPTOR(USB_MS_OS_20_DESC_LEN, WEBUSB_VENDOR_REQUEST_MICROSOFT),
|
||||
};
|
||||
|
||||
static const uint8_t s_desc_ms_os_20[] = {
|
||||
U16_TO_U8S_LE(0x000A), U16_TO_U8S_LE(MS_OS_20_SET_HEADER_DESCRIPTOR), U32_TO_U8S_LE(0x06030000), U16_TO_U8S_LE(USB_MS_OS_20_DESC_LEN),
|
||||
U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_CONFIGURATION), 0, 0, U16_TO_U8S_LE(USB_MS_OS_20_DESC_LEN - 0x0A),
|
||||
U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_FUNCTION), ITF_NUM_VENDOR, 0,
|
||||
U16_TO_U8S_LE(USB_MS_OS_20_DESC_LEN - 0x0A - 0x08),
|
||||
U16_TO_U8S_LE(0x0014), U16_TO_U8S_LE(MS_OS_20_FEATURE_COMPATBLE_ID), 'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
U16_TO_U8S_LE(USB_MS_OS_20_DESC_LEN - 0x0A - 0x08 - 0x08 - 0x14), U16_TO_U8S_LE(MS_OS_20_FEATURE_REG_PROPERTY),
|
||||
U16_TO_U8S_LE(0x0007), U16_TO_U8S_LE(0x002A),
|
||||
'D', 0x00, 'e', 0x00, 'v', 0x00, 'i', 0x00, 'c', 0x00, 'e', 0x00, 'I', 0x00, 'n', 0x00,
|
||||
't', 0x00, 'e', 0x00, 'r', 0x00, 'f', 0x00, 'a', 0x00, 'c', 0x00, 'e', 0x00, 'G', 0x00,
|
||||
'U', 0x00, 'I', 0x00, 'D', 0x00, 's', 0x00, 0x00, 0x00,
|
||||
U16_TO_U8S_LE(0x0050),
|
||||
'{', 0x00, '9', 0x00, '7', 0x00, '5', 0x00, 'F', 0x00, '4', 0x00, '4', 0x00, 'D', 0x00,
|
||||
'9', 0x00, '-', 0x00, '0', 0x00, 'D', 0x00, '0', 0x00, '8', 0x00, '-', 0x00, '4', 0x00,
|
||||
'3', 0x00, 'F', 0x00, 'D', 0x00, '-', 0x00, '8', 0x00, 'B', 0x00, '3', 0x00, 'E', 0x00,
|
||||
'-', 0x00, '1', 0x00, '2', 0x00, '7', 0x00, 'C', 0x00, 'A', 0x00, '8', 0x00, 'A', 0x00,
|
||||
'F', 0x00, 'F', 0x00, 'F', 0x00, '9', 0x00, 'D', 0x00, '}', 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static const tusb_desc_webusb_url_t s_desc_url = {
|
||||
.bLength = 3 + sizeof(USB_URL) - 1,
|
||||
.bDescriptorType = 3,
|
||||
.bScheme = 1,
|
||||
.url = USB_URL,
|
||||
};
|
||||
|
||||
static const char *s_string_desc_arr[] = {
|
||||
(const char[]) { 0x09, 0x04 },
|
||||
"n_signer",
|
||||
"Feather S3 TFT n_signer",
|
||||
"123456",
|
||||
"n_signer CDC",
|
||||
"n_signer WebUSB",
|
||||
};
|
||||
|
||||
static int parse_frame_from_ring(rx_ring_t *ring, uint8_t *payload, size_t payload_max, size_t *out_len)
|
||||
{
|
||||
if (ring == NULL || payload == NULL || out_len == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ring->have < 4) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
uint32_t len = ((uint32_t)ring->buf[0] << 24) |
|
||||
((uint32_t)ring->buf[1] << 16) |
|
||||
((uint32_t)ring->buf[2] << 8) |
|
||||
(uint32_t)ring->buf[3];
|
||||
|
||||
if (len == 0 || len > payload_max) {
|
||||
memmove(ring->buf, ring->buf + 1, ring->have - 1);
|
||||
ring->have -= 1;
|
||||
if (ring->have < 4) {
|
||||
return 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ring->have < (size_t)len + 4U) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
memcpy(payload, ring->buf + 4, len);
|
||||
*out_len = (size_t)len;
|
||||
|
||||
{
|
||||
size_t rem = ring->have - ((size_t)len + 4U);
|
||||
if (rem > 0) {
|
||||
memmove(ring->buf, ring->buf + 4U + len, rem);
|
||||
}
|
||||
ring->have = rem;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void read_into_ring(rx_ring_t *ring, uint8_t const *data, size_t n)
|
||||
{
|
||||
if (ring == NULL || data == NULL || n == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ring->have + n > sizeof(ring->buf)) {
|
||||
ring->have = 0;
|
||||
}
|
||||
|
||||
memcpy(ring->buf + ring->have, data, n);
|
||||
ring->have += n;
|
||||
}
|
||||
|
||||
static int write_all_cdc(uint8_t itf, const uint8_t *data, size_t len, uint32_t timeout_ms)
|
||||
{
|
||||
size_t off = 0;
|
||||
uint32_t waited = 0;
|
||||
|
||||
while (off < len) {
|
||||
uint32_t avail;
|
||||
uint32_t chunk;
|
||||
uint32_t wrote;
|
||||
|
||||
avail = tud_cdc_n_write_available(itf);
|
||||
if (avail == 0) {
|
||||
if (waited >= timeout_ms) {
|
||||
return -1;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
waited += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
chunk = (uint32_t)(len - off);
|
||||
if (chunk > avail) {
|
||||
chunk = avail;
|
||||
}
|
||||
|
||||
wrote = tud_cdc_n_write(itf, data + off, chunk);
|
||||
if (wrote == 0) {
|
||||
if (waited >= timeout_ms) {
|
||||
return -1;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
waited += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
off += wrote;
|
||||
}
|
||||
|
||||
tud_cdc_n_write_flush(itf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int write_all_vendor(const uint8_t *data, size_t len, uint32_t timeout_ms)
|
||||
{
|
||||
size_t off = 0;
|
||||
uint32_t waited = 0;
|
||||
|
||||
while (off < len) {
|
||||
uint32_t avail;
|
||||
uint32_t chunk;
|
||||
uint32_t wrote;
|
||||
|
||||
if (!tud_ready()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
avail = tud_vendor_write_available();
|
||||
if (avail == 0) {
|
||||
if (waited >= timeout_ms) {
|
||||
return -1;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
waited += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
chunk = (uint32_t)(len - off);
|
||||
if (chunk > avail) {
|
||||
chunk = avail;
|
||||
}
|
||||
|
||||
wrote = tud_vendor_write(data + off, chunk);
|
||||
if (wrote == 0) {
|
||||
if (waited >= timeout_ms) {
|
||||
return -1;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
waited += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
off += wrote;
|
||||
}
|
||||
|
||||
if (tud_vendor_write_flush() == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int usb_transport_init(void)
|
||||
{
|
||||
tinyusb_config_t tusb_cfg = {
|
||||
.device_descriptor = &s_desc_device,
|
||||
.string_descriptor = s_string_desc_arr,
|
||||
.string_descriptor_count = (int)(sizeof(s_string_desc_arr) / sizeof(s_string_desc_arr[0])),
|
||||
.configuration_descriptor = s_desc_configuration,
|
||||
.self_powered = false,
|
||||
.vbus_monitor_io = -1,
|
||||
};
|
||||
|
||||
if (s_inited) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (tinyusb_driver_install(&tusb_cfg) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "tinyusb_driver_install failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&s_cdc_rx, 0, sizeof(s_cdc_rx));
|
||||
memset(&s_web_rx, 0, sizeof(s_web_rx));
|
||||
s_web_serial_connected = false;
|
||||
s_inited = true;
|
||||
|
||||
ESP_LOGI(TAG, "TinyUSB composite transport initialized");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cdc_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len)
|
||||
{
|
||||
uint8_t tmp[128];
|
||||
|
||||
if (payload == NULL || out_len == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!s_inited || !tud_ready()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
while (tud_cdc_n_available(CDC_ITF_NUM) > 0) {
|
||||
uint32_t n = tud_cdc_n_read(CDC_ITF_NUM, tmp, sizeof(tmp));
|
||||
if (n == 0) {
|
||||
break;
|
||||
}
|
||||
read_into_ring(&s_cdc_rx, tmp, n);
|
||||
}
|
||||
|
||||
return parse_frame_from_ring(&s_cdc_rx, payload, payload_max, out_len);
|
||||
}
|
||||
|
||||
int cdc_send_frame(const uint8_t *payload, size_t len)
|
||||
{
|
||||
uint8_t hdr[4];
|
||||
|
||||
if (!s_inited || !tud_ready() || payload == NULL || len == 0 || len > UINT32_MAX) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
hdr[0] = (uint8_t)((len >> 24) & 0xFFU);
|
||||
hdr[1] = (uint8_t)((len >> 16) & 0xFFU);
|
||||
hdr[2] = (uint8_t)((len >> 8) & 0xFFU);
|
||||
hdr[3] = (uint8_t)(len & 0xFFU);
|
||||
|
||||
if (write_all_cdc(CDC_ITF_NUM, hdr, sizeof(hdr), 300) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (write_all_cdc(CDC_ITF_NUM, payload, len, 2000) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int webusb_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len)
|
||||
{
|
||||
uint8_t tmp[128];
|
||||
|
||||
if (payload == NULL || out_len == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!s_inited || !tud_ready()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
while (tud_vendor_available() > 0) {
|
||||
uint32_t n = tud_vendor_read(tmp, sizeof(tmp));
|
||||
if (n == 0) {
|
||||
break;
|
||||
}
|
||||
read_into_ring(&s_web_rx, tmp, n);
|
||||
}
|
||||
|
||||
return parse_frame_from_ring(&s_web_rx, payload, payload_max, out_len);
|
||||
}
|
||||
|
||||
int webusb_send_frame(const uint8_t *payload, size_t len)
|
||||
{
|
||||
uint8_t hdr[4];
|
||||
|
||||
if (!s_inited || !tud_ready() || payload == NULL || len == 0 || len > UINT32_MAX) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
hdr[0] = (uint8_t)((len >> 24) & 0xFFU);
|
||||
hdr[1] = (uint8_t)((len >> 16) & 0xFFU);
|
||||
hdr[2] = (uint8_t)((len >> 8) & 0xFFU);
|
||||
hdr[3] = (uint8_t)(len & 0xFFU);
|
||||
|
||||
if (write_all_vendor(hdr, sizeof(hdr), 300) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (write_all_vendor(payload, len, 2000) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t const *tud_descriptor_bos_cb(void)
|
||||
{
|
||||
return s_desc_bos;
|
||||
}
|
||||
|
||||
bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request)
|
||||
{
|
||||
if (stage != CONTROL_STAGE_SETUP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (request->bmRequestType_bit.type) {
|
||||
case TUSB_REQ_TYPE_VENDOR:
|
||||
if (request->bRequest == WEBUSB_VENDOR_REQUEST_WEBUSB) {
|
||||
return tud_control_xfer(rhport, request, (void *)(uintptr_t)&s_desc_url, s_desc_url.bLength);
|
||||
}
|
||||
if (request->bRequest == WEBUSB_VENDOR_REQUEST_MICROSOFT && request->wIndex == 7) {
|
||||
uint16_t total_len;
|
||||
memcpy(&total_len, s_desc_ms_os_20 + 8, 2);
|
||||
return tud_control_xfer(rhport, request, (void *)(uintptr_t)s_desc_ms_os_20, total_len);
|
||||
}
|
||||
break;
|
||||
|
||||
case TUSB_REQ_TYPE_CLASS:
|
||||
if (request->bRequest == 0x22) {
|
||||
s_web_serial_connected = (request->wValue != 0);
|
||||
return tud_control_status(rhport, request);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void tud_vendor_rx_cb(uint8_t itf, uint8_t const *buffer, uint16_t bufsize)
|
||||
{
|
||||
(void)itf;
|
||||
(void)buffer;
|
||||
(void)bufsize;
|
||||
}
|
||||
12
firmware/feather_s3_tft/main/usb_transport.h
Normal file
12
firmware/feather_s3_tft/main/usb_transport.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
int usb_transport_init(void);
|
||||
|
||||
int cdc_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len);
|
||||
int cdc_send_frame(const uint8_t *payload, size_t len);
|
||||
|
||||
int webusb_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len);
|
||||
int webusb_send_frame(const uint8_t *payload, size_t len);
|
||||
261
firmware/feather_s3_tft/main/webusb_transport.c
Normal file
261
firmware/feather_s3_tft/main/webusb_transport.c
Normal file
@@ -0,0 +1,261 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "tinyusb.h"
|
||||
#include "tusb.h"
|
||||
#include "class/vendor/vendor_device.h"
|
||||
|
||||
#include "webusb_transport.h"
|
||||
|
||||
#define WEBUSB_VENDOR_REQUEST_WEBUSB 1
|
||||
#define WEBUSB_VENDOR_REQUEST_MICROSOFT 2
|
||||
|
||||
#define WEBUSB_EP_OUT 0x01
|
||||
#define WEBUSB_EP_IN 0x81
|
||||
#define WEBUSB_ITF_VENDOR 0
|
||||
#define WEBUSB_ITF_TOTAL 1
|
||||
|
||||
#define WEBUSB_MAX_FRAME 4096
|
||||
#define WEBUSB_RX_RING_CAP (WEBUSB_MAX_FRAME + 16)
|
||||
|
||||
#define WEBUSB_BOS_TOTAL_LEN (TUD_BOS_DESC_LEN + TUD_BOS_WEBUSB_DESC_LEN + TUD_BOS_MICROSOFT_OS_DESC_LEN)
|
||||
#define WEBUSB_MS_OS_20_DESC_LEN 0xB2
|
||||
|
||||
#define WEBUSB_URL "example.com/nsigner/feather"
|
||||
|
||||
static const char *TAG = "webusb";
|
||||
|
||||
static bool s_web_serial_connected = false;
|
||||
static bool s_inited = false;
|
||||
|
||||
static uint8_t s_rx_ring[WEBUSB_RX_RING_CAP];
|
||||
static size_t s_rx_have = 0;
|
||||
|
||||
static const tusb_desc_device_t s_desc_device = {
|
||||
.bLength = sizeof(tusb_desc_device_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
.bcdUSB = 0x0210,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.idVendor = 0x303A,
|
||||
.idProduct = 0x4001,
|
||||
.bcdDevice = 0x0100,
|
||||
.iManufacturer = 0x01,
|
||||
.iProduct = 0x02,
|
||||
.iSerialNumber = 0x03,
|
||||
.bNumConfigurations = 0x01,
|
||||
};
|
||||
|
||||
static const uint8_t s_desc_configuration[] = {
|
||||
TUD_CONFIG_DESCRIPTOR(1, WEBUSB_ITF_TOTAL, 0, TUD_CONFIG_DESC_LEN + TUD_VENDOR_DESC_LEN, 0x00, 100),
|
||||
TUD_VENDOR_DESCRIPTOR(WEBUSB_ITF_VENDOR, 4, WEBUSB_EP_OUT, WEBUSB_EP_IN, TUD_OPT_HIGH_SPEED ? 512 : 64),
|
||||
};
|
||||
|
||||
static const uint8_t s_desc_bos[] = {
|
||||
TUD_BOS_DESCRIPTOR(WEBUSB_BOS_TOTAL_LEN, 2),
|
||||
TUD_BOS_WEBUSB_DESCRIPTOR(WEBUSB_VENDOR_REQUEST_WEBUSB, 1),
|
||||
TUD_BOS_MS_OS_20_DESCRIPTOR(WEBUSB_MS_OS_20_DESC_LEN, WEBUSB_VENDOR_REQUEST_MICROSOFT),
|
||||
};
|
||||
|
||||
static const uint8_t s_desc_ms_os_20[] = {
|
||||
U16_TO_U8S_LE(0x000A), U16_TO_U8S_LE(MS_OS_20_SET_HEADER_DESCRIPTOR), U32_TO_U8S_LE(0x06030000), U16_TO_U8S_LE(WEBUSB_MS_OS_20_DESC_LEN),
|
||||
U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_CONFIGURATION), 0, 0, U16_TO_U8S_LE(WEBUSB_MS_OS_20_DESC_LEN - 0x0A),
|
||||
U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_FUNCTION), WEBUSB_ITF_VENDOR, 0,
|
||||
U16_TO_U8S_LE(WEBUSB_MS_OS_20_DESC_LEN - 0x0A - 0x08),
|
||||
U16_TO_U8S_LE(0x0014), U16_TO_U8S_LE(MS_OS_20_FEATURE_COMPATBLE_ID), 'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
U16_TO_U8S_LE(WEBUSB_MS_OS_20_DESC_LEN - 0x0A - 0x08 - 0x08 - 0x14), U16_TO_U8S_LE(MS_OS_20_FEATURE_REG_PROPERTY),
|
||||
U16_TO_U8S_LE(0x0007), U16_TO_U8S_LE(0x002A),
|
||||
'D', 0x00, 'e', 0x00, 'v', 0x00, 'i', 0x00, 'c', 0x00, 'e', 0x00, 'I', 0x00, 'n', 0x00,
|
||||
't', 0x00, 'e', 0x00, 'r', 0x00, 'f', 0x00, 'a', 0x00, 'c', 0x00, 'e', 0x00, 'G', 0x00,
|
||||
'U', 0x00, 'I', 0x00, 'D', 0x00, 's', 0x00, 0x00, 0x00,
|
||||
U16_TO_U8S_LE(0x0050),
|
||||
'{', 0x00, '9', 0x00, '7', 0x00, '5', 0x00, 'F', 0x00, '4', 0x00, '4', 0x00, 'D', 0x00,
|
||||
'9', 0x00, '-', 0x00, '0', 0x00, 'D', 0x00, '0', 0x00, '8', 0x00, '-', 0x00, '4', 0x00,
|
||||
'3', 0x00, 'F', 0x00, 'D', 0x00, '-', 0x00, '8', 0x00, 'B', 0x00, '3', 0x00, 'E', 0x00,
|
||||
'-', 0x00, '1', 0x00, '2', 0x00, '7', 0x00, 'C', 0x00, 'A', 0x00, '8', 0x00, 'A', 0x00,
|
||||
'F', 0x00, 'F', 0x00, 'F', 0x00, '9', 0x00, 'D', 0x00, '}', 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static const tusb_desc_webusb_url_t s_desc_url = {
|
||||
.bLength = 3 + sizeof(WEBUSB_URL) - 1,
|
||||
.bDescriptorType = 3,
|
||||
.bScheme = 1,
|
||||
.url = WEBUSB_URL,
|
||||
};
|
||||
|
||||
static const char *s_string_desc_arr[] = {
|
||||
(const char[]) { 0x09, 0x04 },
|
||||
"n_signer",
|
||||
"Feather S3 TFT n_signer",
|
||||
"123456",
|
||||
"n_signer WebUSB",
|
||||
};
|
||||
|
||||
bool webusb_transport_connected(void)
|
||||
{
|
||||
return s_web_serial_connected;
|
||||
}
|
||||
|
||||
int webusb_transport_init(void)
|
||||
{
|
||||
tinyusb_config_t tusb_cfg = {
|
||||
.device_descriptor = &s_desc_device,
|
||||
.string_descriptor = s_string_desc_arr,
|
||||
.string_descriptor_count = (int)(sizeof(s_string_desc_arr) / sizeof(s_string_desc_arr[0])),
|
||||
.configuration_descriptor = s_desc_configuration,
|
||||
.self_powered = false,
|
||||
.vbus_monitor_io = -1,
|
||||
};
|
||||
|
||||
if (s_inited) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (tinyusb_driver_install(&tusb_cfg) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "tinyusb_driver_install failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
s_inited = true;
|
||||
s_web_serial_connected = false;
|
||||
s_rx_have = 0;
|
||||
ESP_LOGI(TAG, "WebUSB transport initialized");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int webusb_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len)
|
||||
{
|
||||
uint8_t tmp[128];
|
||||
|
||||
if (payload == NULL || out_len == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!s_inited || !tud_ready()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
while (tud_vendor_available() > 0) {
|
||||
uint32_t n = tud_vendor_read(tmp, sizeof(tmp));
|
||||
if (n == 0) {
|
||||
break;
|
||||
}
|
||||
if (s_rx_have + n > sizeof(s_rx_ring)) {
|
||||
s_rx_have = 0;
|
||||
}
|
||||
memcpy(s_rx_ring + s_rx_have, tmp, n);
|
||||
s_rx_have += n;
|
||||
}
|
||||
|
||||
if (s_rx_have < 4) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
uint32_t len = ((uint32_t)s_rx_ring[0] << 24) |
|
||||
((uint32_t)s_rx_ring[1] << 16) |
|
||||
((uint32_t)s_rx_ring[2] << 8) |
|
||||
(uint32_t)s_rx_ring[3];
|
||||
|
||||
if (len == 0 || len > payload_max) {
|
||||
memmove(s_rx_ring, s_rx_ring + 1, s_rx_have - 1);
|
||||
s_rx_have -= 1;
|
||||
if (s_rx_have < 4) {
|
||||
return 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s_rx_have < (size_t)len + 4U) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
memcpy(payload, s_rx_ring + 4, len);
|
||||
*out_len = (size_t)len;
|
||||
|
||||
{
|
||||
size_t rem = s_rx_have - ((size_t)len + 4U);
|
||||
if (rem > 0) {
|
||||
memmove(s_rx_ring, s_rx_ring + 4U + len, rem);
|
||||
}
|
||||
s_rx_have = rem;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int webusb_send_frame(const uint8_t *payload, size_t len)
|
||||
{
|
||||
uint8_t hdr[4];
|
||||
|
||||
if (!s_inited || !tud_ready() || payload == NULL || len == 0 || len > UINT32_MAX) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
hdr[0] = (uint8_t)((len >> 24) & 0xFFU);
|
||||
hdr[1] = (uint8_t)((len >> 16) & 0xFFU);
|
||||
hdr[2] = (uint8_t)((len >> 8) & 0xFFU);
|
||||
hdr[3] = (uint8_t)(len & 0xFFU);
|
||||
|
||||
if (tud_vendor_write(hdr, sizeof(hdr)) != sizeof(hdr)) {
|
||||
return -1;
|
||||
}
|
||||
if (tud_vendor_write(payload, len) != len) {
|
||||
return -1;
|
||||
}
|
||||
if (tud_vendor_write_flush() == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t const *tud_descriptor_bos_cb(void)
|
||||
{
|
||||
return s_desc_bos;
|
||||
}
|
||||
|
||||
bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request)
|
||||
{
|
||||
if (stage != CONTROL_STAGE_SETUP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (request->bmRequestType_bit.type) {
|
||||
case TUSB_REQ_TYPE_VENDOR:
|
||||
if (request->bRequest == WEBUSB_VENDOR_REQUEST_WEBUSB) {
|
||||
return tud_control_xfer(rhport, request, (void *)(uintptr_t)&s_desc_url, s_desc_url.bLength);
|
||||
}
|
||||
if (request->bRequest == WEBUSB_VENDOR_REQUEST_MICROSOFT && request->wIndex == 7) {
|
||||
uint16_t total_len;
|
||||
memcpy(&total_len, s_desc_ms_os_20 + 8, 2);
|
||||
return tud_control_xfer(rhport, request, (void *)(uintptr_t)s_desc_ms_os_20, total_len);
|
||||
}
|
||||
break;
|
||||
|
||||
case TUSB_REQ_TYPE_CLASS:
|
||||
if (request->bRequest == 0x22) {
|
||||
s_web_serial_connected = (request->wValue != 0);
|
||||
return tud_control_status(rhport, request);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void tud_vendor_rx_cb(uint8_t itf, uint8_t const *buffer, uint16_t bufsize)
|
||||
{
|
||||
(void)itf;
|
||||
(void)buffer;
|
||||
(void)bufsize;
|
||||
}
|
||||
10
firmware/feather_s3_tft/main/webusb_transport.h
Normal file
10
firmware/feather_s3_tft/main/webusb_transport.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
int webusb_transport_init(void);
|
||||
int webusb_recv_frame(uint8_t *payload, size_t payload_max, size_t *out_len);
|
||||
int webusb_send_frame(const uint8_t *payload, size_t len);
|
||||
bool webusb_transport_connected(void);
|
||||
@@ -0,0 +1 @@
|
||||
6a50305bc61c7a361da8c0833642be824e92dacb0a6001719a832a4e96e471bf
|
||||
@@ -0,0 +1,107 @@
|
||||
## 1.7.6~2
|
||||
|
||||
- esp_tinyusb: Added support for IDF 6.0 after removal of the USB component
|
||||
|
||||
## 1.7.6~1
|
||||
|
||||
- esp_tinyusb: Added documentation to README.md
|
||||
|
||||
## 1.7.6
|
||||
|
||||
- MSC: Fixed the possibility to use SD/MMC storage with large capacity (more than 4 GB)
|
||||
|
||||
## 1.7.5
|
||||
|
||||
- esp_tinyusb: Provide forward compatibility with IDF 6.0
|
||||
|
||||
## 1.7.4
|
||||
|
||||
- MSC: WL Sector runtime check during spiflash init (fix for build time error check)
|
||||
|
||||
## 1.7.3 [yanked]
|
||||
|
||||
- MSC: Improved transfer speed to SD cards and SPI flash
|
||||
|
||||
## 1.7.2
|
||||
|
||||
- esp_tinyusb: Fixed crash on logging from ISR
|
||||
- PHY: Fixed crash with external_phy=true configuration
|
||||
|
||||
## 1.7.1
|
||||
|
||||
- NCM: Changed default NTB config to decrease DRAM memory usage (fix for DRAM overflow on ESP32S2)
|
||||
|
||||
## 1.7.0 [yanked]
|
||||
|
||||
- NCM: Added possibility to configure NCM Transfer Blocks (NTB) via menuconfig
|
||||
- esp_tinyusb: Added option to select TinyUSB peripheral on esp32p4 via menuconfig (USB_PHY_SUPPORTS_P4_OTG11 in esp-idf is required)
|
||||
- esp_tinyusb: Fixed uninstall tinyusb driver with not default task configuration
|
||||
|
||||
## 1.6.0
|
||||
|
||||
- CDC-ACM: Fixed memory leak on deinit
|
||||
- esp_tinyusb: Added Teardown
|
||||
|
||||
## 1.5.0
|
||||
|
||||
- esp_tinyusb: Added DMA mode option to tinyusb DCD DWC2 configuration
|
||||
- esp_tinyusb: Changed the default affinity mask of the task to CPU1
|
||||
|
||||
## 1.4.5
|
||||
|
||||
- CDC-ACM: Fixed memory leak at VFS unregister
|
||||
- Vendor specific: Provided default configuration
|
||||
|
||||
## 1.4.4
|
||||
|
||||
- esp_tinyusb: Added HighSpeed and Qualifier device descriptors in tinyusb configuration
|
||||
- CDC-ACM: Removed MIN() definition if already defined
|
||||
- MSC: Fixed EP size selecting in default configuration descriptor
|
||||
|
||||
## 1.4.3
|
||||
|
||||
- esp_tinyusb: Added ESP32P4 support (HS only)
|
||||
|
||||
## 1.4.2
|
||||
|
||||
- MSC: Fixed maximum files open
|
||||
- Added uninstall function
|
||||
|
||||
## 1.4.0
|
||||
|
||||
- MSC: Fixed integer overflows
|
||||
- CDC-ACM: Removed intermediate RX ringbuffer
|
||||
- CDC-ACM: Increased default FIFO size to 512 bytes
|
||||
- CDC-ACM: Fixed Virtual File System binding
|
||||
|
||||
## 1.3.0
|
||||
|
||||
- Added NCM extension
|
||||
|
||||
## 1.2.1 - 1.2.2
|
||||
|
||||
- Minor bugfixes
|
||||
|
||||
## 1.2.0
|
||||
|
||||
- Added MSC extension for accessing SPI Flash on memory card https://github.com/espressif/idf-extra-components/commit/a8c00d7707ba4ceeb0970c023d702c7768dba3dc
|
||||
|
||||
## 1.1.0
|
||||
|
||||
- Added support for NCM, ECM/RNDIS, DFU and Bluetooth TinyUSB drivers https://github.com/espressif/idf-extra-components/commit/79f35c9b047b583080f93a63310e2ee7d82ef17b
|
||||
|
||||
## 1.0.4
|
||||
|
||||
- Cleaned up string descriptors handling https://github.com/espressif/idf-extra-components/commit/046cc4b02f524d5c7e3e56480a473cfe844dc3d6
|
||||
|
||||
## 1.0.2 - 1.0.3
|
||||
|
||||
- Minor bugfixes
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- CDC-ACM: Return ESP_OK if there is nothing to flush https://github.com/espressif/idf-extra-components/commit/388ff32eb09aa572d98c54cb355f1912ce42707c
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial version based on [esp-idf v4.4.3](https://github.com/espressif/esp-idf/tree/v4.4.3/components/tinyusb)
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,59 @@
|
||||
set(srcs
|
||||
"descriptors_control.c"
|
||||
"tinyusb.c"
|
||||
"usb_descriptors.c"
|
||||
)
|
||||
|
||||
set(priv_req "")
|
||||
|
||||
if(${IDF_VERSION_MAJOR} LESS 6)
|
||||
list(APPEND priv_req "usb")
|
||||
endif()
|
||||
|
||||
if(NOT CONFIG_TINYUSB_NO_DEFAULT_TASK)
|
||||
list(APPEND srcs "tusb_tasks.c")
|
||||
endif() # CONFIG_TINYUSB_NO_DEFAULT_TASK
|
||||
|
||||
if(CONFIG_TINYUSB_CDC_ENABLED)
|
||||
list(APPEND srcs
|
||||
"cdc.c"
|
||||
"tusb_cdc_acm.c"
|
||||
)
|
||||
if(CONFIG_VFS_SUPPORT_IO)
|
||||
list(APPEND srcs
|
||||
"tusb_console.c"
|
||||
"vfs_tinyusb.c"
|
||||
)
|
||||
endif() # CONFIG_VFS_SUPPORT_IO
|
||||
endif() # CONFIG_TINYUSB_CDC_ENABLED
|
||||
|
||||
if(CONFIG_TINYUSB_MSC_ENABLED)
|
||||
list(APPEND srcs
|
||||
tusb_msc_storage.c
|
||||
)
|
||||
endif() # CONFIG_TINYUSB_MSC_ENABLED
|
||||
|
||||
if(CONFIG_TINYUSB_NET_MODE_NCM)
|
||||
list(APPEND srcs
|
||||
tinyusb_net.c
|
||||
)
|
||||
endif() # CONFIG_TINYUSB_NET_MODE_NCM
|
||||
|
||||
idf_component_register(SRCS ${srcs}
|
||||
INCLUDE_DIRS "include"
|
||||
PRIV_INCLUDE_DIRS "include_private"
|
||||
PRIV_REQUIRES ${priv_req}
|
||||
REQUIRES fatfs vfs
|
||||
)
|
||||
|
||||
# Determine whether tinyusb is fetched from component registry or from local path
|
||||
idf_build_get_property(build_components BUILD_COMPONENTS)
|
||||
if(tinyusb IN_LIST build_components)
|
||||
set(tinyusb_name tinyusb) # Local component
|
||||
else()
|
||||
set(tinyusb_name espressif__tinyusb) # Managed component
|
||||
endif()
|
||||
|
||||
# Pass tusb_config.h from this component to TinyUSB
|
||||
idf_component_get_property(tusb_lib ${tinyusb_name} COMPONENT_LIB)
|
||||
target_include_directories(${tusb_lib} PRIVATE "include")
|
||||
@@ -0,0 +1,379 @@
|
||||
menu "TinyUSB Stack"
|
||||
config TINYUSB_DEBUG_LEVEL
|
||||
int "TinyUSB log level (0-3)"
|
||||
default 1
|
||||
range 0 3
|
||||
help
|
||||
Specify verbosity of TinyUSB log output.
|
||||
|
||||
choice TINYUSB_RHPORT
|
||||
prompt "USB Peripheral"
|
||||
default TINYUSB_RHPORT_HS if IDF_TARGET_ESP32P4
|
||||
default TINYUSB_RHPORT_FS
|
||||
help
|
||||
Allows set the USB Peripheral Controller for TinyUSB.
|
||||
|
||||
- High-speed (USB OTG2.0 Peripheral for High-, Full- and Low-speed)
|
||||
- Full-speed (USB OTG1.1 Peripheral for Full- and Low-speed)
|
||||
|
||||
config TINYUSB_RHPORT_HS
|
||||
bool "OTG2.0"
|
||||
depends on IDF_TARGET_ESP32P4
|
||||
config TINYUSB_RHPORT_FS
|
||||
bool "OTG1.1"
|
||||
endchoice
|
||||
|
||||
menu "TinyUSB DCD"
|
||||
choice TINYUSB_MODE
|
||||
prompt "DCD Mode"
|
||||
default TINYUSB_MODE_DMA
|
||||
help
|
||||
TinyUSB DCD DWC2 Driver supports two modes: Slave mode (based on IRQ) and Buffer DMA mode.
|
||||
|
||||
config TINYUSB_MODE_SLAVE
|
||||
bool "Slave/IRQ"
|
||||
config TINYUSB_MODE_DMA
|
||||
bool "Buffer DMA"
|
||||
endchoice
|
||||
endmenu # "TinyUSB DCD"
|
||||
|
||||
menu "TinyUSB task configuration"
|
||||
config TINYUSB_NO_DEFAULT_TASK
|
||||
bool "Do not create a TinyUSB task"
|
||||
default n
|
||||
help
|
||||
This option allows to not create the FreeRTOS task during the driver initialization.
|
||||
User will have to handle TinyUSB events manually.
|
||||
|
||||
config TINYUSB_TASK_PRIORITY
|
||||
int "TinyUSB task priority"
|
||||
default 5
|
||||
depends on !TINYUSB_NO_DEFAULT_TASK
|
||||
help
|
||||
Set the priority of the default TinyUSB main task.
|
||||
|
||||
config TINYUSB_TASK_STACK_SIZE
|
||||
int "TinyUSB task stack size (bytes)"
|
||||
default 4096
|
||||
depends on !TINYUSB_NO_DEFAULT_TASK
|
||||
help
|
||||
Set the stack size of the default TinyUSB main task.
|
||||
|
||||
choice TINYUSB_TASK_AFFINITY
|
||||
prompt "TinyUSB task affinity"
|
||||
default TINYUSB_TASK_AFFINITY_CPU1 if !FREERTOS_UNICORE
|
||||
default TINYUSB_TASK_AFFINITY_NO_AFFINITY
|
||||
depends on !TINYUSB_NO_DEFAULT_TASK
|
||||
help
|
||||
Allows setting TinyUSB tasks affinity, i.e. whether the task is pinned to
|
||||
CPU0, pinned to CPU1, or allowed to run on any CPU.
|
||||
|
||||
config TINYUSB_TASK_AFFINITY_NO_AFFINITY
|
||||
bool "No affinity"
|
||||
config TINYUSB_TASK_AFFINITY_CPU0
|
||||
bool "CPU0"
|
||||
config TINYUSB_TASK_AFFINITY_CPU1
|
||||
bool "CPU1"
|
||||
depends on !FREERTOS_UNICORE
|
||||
endchoice
|
||||
|
||||
config TINYUSB_TASK_AFFINITY
|
||||
hex
|
||||
default FREERTOS_NO_AFFINITY if TINYUSB_TASK_AFFINITY_NO_AFFINITY
|
||||
default 0x0 if TINYUSB_TASK_AFFINITY_CPU0
|
||||
default 0x1 if TINYUSB_TASK_AFFINITY_CPU1
|
||||
|
||||
config TINYUSB_INIT_IN_DEFAULT_TASK
|
||||
bool "Initialize TinyUSB stack within the default TinyUSB task"
|
||||
default n
|
||||
depends on !TINYUSB_NO_DEFAULT_TASK
|
||||
help
|
||||
Run TinyUSB stack initialization just after starting the default TinyUSB task.
|
||||
This is especially useful in multicore scenarios, when we need to pin the task
|
||||
to a specific core and, at the same time initialize TinyUSB stack
|
||||
(i.e. install interrupts) on the same core.
|
||||
endmenu # "TinyUSB task configuration"
|
||||
|
||||
menu "Descriptor configuration"
|
||||
comment "You can provide your custom descriptors via tinyusb_driver_install()"
|
||||
config TINYUSB_DESC_USE_ESPRESSIF_VID
|
||||
bool "VID: Use Espressif's vendor ID"
|
||||
default y
|
||||
help
|
||||
Enable this option, USB device will use Espressif's vendor ID as its VID.
|
||||
This is helpful at product develop stage.
|
||||
|
||||
config TINYUSB_DESC_CUSTOM_VID
|
||||
hex "VID: Custom vendor ID"
|
||||
default 0x1234
|
||||
depends on !TINYUSB_DESC_USE_ESPRESSIF_VID
|
||||
help
|
||||
Custom Vendor ID.
|
||||
|
||||
config TINYUSB_DESC_USE_DEFAULT_PID
|
||||
bool "PID: Use a default PID assigned to TinyUSB"
|
||||
default y
|
||||
help
|
||||
Default TinyUSB PID assigning uses values 0x4000...0x4007.
|
||||
|
||||
config TINYUSB_DESC_CUSTOM_PID
|
||||
hex "PID: Custom product ID"
|
||||
default 0x5678
|
||||
depends on !TINYUSB_DESC_USE_DEFAULT_PID
|
||||
help
|
||||
Custom Product ID.
|
||||
|
||||
config TINYUSB_DESC_BCD_DEVICE
|
||||
hex "bcdDevice"
|
||||
default 0x0100
|
||||
help
|
||||
Version of the firmware of the USB device.
|
||||
|
||||
config TINYUSB_DESC_MANUFACTURER_STRING
|
||||
string "Manufacturer name"
|
||||
default "Espressif Systems"
|
||||
help
|
||||
Name of the manufacturer of the USB device.
|
||||
|
||||
config TINYUSB_DESC_PRODUCT_STRING
|
||||
string "Product name"
|
||||
default "Espressif Device"
|
||||
help
|
||||
Name of the USB device.
|
||||
|
||||
config TINYUSB_DESC_SERIAL_STRING
|
||||
string "Serial string"
|
||||
default "123456"
|
||||
help
|
||||
Serial number of the USB device.
|
||||
|
||||
config TINYUSB_DESC_CDC_STRING
|
||||
depends on TINYUSB_CDC_ENABLED
|
||||
string "CDC Device String"
|
||||
default "Espressif CDC Device"
|
||||
help
|
||||
Name of the CDC device.
|
||||
|
||||
config TINYUSB_DESC_MSC_STRING
|
||||
depends on TINYUSB_MSC_ENABLED
|
||||
string "MSC Device String"
|
||||
default "Espressif MSC Device"
|
||||
help
|
||||
Name of the MSC device.
|
||||
endmenu # "Descriptor configuration"
|
||||
|
||||
menu "Massive Storage Class (MSC)"
|
||||
config TINYUSB_MSC_ENABLED
|
||||
bool "Enable TinyUSB MSC feature"
|
||||
default n
|
||||
help
|
||||
Enable TinyUSB MSC feature.
|
||||
|
||||
config TINYUSB_MSC_BUFSIZE
|
||||
depends on TINYUSB_MSC_ENABLED
|
||||
int "MSC FIFO size"
|
||||
default 512 if IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3
|
||||
default 8192 if IDF_TARGET_ESP32P4
|
||||
range 64 8192 if IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3
|
||||
range 64 32768 if IDF_TARGET_ESP32P4
|
||||
help
|
||||
MSC FIFO size, in bytes.
|
||||
|
||||
config TINYUSB_MSC_MOUNT_PATH
|
||||
depends on TINYUSB_MSC_ENABLED
|
||||
string "Mount Path"
|
||||
default "/data"
|
||||
help
|
||||
MSC Mount Path of storage.
|
||||
|
||||
menu "TinyUSB FAT Format Options"
|
||||
choice TINYUSB_FAT_FORMAT_TYPE
|
||||
prompt "FatFS Format Type"
|
||||
default TINYUSB_FAT_FORMAT_ANY
|
||||
help
|
||||
Select the FAT filesystem type used when formatting storage.
|
||||
|
||||
config TINYUSB_FAT_FORMAT_ANY
|
||||
bool "FM_ANY - Automatically select from FAT12/FAT16/FAT32"
|
||||
|
||||
config TINYUSB_FAT_FORMAT_FAT
|
||||
bool "FM_FAT - Allow only FAT12/FAT16"
|
||||
|
||||
config TINYUSB_FAT_FORMAT_FAT32
|
||||
bool "FM_FAT32 - Force FAT32 only"
|
||||
|
||||
config TINYUSB_FAT_FORMAT_EXFAT
|
||||
bool "FM_EXFAT - Force exFAT (requires exFAT enabled)"
|
||||
|
||||
endchoice
|
||||
|
||||
config TINYUSB_FAT_FORMAT_SFD
|
||||
bool "FM_SFD - Use SFD (no partition table)"
|
||||
default n
|
||||
help
|
||||
Format as a Super Floppy Disk (no partition table).
|
||||
This is typical for USB flash drives and small volumes.
|
||||
endmenu
|
||||
|
||||
endmenu # "Massive Storage Class"
|
||||
|
||||
menu "Communication Device Class (CDC)"
|
||||
config TINYUSB_CDC_ENABLED
|
||||
bool "Enable TinyUSB CDC feature"
|
||||
default n
|
||||
help
|
||||
Enable TinyUSB CDC feature.
|
||||
|
||||
config TINYUSB_CDC_COUNT
|
||||
int "CDC Channel Count"
|
||||
default 1
|
||||
range 1 2
|
||||
depends on TINYUSB_CDC_ENABLED
|
||||
help
|
||||
Number of independent serial ports.
|
||||
|
||||
config TINYUSB_CDC_RX_BUFSIZE
|
||||
depends on TINYUSB_CDC_ENABLED
|
||||
int "CDC FIFO size of RX channel"
|
||||
default 512
|
||||
range 64 10000
|
||||
help
|
||||
CDC FIFO size of RX channel.
|
||||
|
||||
config TINYUSB_CDC_TX_BUFSIZE
|
||||
depends on TINYUSB_CDC_ENABLED
|
||||
int "CDC FIFO size of TX channel"
|
||||
default 512
|
||||
help
|
||||
CDC FIFO size of TX channel.
|
||||
endmenu # "Communication Device Class"
|
||||
|
||||
menu "Musical Instrument Digital Interface (MIDI)"
|
||||
config TINYUSB_MIDI_COUNT
|
||||
int "TinyUSB MIDI interfaces count"
|
||||
default 0
|
||||
range 0 2
|
||||
help
|
||||
Setting value greater than 0 will enable TinyUSB MIDI feature.
|
||||
endmenu # "Musical Instrument Digital Interface (MIDI)"
|
||||
|
||||
menu "Human Interface Device Class (HID)"
|
||||
config TINYUSB_HID_COUNT
|
||||
int "TinyUSB HID interfaces count"
|
||||
default 0
|
||||
range 0 4
|
||||
help
|
||||
Setting value greater than 0 will enable TinyUSB HID feature.
|
||||
endmenu # "HID Device Class (HID)"
|
||||
|
||||
menu "Device Firmware Upgrade (DFU)"
|
||||
choice TINYUSB_DFU_MODE
|
||||
prompt "DFU mode"
|
||||
default TINYUSB_DFU_MODE_NONE
|
||||
help
|
||||
Select which DFU driver you want to use.
|
||||
|
||||
config TINYUSB_DFU_MODE_DFU
|
||||
bool "DFU"
|
||||
|
||||
config TINYUSB_DFU_MODE_DFU_RUNTIME
|
||||
bool "DFU Runtime"
|
||||
|
||||
config TINYUSB_DFU_MODE_NONE
|
||||
bool "None"
|
||||
endchoice
|
||||
config TINYUSB_DFU_BUFSIZE
|
||||
depends on TINYUSB_DFU_MODE_DFU
|
||||
int "DFU XFER BUFFSIZE"
|
||||
default 512
|
||||
help
|
||||
DFU XFER BUFFSIZE.
|
||||
endmenu # Device Firmware Upgrade (DFU)
|
||||
|
||||
menu "Bluetooth Host Class (BTH)"
|
||||
config TINYUSB_BTH_ENABLED
|
||||
bool "Enable TinyUSB BTH feature"
|
||||
default n
|
||||
help
|
||||
Enable TinyUSB BTH feature.
|
||||
|
||||
config TINYUSB_BTH_ISO_ALT_COUNT
|
||||
depends on TINYUSB_BTH_ENABLED
|
||||
int "BTH ISO ALT COUNT"
|
||||
default 0
|
||||
help
|
||||
BTH ISO ALT COUNT.
|
||||
endmenu # "Bluetooth Host Device Class"
|
||||
|
||||
menu "Network driver (ECM/NCM/RNDIS)"
|
||||
choice TINYUSB_NET_MODE
|
||||
prompt "Network mode"
|
||||
default TINYUSB_NET_MODE_NONE
|
||||
help
|
||||
Select network driver you want to use.
|
||||
|
||||
config TINYUSB_NET_MODE_ECM_RNDIS
|
||||
bool "ECM/RNDIS"
|
||||
|
||||
config TINYUSB_NET_MODE_NCM
|
||||
bool "NCM"
|
||||
|
||||
config TINYUSB_NET_MODE_NONE
|
||||
bool "None"
|
||||
endchoice
|
||||
|
||||
config TINYUSB_NCM_OUT_NTB_BUFFS_COUNT
|
||||
int "Number of NCM NTB buffers for reception side"
|
||||
depends on TINYUSB_NET_MODE_NCM
|
||||
default 3
|
||||
range 1 6
|
||||
help
|
||||
Number of NTB buffers for reception side.
|
||||
Can be increased to improve performance and stability with the cost of additional RAM requirements.
|
||||
Helps to mitigate "tud_network_can_xmit: request blocked" warning message when running NCM device.
|
||||
|
||||
config TINYUSB_NCM_IN_NTB_BUFFS_COUNT
|
||||
int "Number of NCM NTB buffers for transmission side"
|
||||
depends on TINYUSB_NET_MODE_NCM
|
||||
default 3
|
||||
range 1 6
|
||||
help
|
||||
Number of NTB buffers for transmission side.
|
||||
Can be increased to improve performance and stability with the cost of additional RAM requirements.
|
||||
Helps to mitigate "tud_network_can_xmit: request blocked" warning message when running NCM device.
|
||||
|
||||
config TINYUSB_NCM_OUT_NTB_BUFF_MAX_SIZE
|
||||
int "NCM NTB Buffer size for reception size"
|
||||
depends on TINYUSB_NET_MODE_NCM
|
||||
default 3200
|
||||
range 1600 10240
|
||||
help
|
||||
Size of NTB buffers on the reception side. The minimum size used by Linux is 2048 bytes.
|
||||
NTB buffer size must be significantly larger than the MTU (Maximum Transmission Unit).
|
||||
The typical default MTU size for Ethernet is 1500 bytes, plus an additional packet overhead.
|
||||
To improve performance, the NTB buffer size should be large enough to fit multiple MTU-sized
|
||||
frames in a single NTB buffer and it's length should be multiple of 4.
|
||||
|
||||
config TINYUSB_NCM_IN_NTB_BUFF_MAX_SIZE
|
||||
int "NCM NTB Buffer size for transmission size"
|
||||
depends on TINYUSB_NET_MODE_NCM
|
||||
default 3200
|
||||
range 1600 10240
|
||||
help
|
||||
Size of NTB buffers on the transmission side. The minimum size used by Linux is 2048 bytes.
|
||||
NTB buffer size must be significantly larger than the MTU (Maximum Transmission Unit).
|
||||
The typical default MTU size for Ethernet is 1500 bytes, plus an additional packet overhead.
|
||||
To improve performance, the NTB buffer size should be large enough to fit multiple MTU-sized
|
||||
frames in a single NTB buffer and it's length should be multiple of 4.
|
||||
|
||||
endmenu # "Network driver (ECM/NCM/RNDIS)"
|
||||
|
||||
menu "Vendor Specific Interface"
|
||||
config TINYUSB_VENDOR_COUNT
|
||||
int "TinyUSB Vendor specific interfaces count"
|
||||
default 0
|
||||
range 0 2
|
||||
help
|
||||
Setting value greater than 0 will enable TinyUSB Vendor specific feature.
|
||||
endmenu # "Vendor Specific Interface"
|
||||
endmenu # "TinyUSB Stack"
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,166 @@
|
||||
# Espressif's additions to TinyUSB
|
||||
|
||||
[](https://components.espressif.com/components/espressif/esp_tinyusb)
|
||||
|
||||
This component adds features to TinyUSB that help users with integrating TinyUSB with their ESP-IDF application.
|
||||
|
||||
It contains:
|
||||
* Configuration of USB device and string descriptors
|
||||
* USB Serial Device (CDC-ACM) with optional Virtual File System support
|
||||
* Input and output streams through USB Serial Device. This feature is available only when Virtual File System support is enabled.
|
||||
* Other USB classes (MIDI, MSC, HID…) support directly via TinyUSB
|
||||
* VBUS monitoring for self-powered devices
|
||||
* SPI Flash or sd-card access via MSC USB device Class.
|
||||
|
||||
## How to use?
|
||||
|
||||
This component is distributed via [IDF component manager](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/tools/idf-component-manager.html). Just add `idf_component.yml` file to your main component with the following content:
|
||||
|
||||
``` yaml
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
esp_tinyusb: "~1.0.0"
|
||||
```
|
||||
|
||||
Or simply run:
|
||||
```
|
||||
idf.py add-dependency esp_tinyusb~1.0.0
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Hardware-related documentation could be found in [ESP-IDF Programming Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-reference/peripherals/usb_device.html).
|
||||
|
||||
### Device Stack Structure
|
||||
|
||||
The Device Stack is built on top of TinyUSB and provides:
|
||||
|
||||
- Custom USB descriptor support
|
||||
- Serial device (CDC-ACM) support
|
||||
- Standard stream redirection through the serial device
|
||||
- Storage media support (SPI-Flash and SD-Card) for USB MSC Class
|
||||
- A dedicated task for TinyUSB servicing
|
||||
|
||||
### Configuration Options
|
||||
|
||||
Configure the Device Stack using `menuconfig`:
|
||||
|
||||
- TinyUSB log verbosity
|
||||
- Device Stack task options
|
||||
- Default device/string descriptor options
|
||||
- Class-specific options
|
||||
|
||||
### Descriptor Configuration
|
||||
|
||||
Configure USB descriptors using the `tinyusb_config_t` structure:
|
||||
|
||||
- `device_descriptor`
|
||||
- `string_descriptor`
|
||||
- `configuration_descriptor` (full-speed)
|
||||
- For high-speed devices: `fs_configuration_descriptor`, `hs_configuration_descriptor`, `qualifier_descriptor`
|
||||
|
||||
If any descriptor field is set to `NULL`, default descriptors (based on menuconfig) are used.
|
||||
|
||||
### Installation
|
||||
|
||||
Install the Device Stack by calling `tinyusb_driver_install` with a `tinyusb_config_t` structure. Members set to `0` or `NULL` use default values.
|
||||
|
||||
```c
|
||||
const tinyusb_config_t partial_init = {
|
||||
.device_descriptor = NULL,
|
||||
.string_descriptor = NULL,
|
||||
.external_phy = false,
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
.fs_configuration_descriptor = NULL,
|
||||
.hs_configuration_descriptor = NULL,
|
||||
.qualifier_descriptor = NULL,
|
||||
#else
|
||||
.configuration_descriptor = NULL,
|
||||
#endif
|
||||
};
|
||||
```
|
||||
|
||||
### Self-Powered Device
|
||||
|
||||
Self-powered devices must monitor VBUS voltage. Use a GPIO pin with a voltage divider or comparator to detect VBUS state. Set `self_powered = true` and assign the VBUS monitor GPIO in `tinyusb_config_t`.
|
||||
|
||||
### USB Serial Device (CDC-ACM)
|
||||
|
||||
If enabled, initialize the USB Serial Device with `tusb_cdc_acm_init` and a `tinyusb_config_cdcacm_t` structure:
|
||||
|
||||
```c
|
||||
const tinyusb_config_cdcacm_t acm_cfg = {
|
||||
.usb_dev = TINYUSB_USBDEV_0,
|
||||
.cdc_port = TINYUSB_CDC_ACM_0,
|
||||
.rx_unread_buf_sz = 64,
|
||||
.callback_rx = NULL,
|
||||
.callback_rx_wanted_char = NULL,
|
||||
.callback_line_state_changed = NULL,
|
||||
.callback_line_coding_changed = NULL
|
||||
};
|
||||
tusb_cdc_acm_init(&acm_cfg);
|
||||
```
|
||||
|
||||
Redirect standard I/O streams to USB with `esp_tusb_init_console` and revert with `esp_tusb_deinit_console`.
|
||||
|
||||
### USB Mass Storage Device (MSC)
|
||||
|
||||
If enabled, initialize storage media for MSC:
|
||||
|
||||
**SPI-Flash Example:**
|
||||
```c
|
||||
static esp_err_t storage_init_spiflash(wl_handle_t *wl_handle) {
|
||||
// ... partition and mount logic ...
|
||||
}
|
||||
storage_init_spiflash(&wl_handle);
|
||||
|
||||
const tinyusb_msc_spiflash_config_t config_spi = {
|
||||
.wl_handle = wl_handle
|
||||
};
|
||||
tinyusb_msc_storage_init_spiflash(&config_spi);
|
||||
```
|
||||
|
||||
**SD-Card Example:**
|
||||
```c
|
||||
static esp_err_t storage_init_sdmmc(sdmmc_card_t **card) {
|
||||
// ... SDMMC host and slot config ...
|
||||
}
|
||||
storage_init_sdmmc(&card);
|
||||
|
||||
const tinyusb_msc_sdmmc_config_t config_sdmmc = {
|
||||
.card = card
|
||||
};
|
||||
tinyusb_msc_storage_init_sdmmc(&config_sdmmc);
|
||||
```
|
||||
|
||||
### MSC Performance Optimization
|
||||
|
||||
- **Single-buffer approach:** Buffer size is set via `CONFIG_TINYUSB_MSC_BUFSIZE`.
|
||||
- **Performance:** SD cards offer higher throughput than internal SPI flash due to architectural constraints.
|
||||
|
||||
**Performance Table (ESP32-S3):**
|
||||
|
||||
| FIFO Size | Read Speed | Write Speed |
|
||||
|-----------|------------|-------------|
|
||||
| 512 B | 0.566 MB/s | 0.236 MB/s |
|
||||
| 8192 B | 0.925 MB/s | 0.928 MB/s |
|
||||
|
||||
**Performance Table (ESP32-P4):**
|
||||
|
||||
| FIFO Size | Read Speed | Write Speed |
|
||||
|-----------|------------|-------------|
|
||||
| 512 B | 1.174 MB/s | 0.238 MB/s |
|
||||
| 8192 B | 4.744 MB/s | 2.157 MB/s |
|
||||
| 32768 B | 5.998 MB/s | 4.485 MB/s |
|
||||
|
||||
**Performance Table (ESP32-S2, SPI Flash):**
|
||||
|
||||
| FIFO Size | Write Speed |
|
||||
|-----------|-------------|
|
||||
| 512 B | 5.59 KB/s |
|
||||
| 8192 B | 21.54 KB/s |
|
||||
|
||||
**Note:** Internal SPI flash is for demonstration only; use SD cards or external flash for higher performance.
|
||||
|
||||
## Examples
|
||||
You can find examples in [ESP-IDF on GitHub](https://github.com/espressif/esp-idf/tree/master/examples/peripherals/usb/device).
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include "esp_check.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "tusb.h"
|
||||
#include "cdc.h"
|
||||
|
||||
#define CDC_INTF_NUM CFG_TUD_CDC // number of cdc blocks
|
||||
static esp_tusb_cdc_t *cdc_obj[CDC_INTF_NUM] = {};
|
||||
static const char *TAG = "tusb_cdc";
|
||||
|
||||
esp_tusb_cdc_t *tinyusb_cdc_get_intf(int itf_num)
|
||||
{
|
||||
if (itf_num >= CDC_INTF_NUM || itf_num < 0) {
|
||||
return NULL;
|
||||
}
|
||||
return cdc_obj[itf_num];
|
||||
}
|
||||
|
||||
static esp_err_t cdc_obj_check(int itf, bool expected_inited, tusb_class_code_t expected_type)
|
||||
{
|
||||
esp_tusb_cdc_t *this_itf = tinyusb_cdc_get_intf(itf);
|
||||
|
||||
bool inited = (this_itf != NULL);
|
||||
ESP_RETURN_ON_FALSE(expected_inited == inited, ESP_ERR_INVALID_STATE, TAG, "Wrong state of the interface. Expected state: %s", expected_inited ? "initialized" : "not initialized");
|
||||
ESP_RETURN_ON_FALSE(!(inited && (expected_type != -1) && !(this_itf->type == expected_type)), ESP_ERR_INVALID_STATE, TAG, "Wrong type of the interface. Should be : 0x%x (tusb_class_code_t)", expected_type);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t tusb_cdc_comm_init(int itf)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(cdc_obj_check(itf, false, -1), TAG, "cdc_obj_check failed");
|
||||
cdc_obj[itf] = calloc(1, sizeof(esp_tusb_cdc_t));
|
||||
if (cdc_obj[itf] != NULL) {
|
||||
cdc_obj[itf]->type = TUSB_CLASS_CDC;
|
||||
ESP_LOGD(TAG, "CDC Comm class initialized");
|
||||
return ESP_OK;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "CDC Comm initialization error");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t tusb_cdc_deinit_comm(int itf)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(cdc_obj_check(itf, true, TUSB_CLASS_CDC), TAG, "cdc_obj_check failed");
|
||||
free(cdc_obj[itf]);
|
||||
cdc_obj[itf] = NULL;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t tusb_cdc_data_init(int itf)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(cdc_obj_check(itf, false, TUSB_CLASS_CDC_DATA), TAG, "cdc_obj_check failed");
|
||||
cdc_obj[itf] = calloc(1, sizeof(esp_tusb_cdc_t));
|
||||
if (cdc_obj[itf] != NULL) {
|
||||
cdc_obj[itf]->type = TUSB_CLASS_CDC_DATA;
|
||||
ESP_LOGD(TAG, "CDC Data class initialized");
|
||||
return ESP_OK;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "CDC Data initialization error");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t tusb_cdc_deinit_data(int itf)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(cdc_obj_check(itf, true, TUSB_CLASS_CDC_DATA), TAG, "cdc_obj_check failed");
|
||||
free(cdc_obj[itf]);
|
||||
cdc_obj[itf] = NULL;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t tinyusb_cdc_init(int itf, const tinyusb_config_cdc_t *cfg)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(cdc_obj_check(itf, false, -1), TAG, "cdc_obj_check failed");
|
||||
|
||||
ESP_LOGD(TAG, "Init CDC %d", itf);
|
||||
if (cfg->cdc_class == TUSB_CLASS_CDC) {
|
||||
ESP_RETURN_ON_ERROR(tusb_cdc_comm_init(itf), TAG, "tusb_cdc_comm_init failed");
|
||||
cdc_obj[itf]->cdc_subclass.comm_subclass = cfg->cdc_subclass.comm_subclass;
|
||||
} else {
|
||||
ESP_RETURN_ON_ERROR(tusb_cdc_data_init(itf), TAG, "tusb_cdc_data_init failed");
|
||||
cdc_obj[itf]->cdc_subclass.data_subclass = cfg->cdc_subclass.data_subclass;
|
||||
}
|
||||
cdc_obj[itf]->usb_dev = cfg->usb_dev;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t tinyusb_cdc_deinit(int itf)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(cdc_obj_check(itf, true, -1), TAG, "cdc_obj_check failed");
|
||||
|
||||
ESP_LOGD(TAG, "Deinit CDC %d", itf);
|
||||
if (cdc_obj[itf]->type == TUSB_CLASS_CDC) {
|
||||
ESP_RETURN_ON_ERROR(tusb_cdc_deinit_comm(itf), TAG, "tusb_cdc_deinit_comm failed");
|
||||
} else if (cdc_obj[itf]->type == TUSB_CLASS_CDC_DATA) {
|
||||
ESP_RETURN_ON_ERROR(tusb_cdc_deinit_data(itf), TAG, "tusb_cdc_deinit_data failed");
|
||||
} else {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_err.h"
|
||||
#include "descriptors_control.h"
|
||||
#include "usb_descriptors.h"
|
||||
|
||||
#define MAX_DESC_BUF_SIZE 32 // Max length of string descriptor (can be extended, USB supports lengths up to 255 bytes)
|
||||
|
||||
static const char *TAG = "tusb_desc";
|
||||
|
||||
// =============================================================================
|
||||
// STRUCTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Descriptor pointers for tinyusb descriptor requests callbacks
|
||||
*
|
||||
*/
|
||||
typedef struct {
|
||||
const tusb_desc_device_t *dev; /*!< Pointer to device descriptor */
|
||||
union {
|
||||
const uint8_t *cfg; /*!< Pointer to FullSpeed configuration descriptor when device one-speed only */
|
||||
const uint8_t *fs_cfg; /*!< Pointer to FullSpeed configuration descriptor when device support HighSpeed */
|
||||
};
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
const uint8_t *hs_cfg; /*!< Pointer to HighSpeed configuration descriptor */
|
||||
const tusb_desc_device_qualifier_t *qualifier; /*!< Pointer to Qualifier descriptor */
|
||||
uint8_t *other_speed; /*!< Pointer for other speed configuration descriptor */
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
const char *str[USB_STRING_DESCRIPTOR_ARRAY_SIZE]; /*!< Pointer to array of UTF-8 strings */
|
||||
int str_count; /*!< Number of descriptors in str */
|
||||
} tinyusb_descriptor_config_t;
|
||||
|
||||
static tinyusb_descriptor_config_t s_desc_cfg;
|
||||
|
||||
// =============================================================================
|
||||
// CALLBACKS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @brief Invoked when received GET DEVICE DESCRIPTOR.
|
||||
* Descriptor contents must exist long enough for transfer to complete
|
||||
*
|
||||
* @return Pointer to device descriptor
|
||||
*/
|
||||
uint8_t const *tud_descriptor_device_cb(void)
|
||||
{
|
||||
assert(s_desc_cfg.dev);
|
||||
return (uint8_t const *)s_desc_cfg.dev;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Invoked when received GET CONFIGURATION DESCRIPTOR.
|
||||
* Descriptor contents must exist long enough for transfer to complete
|
||||
*
|
||||
* @param[in] index Index of required configuration
|
||||
* @return Pointer to configuration descriptor
|
||||
*/
|
||||
uint8_t const *tud_descriptor_configuration_cb(uint8_t index)
|
||||
{
|
||||
(void)index; // Unused, this driver supports only 1 configuration
|
||||
assert(s_desc_cfg.cfg);
|
||||
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
// HINT: cfg and fs_cfg are union, no need to assert(fs_cfg)
|
||||
assert(s_desc_cfg.hs_cfg);
|
||||
// Return configuration descriptor based on Host speed
|
||||
return (TUSB_SPEED_HIGH == tud_speed_get())
|
||||
? s_desc_cfg.hs_cfg
|
||||
: s_desc_cfg.fs_cfg;
|
||||
#else
|
||||
return s_desc_cfg.cfg;
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
}
|
||||
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
/**
|
||||
* @brief Invoked when received GET DEVICE QUALIFIER DESCRIPTOR request
|
||||
* Descriptor contents must exist long enough for transfer to complete
|
||||
* If not highspeed capable stall this request
|
||||
*/
|
||||
uint8_t const *tud_descriptor_device_qualifier_cb(void)
|
||||
{
|
||||
assert(s_desc_cfg.qualifier);
|
||||
return (uint8_t const *)s_desc_cfg.qualifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Invoked when received GET OTHER SPEED CONFIGURATION DESCRIPTOR request
|
||||
* Descriptor contents must exist long enough for transfer to complete
|
||||
* Configuration descriptor in the other speed e.g if high speed then this is for full speed and vice versa
|
||||
*/
|
||||
uint8_t const *tud_descriptor_other_speed_configuration_cb(uint8_t index)
|
||||
{
|
||||
assert(s_desc_cfg.other_speed);
|
||||
|
||||
const uint8_t *other_speed = (TUSB_SPEED_HIGH == tud_speed_get())
|
||||
? s_desc_cfg.fs_cfg
|
||||
: s_desc_cfg.hs_cfg;
|
||||
|
||||
memcpy(s_desc_cfg.other_speed,
|
||||
other_speed,
|
||||
((tusb_desc_configuration_t *)other_speed)->wTotalLength);
|
||||
|
||||
((tusb_desc_configuration_t *)s_desc_cfg.other_speed)->bDescriptorType = TUSB_DESC_OTHER_SPEED_CONFIG;
|
||||
return s_desc_cfg.other_speed;
|
||||
}
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
|
||||
/**
|
||||
* @brief Invoked when received GET STRING DESCRIPTOR request
|
||||
*
|
||||
* @param[in] index Index of required descriptor
|
||||
* @param[in] langid Language of the descriptor
|
||||
* @return Pointer to UTF-16 string descriptor
|
||||
*/
|
||||
uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid)
|
||||
{
|
||||
(void) langid; // Unused, this driver supports only one language in string descriptors
|
||||
assert(s_desc_cfg.str);
|
||||
uint8_t chr_count;
|
||||
static uint16_t _desc_str[MAX_DESC_BUF_SIZE];
|
||||
|
||||
if (index == 0) {
|
||||
memcpy(&_desc_str[1], s_desc_cfg.str[0], 2);
|
||||
chr_count = 1;
|
||||
} else {
|
||||
if (index >= USB_STRING_DESCRIPTOR_ARRAY_SIZE) {
|
||||
ESP_LOGW(TAG, "String index (%u) is out of bounds, check your string descriptor", index);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (s_desc_cfg.str[index] == NULL) {
|
||||
ESP_LOGW(TAG, "String index (%u) points to NULL, check your string descriptor", index);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *str = s_desc_cfg.str[index];
|
||||
chr_count = strnlen(str, MAX_DESC_BUF_SIZE - 1); // Buffer len - header
|
||||
|
||||
// Convert ASCII string into UTF-16
|
||||
for (uint8_t i = 0; i < chr_count; i++) {
|
||||
_desc_str[1 + i] = str[i];
|
||||
}
|
||||
}
|
||||
|
||||
// First byte is length in bytes (including header), second byte is descriptor type (TUSB_DESC_STRING)
|
||||
_desc_str[0] = (TUSB_DESC_STRING << 8 ) | (2 * chr_count + 2);
|
||||
|
||||
return _desc_str;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Driver functions
|
||||
// =============================================================================
|
||||
esp_err_t tinyusb_set_descriptors(const tinyusb_config_t *config)
|
||||
{
|
||||
esp_err_t ret = ESP_FAIL;
|
||||
assert(config);
|
||||
const char **pstr_desc;
|
||||
// Flush descriptors control struct
|
||||
memset(&s_desc_cfg, 0x00, sizeof(tinyusb_descriptor_config_t));
|
||||
// Parse configuration and save descriptors's pointer
|
||||
// Select Device Descriptor
|
||||
if (config->device_descriptor == NULL) {
|
||||
ESP_LOGW(TAG, "No Device descriptor provided, using default.");
|
||||
s_desc_cfg.dev = &descriptor_dev_default;
|
||||
} else {
|
||||
s_desc_cfg.dev = config->device_descriptor;
|
||||
}
|
||||
|
||||
// Select FullSpeed configuration descriptor
|
||||
if (config->configuration_descriptor == NULL) {
|
||||
// Default configuration descriptor must be provided for the following classes
|
||||
#if (CFG_TUD_HID > 0 || CFG_TUD_MIDI > 0 || CFG_TUD_ECM_RNDIS > 0 || CFG_TUD_DFU > 0 || CFG_TUD_DFU_RUNTIME > 0 || CFG_TUD_BTH > 0)
|
||||
ESP_GOTO_ON_FALSE(config->configuration_descriptor, ESP_ERR_INVALID_ARG, fail, TAG, "Configuration descriptor must be provided for this device");
|
||||
#else
|
||||
ESP_LOGW(TAG, "No FullSpeed configuration descriptor provided, using default.");
|
||||
s_desc_cfg.cfg = descriptor_fs_cfg_default;
|
||||
#endif
|
||||
} else {
|
||||
s_desc_cfg.cfg = config->configuration_descriptor;
|
||||
}
|
||||
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
// High Speed
|
||||
if (config->hs_configuration_descriptor == NULL) {
|
||||
// Default configuration descriptor must be provided for the following classes
|
||||
#if (CFG_TUD_HID > 0 || CFG_TUD_MIDI > 0 || CFG_TUD_ECM_RNDIS > 0 || CFG_TUD_DFU > 0 || CFG_TUD_DFU_RUNTIME > 0 || CFG_TUD_BTH > 0)
|
||||
ESP_GOTO_ON_FALSE(config->hs_configuration_descriptor, ESP_ERR_INVALID_ARG, fail, TAG, "HighSpeed configuration descriptor must be provided for this device");
|
||||
#else
|
||||
ESP_LOGW(TAG, "No HighSpeed configuration descriptor provided, using default.");
|
||||
s_desc_cfg.hs_cfg = descriptor_hs_cfg_default;
|
||||
#endif
|
||||
} else {
|
||||
s_desc_cfg.hs_cfg = config->hs_configuration_descriptor;
|
||||
}
|
||||
|
||||
// HS and FS cfg desc should be equal length
|
||||
ESP_GOTO_ON_FALSE(((tusb_desc_configuration_t *)s_desc_cfg.hs_cfg)->wTotalLength ==
|
||||
((tusb_desc_configuration_t *)s_desc_cfg.fs_cfg)->wTotalLength,
|
||||
ESP_ERR_INVALID_ARG, fail, TAG, "HighSpeed and FullSpeed configuration descriptors must be same length");
|
||||
|
||||
// Qualifier Descriptor
|
||||
if (config->qualifier_descriptor == NULL) {
|
||||
ESP_GOTO_ON_FALSE((s_desc_cfg.dev == &descriptor_dev_default), ESP_ERR_INVALID_ARG, fail, TAG, "Qualifier descriptor must be present (Device Descriptor not default).");
|
||||
// Get default qualifier if device descriptor is default
|
||||
ESP_LOGW(TAG, "No Qulifier descriptor provided, using default.");
|
||||
s_desc_cfg.qualifier = &descriptor_qualifier_default;
|
||||
} else {
|
||||
s_desc_cfg.qualifier = config->qualifier_descriptor;
|
||||
}
|
||||
|
||||
// Other Speed buffer allocate
|
||||
s_desc_cfg.other_speed = calloc(1, ((tusb_desc_configuration_t *)s_desc_cfg.hs_cfg)->wTotalLength);
|
||||
ESP_GOTO_ON_FALSE(s_desc_cfg.other_speed, ESP_ERR_NO_MEM, fail, TAG, "Other speed memory allocation error");
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
|
||||
// Select String Descriptors and count them
|
||||
if (config->string_descriptor == NULL) {
|
||||
ESP_LOGW(TAG, "No String descriptors provided, using default.");
|
||||
pstr_desc = descriptor_str_default;
|
||||
while (descriptor_str_default[++s_desc_cfg.str_count] != NULL);
|
||||
} else {
|
||||
pstr_desc = config->string_descriptor;
|
||||
s_desc_cfg.str_count = (config->string_descriptor_count != 0)
|
||||
? config->string_descriptor_count
|
||||
: 8; // '8' is for backward compatibility with esp_tinyusb v1.0.0. Do NOT remove!
|
||||
}
|
||||
|
||||
ESP_GOTO_ON_FALSE(s_desc_cfg.str_count <= USB_STRING_DESCRIPTOR_ARRAY_SIZE, ESP_ERR_NOT_SUPPORTED, fail, TAG, "String descriptors exceed limit");
|
||||
memcpy(s_desc_cfg.str, pstr_desc, s_desc_cfg.str_count * sizeof(pstr_desc[0]));
|
||||
|
||||
ESP_LOGI(TAG, "\n"
|
||||
"┌─────────────────────────────────┐\n"
|
||||
"│ USB Device Descriptor Summary │\n"
|
||||
"├───────────────────┬─────────────┤\n"
|
||||
"│bDeviceClass │ %-4u │\n"
|
||||
"├───────────────────┼─────────────┤\n"
|
||||
"│bDeviceSubClass │ %-4u │\n"
|
||||
"├───────────────────┼─────────────┤\n"
|
||||
"│bDeviceProtocol │ %-4u │\n"
|
||||
"├───────────────────┼─────────────┤\n"
|
||||
"│bMaxPacketSize0 │ %-4u │\n"
|
||||
"├───────────────────┼─────────────┤\n"
|
||||
"│idVendor │ %-#10x │\n"
|
||||
"├───────────────────┼─────────────┤\n"
|
||||
"│idProduct │ %-#10x │\n"
|
||||
"├───────────────────┼─────────────┤\n"
|
||||
"│bcdDevice │ %-#10x │\n"
|
||||
"├───────────────────┼─────────────┤\n"
|
||||
"│iManufacturer │ %-#10x │\n"
|
||||
"├───────────────────┼─────────────┤\n"
|
||||
"│iProduct │ %-#10x │\n"
|
||||
"├───────────────────┼─────────────┤\n"
|
||||
"│iSerialNumber │ %-#10x │\n"
|
||||
"├───────────────────┼─────────────┤\n"
|
||||
"│bNumConfigurations │ %-#10x │\n"
|
||||
"└───────────────────┴─────────────┘",
|
||||
s_desc_cfg.dev->bDeviceClass, s_desc_cfg.dev->bDeviceSubClass,
|
||||
s_desc_cfg.dev->bDeviceProtocol, s_desc_cfg.dev->bMaxPacketSize0,
|
||||
s_desc_cfg.dev->idVendor, s_desc_cfg.dev->idProduct, s_desc_cfg.dev->bcdDevice,
|
||||
s_desc_cfg.dev->iManufacturer, s_desc_cfg.dev->iProduct, s_desc_cfg.dev->iSerialNumber,
|
||||
s_desc_cfg.dev->bNumConfigurations);
|
||||
|
||||
return ESP_OK;
|
||||
|
||||
fail:
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
free(s_desc_cfg.other_speed);
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
return ret;
|
||||
}
|
||||
|
||||
void tinyusb_set_str_descriptor(const char *str, int str_idx)
|
||||
{
|
||||
assert(str_idx < USB_STRING_DESCRIPTOR_ARRAY_SIZE);
|
||||
s_desc_cfg.str[str_idx] = str;
|
||||
}
|
||||
|
||||
void tinyusb_free_descriptors(void)
|
||||
{
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
assert(s_desc_cfg.other_speed);
|
||||
free(s_desc_cfg.other_speed);
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
dependencies:
|
||||
idf: '>=5.0'
|
||||
tinyusb:
|
||||
public: true
|
||||
version: '>=0.14.2'
|
||||
description: Espressif's additions to TinyUSB
|
||||
documentation: https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-reference/peripherals/usb_device.html
|
||||
repository: git://github.com/espressif/esp-usb.git
|
||||
repository_info:
|
||||
commit_sha: c0be948c1acc6ee1f7ef00ab183c571971fccefd
|
||||
path: device/esp_tinyusb
|
||||
url: https://github.com/espressif/esp-usb/tree/master/device/esp_tinyusb
|
||||
version: 1.7.6~2
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
#include "tusb.h"
|
||||
#include "tinyusb_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Configuration structure of the TinyUSB core
|
||||
*
|
||||
* USB specification mandates self-powered devices to monitor USB VBUS to detect connection/disconnection events.
|
||||
* If you want to use this feature, connected VBUS to any free GPIO through a voltage divider or voltage comparator.
|
||||
* The voltage divider output should be (0.75 * Vdd) if VBUS is 4.4V (lowest valid voltage at device port).
|
||||
* The comparator thresholds should be set with hysteresis: 4.35V (falling edge) and 4.75V (raising edge).
|
||||
*/
|
||||
typedef struct {
|
||||
union {
|
||||
const tusb_desc_device_t *device_descriptor; /*!< Pointer to a device descriptor. If set to NULL, the TinyUSB device will use a default device descriptor whose values are set in Kconfig */
|
||||
const tusb_desc_device_t *descriptor __attribute__((deprecated)); /*!< Alias to `device_descriptor` for backward compatibility */
|
||||
};
|
||||
const char **string_descriptor; /*!< Pointer to array of string descriptors. If set to NULL, TinyUSB device will use a default string descriptors whose values are set in Kconfig */
|
||||
int string_descriptor_count; /*!< Number of descriptors in above array */
|
||||
bool external_phy; /*!< Should USB use an external PHY */
|
||||
union {
|
||||
struct {
|
||||
const uint8_t *configuration_descriptor; /*!< Pointer to a configuration descriptor. If set to NULL, TinyUSB device will use a default configuration descriptor whose values are set in Kconfig */
|
||||
};
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
struct {
|
||||
const uint8_t *fs_configuration_descriptor; /*!< Pointer to a FullSpeed configuration descriptor. If set to NULL, TinyUSB device will use a default configuration descriptor whose values are set in Kconfig */
|
||||
};
|
||||
};
|
||||
const uint8_t *hs_configuration_descriptor; /*!< Pointer to a HighSpeed configuration descriptor. If set to NULL, TinyUSB device will use a default configuration descriptor whose values are set in Kconfig */
|
||||
const tusb_desc_device_qualifier_t *qualifier_descriptor; /*!< Pointer to a qualifier descriptor */
|
||||
#else
|
||||
};
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
bool self_powered; /*!< This is a self-powered USB device. USB VBUS must be monitored. */
|
||||
int vbus_monitor_io; /*!< GPIO for VBUS monitoring. Ignored if not self_powered. */
|
||||
} tinyusb_config_t;
|
||||
|
||||
/**
|
||||
* @brief This is an all-in-one helper function, including:
|
||||
* 1. USB device driver initialization
|
||||
* 2. Descriptors preparation
|
||||
* 3. TinyUSB stack initialization
|
||||
* 4. Creates and start a task to handle usb events
|
||||
*
|
||||
* @note Don't change Custom descriptor, but if it has to be done,
|
||||
* Suggest to define as follows in order to match the Interface Association Descriptor (IAD):
|
||||
* bDeviceClass = TUSB_CLASS_MISC,
|
||||
* bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
*
|
||||
* @param config tinyusb stack specific configuration
|
||||
* @retval ESP_ERR_INVALID_ARG Install driver and tinyusb stack failed because of invalid argument
|
||||
* @retval ESP_FAIL Install driver and tinyusb stack failed because of internal error
|
||||
* @retval ESP_OK Install driver and tinyusb stack successfully
|
||||
*/
|
||||
esp_err_t tinyusb_driver_install(const tinyusb_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief This is an all-in-one helper function, including:
|
||||
* 1. Stops the task to handle usb events
|
||||
* 2. TinyUSB stack tearing down
|
||||
* 2. Freeing resources after descriptors preparation
|
||||
* 3. Deletes USB PHY
|
||||
*
|
||||
* @retval ESP_FAIL Uninstall driver or tinyusb stack failed because of internal error
|
||||
* @retval ESP_OK Uninstall driver, tinyusb stack and USB PHY successfully
|
||||
*/
|
||||
esp_err_t tinyusb_driver_uninstall(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "tinyusb_types.h"
|
||||
#include "esp_err.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if (CONFIG_TINYUSB_NET_MODE_NONE != 1)
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief On receive callback type
|
||||
*/
|
||||
typedef esp_err_t (*tusb_net_rx_cb_t)(void *buffer, uint16_t len, void *ctx);
|
||||
|
||||
/**
|
||||
* @brief Free Tx buffer callback type
|
||||
*/
|
||||
typedef void (*tusb_net_free_tx_cb_t)(void *buffer, void *ctx);
|
||||
|
||||
/**
|
||||
* @brief On init callback type
|
||||
*/
|
||||
typedef void (*tusb_net_init_cb_t)(void *ctx);
|
||||
|
||||
/**
|
||||
* @brief ESP TinyUSB NCM driver configuration structure
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t mac_addr[6]; /*!< MAC address. Must be 6 bytes long. */
|
||||
tusb_net_rx_cb_t on_recv_callback; /*!< TinyUSB receive data callbeck */
|
||||
tusb_net_free_tx_cb_t free_tx_buffer; /*!< User function for freeing the Tx buffer.
|
||||
* - could be NULL, if user app is responsible for freeing the buffer
|
||||
* - must be used in asynchronous send mode
|
||||
* - is only called if the used tinyusb_net_send...() function returns ESP_OK
|
||||
* - in sync mode means that the packet was accepted by TinyUSB
|
||||
* - in async mode means that the packet was queued to be processed in TinyUSB task
|
||||
*/
|
||||
tusb_net_init_cb_t on_init_callback; /*!< TinyUSB init network callback */
|
||||
void *user_context; /*!< User context to be passed to any of the callback */
|
||||
} tinyusb_net_config_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize TinyUSB NET driver
|
||||
*
|
||||
* @param[in] usb_dev USB device to use
|
||||
* @param[in] cfg Configuration of the driver
|
||||
* @return esp_err_t
|
||||
*/
|
||||
esp_err_t tinyusb_net_init(tinyusb_usbdev_t usb_dev, const tinyusb_net_config_t *cfg);
|
||||
|
||||
/**
|
||||
* @brief TinyUSB NET driver send data synchronously
|
||||
*
|
||||
* @note It is possible to use sync and async send interchangeably.
|
||||
* This function needs some synchronization primitives, so using sync mode (even once) uses more heap
|
||||
*
|
||||
* @param[in] buffer USB send data
|
||||
* @param[in] len Send data len
|
||||
* @param[in] buff_free_arg Pointer to be passed to the free_tx_buffer() callback
|
||||
* @param[in] timeout Send data len
|
||||
* @return ESP_OK on success == packet has been consumed by tusb and would be eventually freed
|
||||
* by free_tx_buffer() callback (if non null)
|
||||
* ESP_ERR_TIMEOUT on timeout
|
||||
* ESP_ERR_INVALID_STATE if tusb not initialized, ESP_ERR_NO_MEM on alloc failure
|
||||
*/
|
||||
esp_err_t tinyusb_net_send_sync(void *buffer, uint16_t len, void *buff_free_arg, TickType_t timeout);
|
||||
|
||||
/**
|
||||
* @brief TinyUSB NET driver send data asynchronously
|
||||
*
|
||||
* @note If using asynchronous sends, you must free the buffer using free_tx_buffer() callback.
|
||||
* @note It is possible to use sync and async send interchangeably.
|
||||
* @note Async flavor of the send is useful when the USB stack runs faster than the caller,
|
||||
* since we have no control over the transmitted packets, if they get accepted or discarded.
|
||||
*
|
||||
* @param[in] buffer USB send data
|
||||
* @param[in] len Send data len
|
||||
* @param[in] buff_free_arg Pointer to be passed to the free_tx_buffer() callback
|
||||
* @return ESP_OK on success == packet has been consumed by tusb and will be freed
|
||||
* by free_tx_buffer() callback (if non null)
|
||||
* ESP_ERR_INVALID_STATE if tusb not initialized
|
||||
*/
|
||||
esp_err_t tinyusb_net_send_async(void *buffer, uint16_t len, void *buff_free_arg);
|
||||
|
||||
#endif // (CONFIG_TINYUSB_NET_MODE_NONE != 1)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2023 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define USB_ESPRESSIF_VID 0x303A
|
||||
|
||||
typedef enum {
|
||||
TINYUSB_USBDEV_0,
|
||||
} tinyusb_usbdev_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2023 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "tinyusb_types.h"
|
||||
#include "class/cdc/cdc.h"
|
||||
|
||||
#if (CONFIG_TINYUSB_CDC_ENABLED != 1)
|
||||
#error "TinyUSB CDC driver must be enabled in menuconfig"
|
||||
#endif
|
||||
|
||||
|
||||
/**
|
||||
* @brief CDC ports available to setup
|
||||
*/
|
||||
typedef enum {
|
||||
TINYUSB_CDC_ACM_0 = 0x0,
|
||||
TINYUSB_CDC_ACM_1,
|
||||
TINYUSB_CDC_ACM_MAX
|
||||
} tinyusb_cdcacm_itf_t;
|
||||
|
||||
/* Callbacks and events
|
||||
********************************************************************* */
|
||||
|
||||
/**
|
||||
* @brief Data provided to the input of the `callback_rx_wanted_char` callback
|
||||
*/
|
||||
typedef struct {
|
||||
char wanted_char; /*!< Wanted character */
|
||||
} cdcacm_event_rx_wanted_char_data_t;
|
||||
|
||||
/**
|
||||
* @brief Data provided to the input of the `callback_line_state_changed` callback
|
||||
*/
|
||||
typedef struct {
|
||||
bool dtr; /*!< Data Terminal Ready (DTR) line state */
|
||||
bool rts; /*!< Request To Send (RTS) line state */
|
||||
} cdcacm_event_line_state_changed_data_t;
|
||||
|
||||
/**
|
||||
* @brief Data provided to the input of the `line_coding_changed` callback
|
||||
*/
|
||||
typedef struct {
|
||||
cdc_line_coding_t const *p_line_coding; /*!< New line coding value */
|
||||
} cdcacm_event_line_coding_changed_data_t;
|
||||
|
||||
/**
|
||||
* @brief Types of CDC ACM events
|
||||
*/
|
||||
typedef enum {
|
||||
CDC_EVENT_RX,
|
||||
CDC_EVENT_RX_WANTED_CHAR,
|
||||
CDC_EVENT_LINE_STATE_CHANGED,
|
||||
CDC_EVENT_LINE_CODING_CHANGED
|
||||
} cdcacm_event_type_t;
|
||||
|
||||
/**
|
||||
* @brief Describes an event passing to the input of a callbacks
|
||||
*/
|
||||
typedef struct {
|
||||
cdcacm_event_type_t type; /*!< Event type */
|
||||
union {
|
||||
cdcacm_event_rx_wanted_char_data_t rx_wanted_char_data; /*!< Data input of the `callback_rx_wanted_char` callback */
|
||||
cdcacm_event_line_state_changed_data_t line_state_changed_data; /*!< Data input of the `callback_line_state_changed` callback */
|
||||
cdcacm_event_line_coding_changed_data_t line_coding_changed_data; /*!< Data input of the `line_coding_changed` callback */
|
||||
};
|
||||
} cdcacm_event_t;
|
||||
|
||||
/**
|
||||
* @brief CDC-ACM callback type
|
||||
*/
|
||||
typedef void(*tusb_cdcacm_callback_t)(int itf, cdcacm_event_t *event);
|
||||
|
||||
/*********************************************************************** Callbacks and events*/
|
||||
/* Other structs
|
||||
********************************************************************* */
|
||||
|
||||
/**
|
||||
* @brief Configuration structure for CDC-ACM
|
||||
*/
|
||||
typedef struct {
|
||||
tinyusb_usbdev_t usb_dev; /*!< Usb device to set up */
|
||||
tinyusb_cdcacm_itf_t cdc_port; /*!< CDC port */
|
||||
size_t rx_unread_buf_sz __attribute__((deprecated("This parameter is not used any more. Configure RX buffer in menuconfig.")));
|
||||
tusb_cdcacm_callback_t callback_rx; /*!< Pointer to the function with the `tusb_cdcacm_callback_t` type that will be handled as a callback */
|
||||
tusb_cdcacm_callback_t callback_rx_wanted_char; /*!< Pointer to the function with the `tusb_cdcacm_callback_t` type that will be handled as a callback */
|
||||
tusb_cdcacm_callback_t callback_line_state_changed; /*!< Pointer to the function with the `tusb_cdcacm_callback_t` type that will be handled as a callback */
|
||||
tusb_cdcacm_callback_t callback_line_coding_changed; /*!< Pointer to the function with the `tusb_cdcacm_callback_t` type that will be handled as a callback */
|
||||
} tinyusb_config_cdcacm_t;
|
||||
|
||||
/*********************************************************************** Other structs*/
|
||||
/* Public functions
|
||||
********************************************************************* */
|
||||
/**
|
||||
* @brief Initialize CDC ACM. Initialization will be finished with
|
||||
* the `tud_cdc_line_state_cb` callback
|
||||
*
|
||||
* @param[in] cfg Configuration structure
|
||||
* @return esp_err_t
|
||||
*/
|
||||
esp_err_t tusb_cdc_acm_init(const tinyusb_config_cdcacm_t *cfg);
|
||||
|
||||
/**
|
||||
* @brief De-initialize CDC ACM.
|
||||
*
|
||||
* @param[in] itf Index of CDC interface
|
||||
* @return esp_err_t
|
||||
*/
|
||||
esp_err_t tusb_cdc_acm_deinit(int itf);
|
||||
|
||||
/**
|
||||
* @brief Register a callback invoking on CDC event. If the callback had been
|
||||
* already registered, it will be overwritten
|
||||
*
|
||||
* @param[in] itf Index of CDC interface
|
||||
* @param[in] event_type Type of registered event for a callback
|
||||
* @param[in] callback Callback function
|
||||
* @return esp_err_t - ESP_OK or ESP_ERR_INVALID_ARG
|
||||
*/
|
||||
esp_err_t tinyusb_cdcacm_register_callback(tinyusb_cdcacm_itf_t itf,
|
||||
cdcacm_event_type_t event_type,
|
||||
tusb_cdcacm_callback_t callback);
|
||||
|
||||
/**
|
||||
* @brief Unregister a callback invoking on CDC event
|
||||
*
|
||||
* @param[in] itf Index of CDC interface
|
||||
* @param[in] event_type Type of registered event for a callback
|
||||
* @return esp_err_t - ESP_OK or ESP_ERR_INVALID_ARG
|
||||
*/
|
||||
esp_err_t tinyusb_cdcacm_unregister_callback(tinyusb_cdcacm_itf_t itf, cdcacm_event_type_t event_type);
|
||||
|
||||
/**
|
||||
* @brief Sent one character to a write buffer
|
||||
*
|
||||
* @param[in] itf Index of CDC interface
|
||||
* @param[in] ch Character to send
|
||||
* @return size_t - amount of queued bytes
|
||||
*/
|
||||
size_t tinyusb_cdcacm_write_queue_char(tinyusb_cdcacm_itf_t itf, char ch);
|
||||
|
||||
/**
|
||||
* @brief Write data to write buffer
|
||||
*
|
||||
* @param[in] itf Index of CDC interface
|
||||
* @param[in] in_buf Data
|
||||
* @param[in] in_size Data size in bytes
|
||||
* @return size_t - amount of queued bytes
|
||||
*/
|
||||
size_t tinyusb_cdcacm_write_queue(tinyusb_cdcacm_itf_t itf, const uint8_t *in_buf, size_t in_size);
|
||||
|
||||
/**
|
||||
* @brief Flush data in write buffer of CDC interface
|
||||
*
|
||||
* Use `tinyusb_cdcacm_write_queue` to add data to the buffer
|
||||
*
|
||||
* WARNING! TinyUSB can block output Endpoint for several RX callbacks, after will do additional flush
|
||||
* after the each transfer. That can leads to the situation when you requested a flush, but it will fail until
|
||||
* one of the next callbacks ends.
|
||||
* SO USING OF THE FLUSH WITH TIMEOUTS IN CALLBACKS IS NOT RECOMMENDED - YOU CAN GET A LOCK FOR THE TIMEOUT
|
||||
*
|
||||
* @param[in] itf Index of CDC interface
|
||||
* @param[in] timeout_ticks Transfer timeout. Set to zero for non-blocking mode
|
||||
* @return - ESP_OK All data flushed
|
||||
* - ESP_ERR_TIMEOUT Time out occurred in blocking mode
|
||||
* - ESP_NOT_FINISHED The transfer is still in progress in non-blocking mode
|
||||
*/
|
||||
esp_err_t tinyusb_cdcacm_write_flush(tinyusb_cdcacm_itf_t itf, uint32_t timeout_ticks);
|
||||
|
||||
/**
|
||||
* @brief Receive data from CDC interface
|
||||
*
|
||||
* @param[in] itf Index of CDC interface
|
||||
* @param[out] out_buf Data buffer
|
||||
* @param[in] out_buf_sz Data buffer size in bytes
|
||||
* @param[out] rx_data_size Number of bytes written to out_buf
|
||||
* @return esp_err_t ESP_OK, ESP_FAIL or ESP_ERR_INVALID_STATE
|
||||
*/
|
||||
esp_err_t tinyusb_cdcacm_read(tinyusb_cdcacm_itf_t itf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size);
|
||||
|
||||
/**
|
||||
* @brief Check if the CDC interface is initialized
|
||||
*
|
||||
* @param[in] itf Index of CDC interface
|
||||
* @return - true Initialized
|
||||
* - false Not Initialized
|
||||
*/
|
||||
bool tusb_cdc_acm_initialized(tinyusb_cdcacm_itf_t itf);
|
||||
|
||||
/*********************************************************************** Public functions*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2019 Ha Thach (tinyusb.org),
|
||||
* SPDX-FileContributor: 2020-2025 Espressif Systems (Shanghai) CO LTD
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (c) 2019 Ha Thach (tinyusb.org),
|
||||
* Additions Copyright (c) 2020, Espressif Systems (Shanghai) PTE LTD
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "tusb_option.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_CDC_ENABLED
|
||||
# define CONFIG_TINYUSB_CDC_ENABLED 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_CDC_COUNT
|
||||
# define CONFIG_TINYUSB_CDC_COUNT 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_MSC_ENABLED
|
||||
# define CONFIG_TINYUSB_MSC_ENABLED 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_HID_COUNT
|
||||
# define CONFIG_TINYUSB_HID_COUNT 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_MIDI_COUNT
|
||||
# define CONFIG_TINYUSB_MIDI_COUNT 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_VENDOR_COUNT
|
||||
# define CONFIG_TINYUSB_VENDOR_COUNT 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_NET_MODE_ECM_RNDIS
|
||||
# define CONFIG_TINYUSB_NET_MODE_ECM_RNDIS 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_NET_MODE_NCM
|
||||
# define CONFIG_TINYUSB_NET_MODE_NCM 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_DFU_MODE_DFU
|
||||
# define CONFIG_TINYUSB_DFU_MODE_DFU 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_DFU_MODE_DFU_RUNTIME
|
||||
# define CONFIG_TINYUSB_DFU_MODE_DFU_RUNTIME 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_BTH_ENABLED
|
||||
# define CONFIG_TINYUSB_BTH_ENABLED 0
|
||||
# define CONFIG_TINYUSB_BTH_ISO_ALT_COUNT 0
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TINYUSB_DEBUG_LEVEL
|
||||
# define CONFIG_TINYUSB_DEBUG_LEVEL 0
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_TINYUSB_RHPORT_HS
|
||||
# define CFG_TUSB_RHPORT1_MODE OPT_MODE_DEVICE | OPT_MODE_HIGH_SPEED
|
||||
#else
|
||||
# define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED
|
||||
#endif
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// DCD DWC2 Mode
|
||||
// ------------------------------------------------------------------------
|
||||
#define CFG_TUD_DWC2_SLAVE_ENABLE 1 // Enable Slave/IRQ by default
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// DMA & Cache
|
||||
// ------------------------------------------------------------------------
|
||||
#ifdef CONFIG_TINYUSB_MODE_DMA
|
||||
// DMA Mode has a priority over Slave/IRQ mode and will be used if hardware supports it
|
||||
#define CFG_TUD_DWC2_DMA_ENABLE 1 // Enable DMA
|
||||
|
||||
#if CONFIG_CACHE_L1_CACHE_LINE_SIZE
|
||||
// To enable the dcd_dcache clean/invalidate/clean_invalidate calls
|
||||
# define CFG_TUD_MEM_DCACHE_ENABLE 1
|
||||
#define CFG_TUD_MEM_DCACHE_LINE_SIZE CONFIG_CACHE_L1_CACHE_LINE_SIZE
|
||||
// NOTE: starting with esp-idf v5.3 there is specific attribute present: DRAM_DMA_ALIGNED_ATTR
|
||||
# define CFG_TUSB_MEM_SECTION __attribute__((aligned(CONFIG_CACHE_L1_CACHE_LINE_SIZE))) DRAM_ATTR
|
||||
#else
|
||||
# define CFG_TUD_MEM_CACHE_ENABLE 0
|
||||
# define CFG_TUSB_MEM_SECTION TU_ATTR_ALIGNED(4) DRAM_ATTR
|
||||
#endif // CONFIG_CACHE_L1_CACHE_LINE_SIZE
|
||||
#endif // CONFIG_TINYUSB_MODE_DMA
|
||||
|
||||
#define CFG_TUSB_OS OPT_OS_FREERTOS
|
||||
|
||||
/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment.
|
||||
* Tinyusb use follows macros to declare transferring memory so that they can be put
|
||||
* into those specific section.
|
||||
* e.g
|
||||
* - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") ))
|
||||
* - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4)))
|
||||
*/
|
||||
#ifndef CFG_TUSB_MEM_SECTION
|
||||
# define CFG_TUSB_MEM_SECTION
|
||||
#endif
|
||||
|
||||
#ifndef CFG_TUSB_MEM_ALIGN
|
||||
# define CFG_TUSB_MEM_ALIGN TU_ATTR_ALIGNED(4)
|
||||
#endif
|
||||
|
||||
#ifndef CFG_TUD_ENDPOINT0_SIZE
|
||||
#define CFG_TUD_ENDPOINT0_SIZE 64
|
||||
#endif
|
||||
|
||||
// Debug Level
|
||||
#define CFG_TUSB_DEBUG CONFIG_TINYUSB_DEBUG_LEVEL
|
||||
#define CFG_TUSB_DEBUG_PRINTF esp_rom_printf // TinyUSB can print logs from ISR, so we must use esp_rom_printf()
|
||||
|
||||
// CDC FIFO size of TX and RX
|
||||
#define CFG_TUD_CDC_RX_BUFSIZE CONFIG_TINYUSB_CDC_RX_BUFSIZE
|
||||
#define CFG_TUD_CDC_TX_BUFSIZE CONFIG_TINYUSB_CDC_TX_BUFSIZE
|
||||
|
||||
// MSC Buffer size of Device Mass storage
|
||||
#define CFG_TUD_MSC_BUFSIZE CONFIG_TINYUSB_MSC_BUFSIZE
|
||||
|
||||
// MIDI macros
|
||||
#define CFG_TUD_MIDI_EP_BUFSIZE 64
|
||||
#define CFG_TUD_MIDI_EPSIZE CFG_TUD_MIDI_EP_BUFSIZE
|
||||
#define CFG_TUD_MIDI_RX_BUFSIZE 64
|
||||
#define CFG_TUD_MIDI_TX_BUFSIZE 64
|
||||
|
||||
// Vendor FIFO size of TX and RX
|
||||
#define CFG_TUD_VENDOR_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64)
|
||||
#define CFG_TUD_VENDOR_TX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64)
|
||||
|
||||
// DFU macros
|
||||
#define CFG_TUD_DFU_XFER_BUFSIZE CONFIG_TINYUSB_DFU_BUFSIZE
|
||||
|
||||
// Number of BTH ISO alternatives
|
||||
#define CFG_TUD_BTH_ISO_ALT_COUNT CONFIG_TINYUSB_BTH_ISO_ALT_COUNT
|
||||
|
||||
// Enabled device class driver
|
||||
#define CFG_TUD_CDC CONFIG_TINYUSB_CDC_COUNT
|
||||
#define CFG_TUD_MSC CONFIG_TINYUSB_MSC_ENABLED
|
||||
#define CFG_TUD_HID CONFIG_TINYUSB_HID_COUNT
|
||||
#define CFG_TUD_MIDI CONFIG_TINYUSB_MIDI_COUNT
|
||||
#define CFG_TUD_VENDOR CONFIG_TINYUSB_VENDOR_COUNT
|
||||
#define CFG_TUD_ECM_RNDIS CONFIG_TINYUSB_NET_MODE_ECM_RNDIS
|
||||
#define CFG_TUD_NCM CONFIG_TINYUSB_NET_MODE_NCM
|
||||
#define CFG_TUD_DFU CONFIG_TINYUSB_DFU_MODE_DFU
|
||||
#define CFG_TUD_DFU_RUNTIME CONFIG_TINYUSB_DFU_MODE_DFU_RUNTIME
|
||||
#define CFG_TUD_BTH CONFIG_TINYUSB_BTH_ENABLED
|
||||
|
||||
// NCM NET Mode NTB buffers configuration
|
||||
#define CFG_TUD_NCM_OUT_NTB_N CONFIG_TINYUSB_NCM_OUT_NTB_BUFFS_COUNT
|
||||
#define CFG_TUD_NCM_IN_NTB_N CONFIG_TINYUSB_NCM_IN_NTB_BUFFS_COUNT
|
||||
#define CFG_TUD_NCM_OUT_NTB_MAX_SIZE CONFIG_TINYUSB_NCM_OUT_NTB_BUFF_MAX_SIZE
|
||||
#define CFG_TUD_NCM_IN_NTB_MAX_SIZE CONFIG_TINYUSB_NCM_IN_NTB_BUFF_MAX_SIZE
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/**
|
||||
* @brief Redirect output to the USB serial
|
||||
* @param cdc_intf - interface number of TinyUSB's CDC
|
||||
*
|
||||
* @return esp_err_t - ESP_OK, ESP_FAIL or an error code
|
||||
*/
|
||||
esp_err_t esp_tusb_init_console(int cdc_intf);
|
||||
|
||||
/**
|
||||
* @brief Switch log to the default output
|
||||
* @param cdc_intf - interface number of TinyUSB's CDC
|
||||
*
|
||||
* @return esp_err_t
|
||||
*/
|
||||
esp_err_t esp_tusb_deinit_console(int cdc_intf);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stddef.h>
|
||||
#include "esp_err.h"
|
||||
#include "wear_levelling.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#if SOC_SDMMC_HOST_SUPPORTED
|
||||
#include "driver/sdmmc_host.h"
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Data provided to the input of the `callback_mount_changed` and `callback_premount_changed` callback
|
||||
*/
|
||||
typedef struct {
|
||||
bool is_mounted; /*!< Flag if storage is mounted or not */
|
||||
} tinyusb_msc_event_mount_changed_data_t;
|
||||
|
||||
/**
|
||||
* @brief Types of MSC events
|
||||
*/
|
||||
typedef enum {
|
||||
TINYUSB_MSC_EVENT_MOUNT_CHANGED, /*!< Event type AFTER mount/unmount operation is successfully finished */
|
||||
TINYUSB_MSC_EVENT_PREMOUNT_CHANGED /*!< Event type BEFORE mount/unmount operation is started */
|
||||
} tinyusb_msc_event_type_t;
|
||||
|
||||
/**
|
||||
* @brief Describes an event passing to the input of a callbacks
|
||||
*/
|
||||
typedef struct {
|
||||
tinyusb_msc_event_type_t type; /*!< Event type */
|
||||
union {
|
||||
tinyusb_msc_event_mount_changed_data_t mount_changed_data; /*!< Data input of the callback */
|
||||
};
|
||||
} tinyusb_msc_event_t;
|
||||
|
||||
/**
|
||||
* @brief MSC callback that is delivered whenever a specific event occurs.
|
||||
*/
|
||||
typedef void(*tusb_msc_callback_t)(tinyusb_msc_event_t *event);
|
||||
|
||||
#if SOC_SDMMC_HOST_SUPPORTED
|
||||
/**
|
||||
* @brief Configuration structure for sdmmc initialization
|
||||
*
|
||||
* User configurable parameters that are used while
|
||||
* initializing the sdmmc media.
|
||||
*/
|
||||
typedef struct {
|
||||
sdmmc_card_t *card; /*!< Pointer to sdmmc card configuration structure */
|
||||
tusb_msc_callback_t callback_mount_changed; /*!< Pointer to the function callback that will be delivered AFTER mount/unmount operation is successfully finished */
|
||||
tusb_msc_callback_t callback_premount_changed; /*!< Pointer to the function callback that will be delivered BEFORE mount/unmount operation is started */
|
||||
const esp_vfs_fat_mount_config_t mount_config; /*!< FATFS mount config */
|
||||
} tinyusb_msc_sdmmc_config_t;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Configuration structure for spiflash initialization
|
||||
*
|
||||
* User configurable parameters that are used while
|
||||
* initializing the SPI Flash media.
|
||||
*/
|
||||
typedef struct {
|
||||
wl_handle_t wl_handle; /*!< Pointer to spiflash wera-levelling handle */
|
||||
tusb_msc_callback_t callback_mount_changed; /*!< Pointer to the function callback that will be delivered AFTER mount/unmount operation is successfully finished */
|
||||
tusb_msc_callback_t callback_premount_changed; /*!< Pointer to the function callback that will be delivered BEFORE mount/unmount operation is started */
|
||||
const esp_vfs_fat_mount_config_t mount_config; /*!< FATFS mount config */
|
||||
} tinyusb_msc_spiflash_config_t;
|
||||
|
||||
/**
|
||||
* @brief Register storage type spiflash with tinyusb driver
|
||||
*
|
||||
* @param config pointer to the spiflash configuration
|
||||
* @return esp_err_t
|
||||
* - ESP_OK, if success;
|
||||
* - ESP_ERR_NO_MEM, if there was no memory to allocate storage components;
|
||||
* - ESP_ERR_NOT_SUPPORTED, if wear leveling sector size CONFIG_WL_SECTOR_SIZE is bigger than
|
||||
* the tinyusb MSC buffer size CONFIG_TINYUSB_MSC_BUFSIZE
|
||||
*/
|
||||
esp_err_t tinyusb_msc_storage_init_spiflash(const tinyusb_msc_spiflash_config_t *config);
|
||||
|
||||
#if SOC_SDMMC_HOST_SUPPORTED
|
||||
/**
|
||||
* @brief Register storage type sd-card with tinyusb driver
|
||||
*
|
||||
* @param config pointer to the sd card configuration
|
||||
* @return esp_err_t
|
||||
* - ESP_OK, if success;
|
||||
* - ESP_ERR_NO_MEM, if there was no memory to allocate storage components;
|
||||
*/
|
||||
esp_err_t tinyusb_msc_storage_init_sdmmc(const tinyusb_msc_sdmmc_config_t *config);
|
||||
#endif
|
||||
/**
|
||||
* @brief Deregister storage with tinyusb driver and frees the memory
|
||||
*
|
||||
*/
|
||||
void tinyusb_msc_storage_deinit(void);
|
||||
|
||||
/**
|
||||
* @brief Register a callback invoking on MSC event. If the callback had been
|
||||
* already registered, it will be overwritten
|
||||
*
|
||||
* @param event_type - type of registered event for a callback
|
||||
* @param callback - callback function
|
||||
* @return esp_err_t - ESP_OK or ESP_ERR_INVALID_ARG
|
||||
*/
|
||||
esp_err_t tinyusb_msc_register_callback(tinyusb_msc_event_type_t event_type,
|
||||
tusb_msc_callback_t callback);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Unregister a callback invoking on MSC event.
|
||||
*
|
||||
* @param event_type - type of registered event for a callback
|
||||
* @return esp_err_t - ESP_OK or ESP_ERR_INVALID_ARG
|
||||
*/
|
||||
esp_err_t tinyusb_msc_unregister_callback(tinyusb_msc_event_type_t event_type);
|
||||
|
||||
/**
|
||||
* @brief Mount the storage partition locally on the firmware application.
|
||||
*
|
||||
* Get the available drive number. Register spi flash partition.
|
||||
* Connect POSIX and C standard library IO function with FATFS.
|
||||
* Mounts the partition.
|
||||
* This API is used by the firmware application. If the storage partition is
|
||||
* mounted by this API, host (PC) can't access the storage via MSC.
|
||||
* When this function is called from the tinyusb callback functions, care must be taken
|
||||
* so as to make sure that user callbacks must be completed within a
|
||||
* specific time. Otherwise, MSC device may re-appear again on Host.
|
||||
*
|
||||
* @param base_path path prefix where FATFS should be registered
|
||||
* @return esp_err_t
|
||||
* - ESP_OK, if success;
|
||||
* - ESP_ERR_NOT_FOUND if the maximum count of volumes is already mounted
|
||||
* - ESP_ERR_NO_MEM if not enough memory or too many VFSes already registered;
|
||||
*/
|
||||
esp_err_t tinyusb_msc_storage_mount(const char *base_path);
|
||||
|
||||
/**
|
||||
* @brief Unmount the storage partition from the firmware application.
|
||||
*
|
||||
* Unmount the partition. Unregister diskio driver.
|
||||
* Unregister the SPI flash partition.
|
||||
* Finally, Un-register FATFS from VFS.
|
||||
* After this function is called, storage device can be seen (recognized) by host (PC).
|
||||
* When this function is called from the tinyusb callback functions, care must be taken
|
||||
* so as to make sure that user callbacks must be completed within a specific time.
|
||||
* Otherwise, MSC device may not appear on Host.
|
||||
*
|
||||
* @return esp_err_t
|
||||
* - ESP_OK on success
|
||||
* - ESP_ERR_INVALID_STATE if FATFS is not registered in VFS
|
||||
*/
|
||||
esp_err_t tinyusb_msc_storage_unmount(void);
|
||||
|
||||
/**
|
||||
* @brief Get number of sectors in storage media
|
||||
*
|
||||
* @return usable size, in bytes
|
||||
*/
|
||||
uint32_t tinyusb_msc_storage_get_sector_count(void);
|
||||
|
||||
/**
|
||||
* @brief Get sector size of storage media
|
||||
*
|
||||
* @return sector count
|
||||
*/
|
||||
uint32_t tinyusb_msc_storage_get_sector_size(void);
|
||||
|
||||
/**
|
||||
* @brief Get status if storage media is exposed over USB to Host
|
||||
*
|
||||
* @return bool
|
||||
* - true, if the storage media is exposed to Host
|
||||
* - false, if the stoarge media is mounted on application (not exposed to Host)
|
||||
*/
|
||||
bool tinyusb_msc_storage_in_use_by_usb_host(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief This helper function creates and starts a task which wraps `tud_task()`.
|
||||
*
|
||||
* The wrapper function basically wraps tud_task and some log.
|
||||
* Default parameters: stack size and priority as configured, argument = NULL, not pinned to any core.
|
||||
* If you have more requirements for this task, you can create your own task which calls tud_task as the last step.
|
||||
*
|
||||
* @retval ESP_OK run tinyusb main task successfully
|
||||
* @retval ESP_FAIL run tinyusb main task failed of internal error or initialization within the task failed when TINYUSB_INIT_IN_DEFAULT_TASK=y
|
||||
* @retval ESP_FAIL initialization within the task failed if CONFIG_TINYUSB_INIT_IN_DEFAULT_TASK is enabled
|
||||
* @retval ESP_ERR_INVALID_STATE tinyusb main task has been created before
|
||||
*/
|
||||
esp_err_t tusb_run_task(void);
|
||||
|
||||
/**
|
||||
* @brief This helper function stops and destroys the task created by `tusb_run_task()`
|
||||
*
|
||||
* @retval ESP_OK stop and destroy tinyusb main task successfully
|
||||
* @retval ESP_ERR_INVALID_STATE tinyusb main task hasn't been created yet
|
||||
*/
|
||||
esp_err_t tusb_stop_task(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2023 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_vfs_common.h" // For esp_line_endings_t definitions
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define VFS_TUSB_MAX_PATH 16
|
||||
#define VFS_TUSB_PATH_DEFAULT "/dev/tusb_cdc"
|
||||
|
||||
/**
|
||||
* @brief Register TinyUSB CDC at VFS with path
|
||||
*
|
||||
* Know limitation:
|
||||
* In case there are multiple CDC interfaces in the system, only one of them can be registered to VFS.
|
||||
*
|
||||
* @param[in] cdc_intf Interface number of TinyUSB's CDC
|
||||
* @param[in] path Path where the CDC will be registered, `/dev/tusb_cdc` will be used if left NULL.
|
||||
* @return esp_err_t ESP_OK or ESP_FAIL
|
||||
*/
|
||||
esp_err_t esp_vfs_tusb_cdc_register(int cdc_intf, char const *path);
|
||||
|
||||
/**
|
||||
* @brief Unregister TinyUSB CDC from VFS
|
||||
*
|
||||
* @param[in] path Path where the CDC will be unregistered if NULL will be used `/dev/tusb_cdc`
|
||||
* @return esp_err_t ESP_OK or ESP_FAIL
|
||||
*/
|
||||
esp_err_t esp_vfs_tusb_cdc_unregister(char const *path);
|
||||
|
||||
/**
|
||||
* @brief Set the line endings to sent
|
||||
*
|
||||
* This specifies the conversion between newlines ('\n', LF) on stdout and line
|
||||
* endings sent:
|
||||
*
|
||||
* - ESP_LINE_ENDINGS_CRLF: convert LF to CRLF
|
||||
* - ESP_LINE_ENDINGS_CR: convert LF to CR
|
||||
* - ESP_LINE_ENDINGS_LF: no modification
|
||||
*
|
||||
* @param[in] mode line endings to send
|
||||
*/
|
||||
void esp_vfs_tusb_cdc_set_tx_line_endings(esp_line_endings_t mode);
|
||||
|
||||
/**
|
||||
* @brief Set the line endings expected to be received
|
||||
*
|
||||
* This specifies the conversion between line endings received and
|
||||
* newlines ('\n', LF) passed into stdin:
|
||||
*
|
||||
* - ESP_LINE_ENDINGS_CRLF: convert CRLF to LF
|
||||
* - ESP_LINE_ENDINGS_CR: convert CR to LF
|
||||
* - ESP_LINE_ENDINGS_LF: no modification
|
||||
*
|
||||
* @param[in] mode line endings expected
|
||||
*/
|
||||
void esp_vfs_tusb_cdc_set_rx_line_endings(esp_line_endings_t mode);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/timers.h"
|
||||
#include "tusb.h"
|
||||
#include "tinyusb_types.h"
|
||||
|
||||
/* CDC classification
|
||||
********************************************************************* */
|
||||
typedef enum {
|
||||
TINYUSB_CDC_DATA = 0x00,
|
||||
} cdc_data_sublcass_type_t; // CDC120 specification
|
||||
|
||||
/* Note:other classification is represented in the file components\tinyusb\tinyusb\src\class\cdc\cdc.h */
|
||||
|
||||
/*********************************************************************** CDC classification*/
|
||||
/* Structs
|
||||
********************************************************************* */
|
||||
typedef struct {
|
||||
tinyusb_usbdev_t usb_dev; /*!< USB device to set up */
|
||||
tusb_class_code_t cdc_class; /*!< CDC device class : Communications or Data device */
|
||||
union {
|
||||
cdc_comm_sublcass_type_t comm_subclass; /*!< Communications device subclasses: ACM, ECM, etc. */
|
||||
cdc_data_sublcass_type_t data_subclass; /*!< Data device has only one subclass.*/
|
||||
} cdc_subclass; /*!< CDC device subclass according to Class Definitions for Communications Devices the CDC v.1.20 */
|
||||
} tinyusb_config_cdc_t; /*!< Main configuration structure of a CDC device */
|
||||
|
||||
typedef struct {
|
||||
tinyusb_usbdev_t usb_dev; /*!< USB device used for the instance */
|
||||
tusb_class_code_t type;
|
||||
union {
|
||||
cdc_comm_sublcass_type_t comm_subclass; /*!< Communications device subclasses: ACM, ECM, etc. */
|
||||
cdc_data_sublcass_type_t data_subclass; /*!< Data device has only one subclass.*/
|
||||
} cdc_subclass; /*!< CDC device subclass according to Class Definitions for Communications Devices the CDC v.1.20 */
|
||||
void *subclass_obj; /*!< Dynamically allocated subclass specific object */
|
||||
} esp_tusb_cdc_t;
|
||||
/*********************************************************************** Structs*/
|
||||
/* Functions
|
||||
********************************************************************* */
|
||||
/**
|
||||
* @brief Initializing CDC basic object
|
||||
* @param itf - number of a CDC object
|
||||
* @param cfg - CDC configuration structure
|
||||
*
|
||||
* @return esp_err_t ESP_OK or ESP_FAIL
|
||||
*/
|
||||
esp_err_t tinyusb_cdc_init(int itf, const tinyusb_config_cdc_t *cfg);
|
||||
|
||||
|
||||
/**
|
||||
* @brief De-initializing CDC. Clean its objects
|
||||
* @param itf - number of a CDC object
|
||||
* @return esp_err_t ESP_OK, ESP_ERR_INVALID_ARG, ESP_ERR_INVALID_STATE
|
||||
*
|
||||
*/
|
||||
esp_err_t tinyusb_cdc_deinit(int itf);
|
||||
|
||||
|
||||
/**
|
||||
* @brief Return interface of a CDC device
|
||||
*
|
||||
* @param itf_num
|
||||
* @return esp_tusb_cdc_t* pointer to the interface or (NULL) on error
|
||||
*/
|
||||
esp_tusb_cdc_t *tinyusb_cdc_get_intf(int itf_num);
|
||||
/*********************************************************************** Functions*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "tinyusb.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define USB_STRING_DESCRIPTOR_ARRAY_SIZE 8 // Max 8 string descriptors for a device. LANGID, Manufacturer, Product, Serial number + 4 user defined
|
||||
|
||||
/**
|
||||
* @brief Parse tinyusb configuration and prepare the device configuration pointer list to configure tinyusb driver
|
||||
*
|
||||
* @attention All descriptors passed to this function must exist for the duration of USB device lifetime
|
||||
*
|
||||
* @param[in] config tinyusb stack specific configuration
|
||||
* @retval ESP_ERR_INVALID_ARG Default configuration descriptor is provided only for CDC, MSC and NCM classes
|
||||
* @retval ESP_ERR_NO_MEM Memory allocation error
|
||||
* @retval ESP_OK Descriptors configured without error
|
||||
*/
|
||||
esp_err_t tinyusb_set_descriptors(const tinyusb_config_t *config);
|
||||
|
||||
/**
|
||||
* @brief Set specific string descriptor
|
||||
*
|
||||
* @attention The descriptor passed to this function must exist for the duration of USB device lifetime
|
||||
*
|
||||
* @param[in] str UTF-8 string
|
||||
* @param[in] str_idx String descriptor index
|
||||
*/
|
||||
void tinyusb_set_str_descriptor(const char *str, int str_idx);
|
||||
|
||||
/**
|
||||
* @brief Free memory allocated during tinyusb_set_descriptors
|
||||
*
|
||||
*/
|
||||
void tinyusb_free_descriptors(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "tusb.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Device descriptor generated from Kconfig
|
||||
*
|
||||
* This descriptor is used by default.
|
||||
* The user can provide their own device descriptor via tinyusb_driver_install() call
|
||||
*/
|
||||
extern const tusb_desc_device_t descriptor_dev_default;
|
||||
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
/**
|
||||
* @brief Qualifier Device descriptor generated from Kconfig
|
||||
*
|
||||
* This descriptor is used by default.
|
||||
* The user can provide their own descriptor via tinyusb_driver_install() call
|
||||
*/
|
||||
extern const tusb_desc_device_qualifier_t descriptor_qualifier_default;
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
|
||||
/**
|
||||
* @brief Array of string descriptors generated from Kconfig
|
||||
*
|
||||
* This descriptor is used by default.
|
||||
* The user can provide their own descriptor via tinyusb_driver_install() call
|
||||
*/
|
||||
extern const char *descriptor_str_default[];
|
||||
|
||||
/**
|
||||
* @brief FullSpeed configuration descriptor generated from Kconfig
|
||||
* This descriptor is used by default.
|
||||
* The user can provide their own FullSpeed configuration descriptor via tinyusb_driver_install() call
|
||||
*/
|
||||
extern const uint8_t descriptor_fs_cfg_default[];
|
||||
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
/**
|
||||
* @brief HighSpeed Configuration descriptor generated from Kconfig
|
||||
*
|
||||
* This descriptor is used by default.
|
||||
* The user can provide their own HighSpeed configuration descriptor via tinyusb_driver_install() call
|
||||
*/
|
||||
extern const uint8_t descriptor_hs_cfg_default[];
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
|
||||
uint8_t tusb_get_mac_string_id(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
supplier: 'Organization: Espressif Systems (Shanghai) CO LTD'
|
||||
originator: 'Organization: Espressif Systems (Shanghai) CO LTD'
|
||||
@@ -0,0 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(libusb_test
|
||||
LANGUAGES C
|
||||
)
|
||||
|
||||
add_executable(libusb_test libusb_test.c)
|
||||
target_link_libraries(libusb_test -lusb-1.0)
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <libusb-1.0/libusb.h>
|
||||
|
||||
#define TINYUSB_VENDOR 0x303A
|
||||
#define TINYUSB_PRODUCT 0x4002
|
||||
|
||||
#define DESC_TYPE_DEVICE_QUALIFIER 0x06
|
||||
#define DESC_TYOE_OTHER_SPEED_CONFIG 0x07
|
||||
|
||||
// Buffer for descriptor data
|
||||
unsigned char buffer[512] = { 0 };
|
||||
|
||||
// USB Other Speed Configuration Descriptor
|
||||
typedef struct __attribute__ ((packed))
|
||||
{
|
||||
uint8_t bLength ; ///< Size of descriptor
|
||||
uint8_t bDescriptorType ; ///< Other_speed_Configuration Type
|
||||
uint16_t wTotalLength ; ///< Total length of data returned
|
||||
|
||||
uint8_t bNumInterfaces ; ///< Number of interfaces supported by this speed configuration
|
||||
uint8_t bConfigurationValue ; ///< Value to use to select configuration
|
||||
uint8_t iConfiguration ; ///< Index of string descriptor
|
||||
uint8_t bmAttributes ; ///< Same as Configuration descriptor
|
||||
uint8_t bMaxPower ; ///< Same as Configuration descriptor
|
||||
} desc_other_speed_t;
|
||||
|
||||
// USB Device Qualifier Descriptor
|
||||
typedef struct __attribute__ ((packed))
|
||||
{
|
||||
uint8_t bLength ; ///< Size of descriptor
|
||||
uint8_t bDescriptorType ; ///< Device Qualifier Type
|
||||
uint16_t bcdUSB ; ///< USB specification version number (e.g., 0200H for V2.00)
|
||||
|
||||
uint8_t bDeviceClass ; ///< Class Code
|
||||
uint8_t bDeviceSubClass ; ///< SubClass Code
|
||||
uint8_t bDeviceProtocol ; ///< Protocol Code
|
||||
|
||||
uint8_t bMaxPacketSize0 ; ///< Maximum packet size for other speed
|
||||
uint8_t bNumConfigurations ; ///< Number of Other-speed Configurations
|
||||
uint8_t bReserved ; ///< Reserved for future use, must be zero
|
||||
} desc_device_qualifier_t;
|
||||
|
||||
// printf helpers
|
||||
static void _print_device_qulifier_desc(unsigned char *buffer, int length);
|
||||
static void _print_other_speed_desc(unsigned char *buffer, int length);
|
||||
|
||||
//
|
||||
// MAIN
|
||||
//
|
||||
int main()
|
||||
{
|
||||
libusb_context *context = NULL;
|
||||
int rc = 0;
|
||||
|
||||
rc = libusb_init(&context);
|
||||
assert(rc == 0);
|
||||
libusb_device_handle *dev_handle = libusb_open_device_with_vid_pid(context,
|
||||
TINYUSB_VENDOR,
|
||||
TINYUSB_PRODUCT);
|
||||
|
||||
if (dev_handle != NULL) {
|
||||
printf("TinyUSB Device has been found\n");
|
||||
|
||||
// Test Qualifier Descriprtor
|
||||
// 1. Get Qualifier Descriptor
|
||||
// 2. print descriptor data
|
||||
rc = libusb_get_descriptor(dev_handle, DESC_TYPE_DEVICE_QUALIFIER, 0, buffer, 512);
|
||||
_print_device_qulifier_desc(buffer, rc);
|
||||
|
||||
// Test Other Speed Descriptor
|
||||
// 1. Get Other Speed Descriptor
|
||||
// 2. print descriptor data
|
||||
rc = libusb_get_descriptor(dev_handle, DESC_TYOE_OTHER_SPEED_CONFIG, 0, buffer, 512);
|
||||
_print_other_speed_desc(buffer, rc);
|
||||
|
||||
libusb_close(dev_handle);
|
||||
} else {
|
||||
printf("TinyUSB Device has NOT been found\n");
|
||||
}
|
||||
|
||||
libusb_exit(context);
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
static void _print_device_qulifier_desc(unsigned char *buffer, int length)
|
||||
{
|
||||
assert(buffer);
|
||||
desc_device_qualifier_t *qualifier_desc = (desc_device_qualifier_t *) buffer;
|
||||
printf("========= Device Qualifier ========== \n");
|
||||
printf("\t bLength: %d \n", qualifier_desc->bLength);
|
||||
printf("\t bDescriptorType: %d (%#x)\n", qualifier_desc->bDescriptorType, qualifier_desc->bDescriptorType);
|
||||
printf("\t bcdUSB: %d (%#x) \n", qualifier_desc->bcdUSB, qualifier_desc->bcdUSB);
|
||||
printf("\t bDeviceClass: %d (%#x) \n", qualifier_desc->bDeviceClass, qualifier_desc->bDeviceClass);
|
||||
printf("\t bDeviceSubClass: %d \n", qualifier_desc->bDeviceSubClass);
|
||||
printf("\t bDeviceProtocol: %d \n", qualifier_desc->bDeviceProtocol);
|
||||
printf("\t bMaxPacketSize0: %d \n", qualifier_desc->bMaxPacketSize0);
|
||||
printf("\t bNumConfigurations: %d \n", qualifier_desc->bNumConfigurations);
|
||||
}
|
||||
|
||||
static void _print_other_speed_desc(unsigned char *buffer, int length)
|
||||
{
|
||||
assert(buffer);
|
||||
desc_other_speed_t *other_speed = (desc_other_speed_t *) buffer;
|
||||
printf("============ Other Speed ============ \n");
|
||||
printf("\t bLength: %d \n", other_speed->bLength);
|
||||
printf("\t bDescriptorType: %d (%#x) \n", other_speed->bDescriptorType, other_speed->bDescriptorType);
|
||||
printf("\t wTotalLength: %d \n", other_speed->wTotalLength);
|
||||
printf("\t bNumInterfaces: %d \n", other_speed->bNumInterfaces);
|
||||
printf("\t bConfigurationValue: %d \n", other_speed->bConfigurationValue);
|
||||
printf("\t iConfiguration: %d \n", other_speed->iConfiguration);
|
||||
printf("\t bmAttributes: %d (%#x) \n", other_speed->bmAttributes, other_speed->bmAttributes);
|
||||
printf("\t bMaxPower: %d (%#x) \n", other_speed->bMaxPower, other_speed->bMaxPower);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
# CI target runner setup
|
||||
|
||||
To allow a Docker container, running on a CI target runner, to access USB devices connected to the CI target runner, some modifications must be made.
|
||||
In our case, it's an `RPI` target runner.
|
||||
|
||||
The main idea comes from this response on [stackoverflow](https://stackoverflow.com/a/66427245/19840830). The same approach is also recommended in the official Docker [documentation](https://docs.docker.com/reference/cli/docker/container/run/#device-cgroup-rule)
|
||||
|
||||
|
||||
### Following changes shall be made on a CI target runner
|
||||
|
||||
- [`UDEV rules`](#udev-rules)
|
||||
- [`Docker tty script`](#docker-tty-script)
|
||||
- [`Logging`](#logging)
|
||||
- [`Running a docker container`](#running-a-docker-container)
|
||||
- [`GitHub CI target runner setup`](#github-ci-target-runner-setup)
|
||||
- [`GitLab CI target runner setup`](#gitlab-ci-target-runner-setup)
|
||||
|
||||
## UDEV rules
|
||||
|
||||
- This UDEV rule will trigger a `docker_tty.sh` script every time a USB device is connected, disconnected, or enumerated by the host machine (CI target runner)
|
||||
- Location: `/etc/udev/rules.d/99-docker-tty.rules`
|
||||
- `99-docker-tty.rules` file content:
|
||||
|
||||
``` sh
|
||||
ACTION=="add", SUBSYSTEM=="tty", RUN+="/usr/local/bin/docker_tty.sh 'added' '%E{DEVNAME}' '%M' '%m'"
|
||||
ACTION=="remove", SUBSYSTEM=="tty", RUN+="/usr/local/bin/docker_tty.sh 'removed' '%E{DEVNAME}' '%M' '%m'"
|
||||
```
|
||||
|
||||
## Docker tty script
|
||||
|
||||
- This `.sh` script, triggered by the UDEV rule above, will propagate USB devices to a running Docker container.
|
||||
- Location: `/usr/local/bin/docker_tty.sh`
|
||||
- `docker_tty.sh` file content:
|
||||
|
||||
``` sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Log the USB event with parameters
|
||||
echo "USB event: $1 $2 $3 $4" >> /tmp/docker_tty.log
|
||||
|
||||
# Find a running Docker container (using the first one found)
|
||||
docker_name=$(docker ps --format "{{.Names}}" | head -n 1)
|
||||
|
||||
# Check if a container was found
|
||||
if [ ! -z "$docker_name" ]; then
|
||||
if [ "$1" == "added" ]; then
|
||||
docker exec -u 0 "$docker_name" mknod $2 c $3 $4
|
||||
docker exec -u 0 "$docker_name" chmod -R 777 $2
|
||||
echo "Adding $2 to Docker container $docker_name" >> /tmp/docker_tty.log
|
||||
else
|
||||
docker exec -u 0 "$docker_name" rm $2
|
||||
echo "Removing $2 from Docker container $docker_name" >> /tmp/docker_tty.log
|
||||
fi
|
||||
else
|
||||
echo "No running Docker containers found." >> /tmp/docker_tty.log
|
||||
fi
|
||||
```
|
||||
|
||||
### Making the script executable
|
||||
|
||||
Don't forget to make the created script executable:
|
||||
``` sh
|
||||
root@~$ chmod +x /usr/local/bin/docker_tty.sh
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
- The `docker_tty.sh` script logs information about the USB devices it processes.
|
||||
- Location: `/tmp/docker_tty.log`
|
||||
- Example of a log from the `docker_tty.log` file, showing a flow of the `pytest_usb_device.py` test
|
||||
|
||||
```
|
||||
USB event: added /dev/ttyACM0 166 0
|
||||
USB event: added /dev/ttyACM1 166 1
|
||||
Adding /dev/ttyACM0 to Docker container d5e5c774174b435b8befea864f8fcb7f_python311bookworm_6a975d
|
||||
Adding /dev/ttyACM1 to Docker container d5e5c774174b435b8befea864f8fcb7f_python311bookworm_6a975d
|
||||
USB event: removed /dev/ttyACM0 166 0
|
||||
USB event: removed /dev/ttyACM1 166 1
|
||||
```
|
||||
|
||||
## Running a docker container
|
||||
|
||||
### Check Major and Minor numbers of connected devices
|
||||
|
||||
Check the Major and Minor numbers assigned by the Linux kernel to devices that you want the Docker container to access.
|
||||
In our case, we want to access `/dev/ttyUSB0`, `/dev/ttyACM0` and `/dev/ttyACM1`
|
||||
|
||||
`/dev/ttyUSB0`: Major 188, Minor 0
|
||||
``` sh
|
||||
peter@BrnoRPIG007:~ $ ls -l /dev/ttyUSB0
|
||||
crw-rw-rw- 1 root dialout 188, 0 Nov 12 11:08 /dev/ttyUSB0
|
||||
```
|
||||
|
||||
`/dev/ttyACM0` and `/dev/ttyACM1`: Major 166, Minor 0 (1)
|
||||
``` sh
|
||||
peter@BrnoRPIG007:~ $ ls -l /dev/ttyACM0
|
||||
crw-rw---- 1 root dialout 166, 0 Nov 13 10:26 /dev/ttyACM0
|
||||
peter@BrnoRPIG007:~ $ ls -l /dev/ttyACM1
|
||||
crw-rw---- 1 root dialout 166, 1 Nov 13 10:26 /dev/ttyACM1
|
||||
```
|
||||
|
||||
### Run a docker container
|
||||
|
||||
Run a Docker container with the following extra options:
|
||||
``` sh
|
||||
docker run --device-cgroup-rule='c 188:* rmw' --device-cgroup-rule='c 166:* rmw' --privileged ..
|
||||
```
|
||||
- `--device-cgroup-rule='c 188:* rmw'`: allow access to `ttyUSBx` (Major 188, all Minors)
|
||||
- `--device-cgroup-rule='c 166:* rmw'`: allow access to `ttyACMx` (Major 166, all Minors)
|
||||
|
||||
## GitHub CI target runner setup
|
||||
|
||||
To apply these changes to a GitHub target runner a `.yml` file used to run a Docker container for pytest must be modified. The Docker container is then run with the following options:
|
||||
|
||||
``` yaml
|
||||
container:
|
||||
image: python:3.11-bookworm
|
||||
options: --privileged --device-cgroup-rule="c 188:* rmw" --device-cgroup-rule="c 166:* rmw"
|
||||
```
|
||||
|
||||
## GitLab CI target runner setup
|
||||
|
||||
To apply these changes to a GitLab runner the `config.toml` file located at `/etc/gitlab-runner/config.toml` on each GitLab target runner must be modified.
|
||||
|
||||
According to GitLab's [documentation](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersdocker-section) the `[runners.docker]` section of the `config.toml` file should include the `device_cgroup_rules` parameter:
|
||||
|
||||
``` toml
|
||||
[runners.docker]
|
||||
...
|
||||
device_cgroup_rules = ["c 188:* rmw", "c 166:* rmw"]
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
set(COMPONENTS main)
|
||||
|
||||
project(test_app_cdc)
|
||||
@@ -0,0 +1,4 @@
|
||||
idf_component_register(SRC_DIRS .
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES unity
|
||||
WHOLE_ARCHIVE)
|
||||
@@ -0,0 +1,5 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
espressif/esp_tinyusb:
|
||||
version: "*"
|
||||
override_path: "../../../"
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "unity.h"
|
||||
#include "unity_test_runner.h"
|
||||
#include "unity_test_utils_memory.h"
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/*
|
||||
_ _ _
|
||||
| | (_) | |
|
||||
___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__
|
||||
/ _ \/ __| '_ \| __| | '_ \| | | | | | / __| '_ \
|
||||
| __/\__ \ |_) | |_| | | | | |_| | |_| \__ \ |_) |
|
||||
\___||___/ .__/ \__|_|_| |_|\__, |\__,_|___/_.__/
|
||||
| |______ __/ |
|
||||
|_|______| |___/
|
||||
_____ _____ _____ _____
|
||||
|_ _| ___/ ___|_ _|
|
||||
| | | |__ \ `--. | |
|
||||
| | | __| `--. \ | |
|
||||
| | | |___/\__/ / | |
|
||||
\_/ \____/\____/ \_/
|
||||
*/
|
||||
|
||||
printf(" _ _ _ \n");
|
||||
printf(" | | (_) | | \n");
|
||||
printf(" ___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__ \n");
|
||||
printf(" / _ \\/ __| '_ \\| __| | '_ \\| | | | | | / __| '_ \\ \n");
|
||||
printf("| __/\\__ \\ |_) | |_| | | | | |_| | |_| \\__ \\ |_) |\n");
|
||||
printf(" \\___||___/ .__/ \\__|_|_| |_|\\__, |\\__,_|___/_.__/ \n");
|
||||
printf(" | |______ __/ | \n");
|
||||
printf(" |_|______| |___/ \n");
|
||||
printf(" _____ _____ _____ _____ \n");
|
||||
printf("|_ _| ___/ ___|_ _| \n");
|
||||
printf(" | | | |__ \\ `--. | | \n");
|
||||
printf(" | | | __| `--. \\ | | \n");
|
||||
printf(" | | | |___/\\__/ / | | \n");
|
||||
printf(" \\_/ \\____/\\____/ \\_/ \n");
|
||||
|
||||
unity_utils_setup_heap_record(80);
|
||||
unity_utils_set_leak_level(128);
|
||||
unity_run_menu();
|
||||
}
|
||||
|
||||
/* setUp runs before every test */
|
||||
void setUp(void)
|
||||
{
|
||||
unity_utils_record_free_mem();
|
||||
}
|
||||
|
||||
/* tearDown runs after every test */
|
||||
void tearDown(void)
|
||||
{
|
||||
unity_utils_evaluate_leaks();
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "soc/soc_caps.h"
|
||||
#if SOC_USB_OTG_SUPPORTED
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_system.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#include "unity.h"
|
||||
#include "tinyusb.h"
|
||||
#include "tusb_cdc_acm.h"
|
||||
#include "vfs_tinyusb.h"
|
||||
|
||||
#define VFS_PATH "/dev/usb-cdc1"
|
||||
|
||||
static const tusb_desc_device_t cdc_device_descriptor = {
|
||||
.bLength = sizeof(cdc_device_descriptor),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
.bcdUSB = 0x0200,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.idVendor = USB_ESPRESSIF_VID,
|
||||
.idProduct = 0x4002,
|
||||
.bcdDevice = 0x0100,
|
||||
.iManufacturer = 0x01,
|
||||
.iProduct = 0x02,
|
||||
.iSerialNumber = 0x03,
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
static const uint16_t cdc_desc_config_len = TUD_CONFIG_DESC_LEN + CFG_TUD_CDC * TUD_CDC_DESC_LEN;
|
||||
static const uint8_t cdc_desc_configuration[] = {
|
||||
TUD_CONFIG_DESCRIPTOR(1, 4, 0, cdc_desc_config_len, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
|
||||
TUD_CDC_DESCRIPTOR(0, 4, 0x81, 8, 0x02, 0x82, (TUD_OPT_HIGH_SPEED ? 512 : 64)),
|
||||
TUD_CDC_DESCRIPTOR(2, 4, 0x83, 8, 0x04, 0x84, (TUD_OPT_HIGH_SPEED ? 512 : 64)),
|
||||
};
|
||||
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
static const tusb_desc_device_qualifier_t device_qualifier = {
|
||||
.bLength = sizeof(tusb_desc_device_qualifier_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
|
||||
.bcdUSB = 0x0200,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.bNumConfigurations = 0x01,
|
||||
.bReserved = 0
|
||||
};
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
|
||||
static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TinyUSB CDC testcase
|
||||
*
|
||||
* This is not a 'standard' testcase, as it never exits. The testcase runs in a loop where it echoes back all the data received.
|
||||
*
|
||||
* - Init TinyUSB with standard CDC device and configuration descriptors
|
||||
* - Init 2 CDC-ACM interfaces
|
||||
* - Map CDC1 to Virtual File System
|
||||
* - In a loop: Read data from CDC0 and CDC1. Echo received data back
|
||||
*
|
||||
* Note: CDC0 appends 'novfs' to echoed data, so the host (test runner) can easily determine which port is which.
|
||||
*/
|
||||
TEST_CASE("tinyusb_cdc", "[esp_tinyusb][cdc]")
|
||||
{
|
||||
// Install TinyUSB driver
|
||||
const tinyusb_config_t tusb_cfg = {
|
||||
.device_descriptor = &cdc_device_descriptor,
|
||||
.string_descriptor = NULL,
|
||||
.string_descriptor_count = 0,
|
||||
.external_phy = false,
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
.fs_configuration_descriptor = cdc_desc_configuration,
|
||||
.hs_configuration_descriptor = cdc_desc_configuration,
|
||||
.qualifier_descriptor = &device_qualifier,
|
||||
#else
|
||||
.configuration_descriptor = cdc_desc_configuration,
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
};
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_install(&tusb_cfg));
|
||||
|
||||
tinyusb_config_cdcacm_t acm_cfg = {
|
||||
.usb_dev = TINYUSB_USBDEV_0,
|
||||
.cdc_port = TINYUSB_CDC_ACM_0,
|
||||
.rx_unread_buf_sz = 64,
|
||||
.callback_rx = &tinyusb_cdc_rx_callback,
|
||||
.callback_rx_wanted_char = NULL,
|
||||
.callback_line_state_changed = NULL,
|
||||
.callback_line_coding_changed = NULL
|
||||
};
|
||||
|
||||
// Init CDC 0
|
||||
TEST_ASSERT_FALSE(tusb_cdc_acm_initialized(TINYUSB_CDC_ACM_0));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, tusb_cdc_acm_init(&acm_cfg));
|
||||
TEST_ASSERT_TRUE(tusb_cdc_acm_initialized(TINYUSB_CDC_ACM_0));
|
||||
|
||||
// Init CDC 1
|
||||
acm_cfg.cdc_port = TINYUSB_CDC_ACM_1;
|
||||
acm_cfg.callback_rx = NULL;
|
||||
TEST_ASSERT_FALSE(tusb_cdc_acm_initialized(TINYUSB_CDC_ACM_1));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, tusb_cdc_acm_init(&acm_cfg));
|
||||
TEST_ASSERT_TRUE(tusb_cdc_acm_initialized(TINYUSB_CDC_ACM_1));
|
||||
|
||||
// Install VFS to CDC 1
|
||||
TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_tusb_cdc_register(TINYUSB_CDC_ACM_1, VFS_PATH));
|
||||
esp_vfs_tusb_cdc_set_rx_line_endings(ESP_LINE_ENDINGS_CRLF);
|
||||
esp_vfs_tusb_cdc_set_tx_line_endings(ESP_LINE_ENDINGS_LF);
|
||||
FILE *cdc = fopen(VFS_PATH, "r+");
|
||||
TEST_ASSERT_NOT_NULL(cdc);
|
||||
|
||||
uint8_t buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE + 1];
|
||||
while (true) {
|
||||
size_t b = fread(buf, 1, sizeof(buf), cdc);
|
||||
if (b > 0) {
|
||||
printf("Intf VFS, RX %d bytes\n", b);
|
||||
//ESP_LOG_BUFFER_HEXDUMP("test", buf, b, ESP_LOG_INFO);
|
||||
fwrite(buf, 1, b, cdc);
|
||||
}
|
||||
vTaskDelay(1);
|
||||
|
||||
size_t rx_size = 0;
|
||||
int itf = 0;
|
||||
ESP_ERROR_CHECK(tinyusb_cdcacm_read(itf, buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size));
|
||||
if (rx_size > 0) {
|
||||
printf("Intf %d, RX %d bytes\n", itf, rx_size);
|
||||
|
||||
// Add 'novfs' to reply so the host can identify the port
|
||||
strcpy((char *)&buf[rx_size - 2], "novfs\r\n");
|
||||
tinyusb_cdcacm_write_queue(itf, buf, rx_size + sizeof("novfs") - 1);
|
||||
|
||||
tinyusb_cdcacm_write_flush(itf, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,84 @@
|
||||
# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import pytest
|
||||
from pytest_embedded_idf.dut import IdfDut
|
||||
from time import sleep
|
||||
from serial import Serial, SerialException
|
||||
from serial.tools.list_ports import comports
|
||||
|
||||
|
||||
@pytest.mark.esp32s2
|
||||
@pytest.mark.esp32s3
|
||||
@pytest.mark.esp32p4
|
||||
@pytest.mark.usb_device
|
||||
def test_usb_device_cdc(dut) -> None:
|
||||
'''
|
||||
Running the test locally:
|
||||
1. Build the testa app for your DUT (ESP32-S2 or S3)
|
||||
2. Connect you DUT to your test runner (local machine) with USB port and flashing port
|
||||
3. Run `pytest --target esp32s3`
|
||||
|
||||
Test procedure:
|
||||
1. Run the test on the DUT
|
||||
2. Expect 2 Virtual COM Ports in the system
|
||||
3. Open both comports and send some data. Expect echoed data
|
||||
'''
|
||||
dut.expect_exact('Press ENTER to see the list of tests.')
|
||||
dut.write('[cdc]')
|
||||
dut.expect_exact('TinyUSB: TinyUSB Driver installed')
|
||||
sleep(2) # Some time for the OS to enumerate our USB device
|
||||
|
||||
# Find devices with Espressif TinyUSB VID/PID
|
||||
s = []
|
||||
ports = comports()
|
||||
|
||||
for port, _, hwid in ports:
|
||||
if '303A:4002' in hwid:
|
||||
s.append(port)
|
||||
|
||||
if len(s) != 2:
|
||||
raise Exception('TinyUSB COM port not found')
|
||||
|
||||
try:
|
||||
with Serial(s[0]) as cdc0:
|
||||
with Serial(s[1]) as cdc1:
|
||||
# Write dummy string and check for echo
|
||||
cdc0.write('text\r\n'.encode())
|
||||
res = cdc0.readline()
|
||||
assert b'text' in res
|
||||
if b'novfs' in res:
|
||||
novfs_cdc = cdc0
|
||||
vfs_cdc = cdc1
|
||||
|
||||
cdc1.write('text\r\n'.encode())
|
||||
res = cdc1.readline()
|
||||
assert b'text' in res
|
||||
if b'novfs' in res:
|
||||
novfs_cdc = cdc1
|
||||
vfs_cdc = cdc0
|
||||
|
||||
# Write more than MPS, check that the transfer is not divided
|
||||
novfs_cdc.write(bytes(100))
|
||||
dut.expect_exact("Intf 0, RX 100 bytes")
|
||||
|
||||
# Write more than RX buffer, check correct reception
|
||||
novfs_cdc.write(bytes(600))
|
||||
transfer_len1 = int(dut.expect(r'Intf 0, RX (\d+) bytes')[1].decode())
|
||||
transfer_len2 = int(dut.expect(r'Intf 0, RX (\d+) bytes')[1].decode())
|
||||
assert transfer_len1 + transfer_len2 == 600
|
||||
|
||||
# The VFS is setup for CRLF RX and LF TX
|
||||
vfs_cdc.write('text\r\n'.encode())
|
||||
res = vfs_cdc.readline()
|
||||
assert b'text\n' in res
|
||||
|
||||
return
|
||||
|
||||
except SerialException as e:
|
||||
print(f"SerialException occurred: {e}")
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
raise
|
||||
@@ -0,0 +1,18 @@
|
||||
# Configure TinyUSB, it will be used to mock USB devices
|
||||
CONFIG_TINYUSB_MSC_ENABLED=n
|
||||
CONFIG_TINYUSB_CDC_ENABLED=y
|
||||
CONFIG_TINYUSB_CDC_COUNT=2
|
||||
CONFIG_TINYUSB_HID_COUNT=0
|
||||
|
||||
# Disable watchdogs, they'd get triggered during unity interactive menu
|
||||
CONFIG_ESP_INT_WDT=n
|
||||
CONFIG_ESP_TASK_WDT=n
|
||||
|
||||
# Run-time checks of Heap and Stack
|
||||
CONFIG_HEAP_POISONING_COMPREHENSIVE=y
|
||||
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
|
||||
CONFIG_COMPILER_STACK_CHECK=y
|
||||
|
||||
CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL=y
|
||||
|
||||
CONFIG_COMPILER_CXX_EXCEPTIONS=y
|
||||
@@ -0,0 +1,9 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
set(COMPONENTS main)
|
||||
|
||||
project(test_app_configuration_descriptor)
|
||||
@@ -0,0 +1,4 @@
|
||||
idf_component_register(SRC_DIRS .
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES unity
|
||||
WHOLE_ARCHIVE)
|
||||
@@ -0,0 +1,5 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
espressif/esp_tinyusb:
|
||||
version: "*"
|
||||
override_path: "../../../"
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "unity.h"
|
||||
#include "unity_test_runner.h"
|
||||
#include "unity_test_utils_memory.h"
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/*
|
||||
_ _ _
|
||||
| | (_) | |
|
||||
___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__
|
||||
/ _ \/ __| '_ \| __| | '_ \| | | | | | / __| '_ \
|
||||
| __/\__ \ |_) | |_| | | | | |_| | |_| \__ \ |_) |
|
||||
\___||___/ .__/ \__|_|_| |_|\__, |\__,_|___/_.__/
|
||||
| |______ __/ |
|
||||
|_|______| |___/
|
||||
_____ _____ _____ _____
|
||||
|_ _| ___/ ___|_ _|
|
||||
| | | |__ \ `--. | |
|
||||
| | | __| `--. \ | |
|
||||
| | | |___/\__/ / | |
|
||||
\_/ \____/\____/ \_/
|
||||
*/
|
||||
|
||||
printf(" _ _ _ \n");
|
||||
printf(" | | (_) | | \n");
|
||||
printf(" ___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__ \n");
|
||||
printf(" / _ \\/ __| '_ \\| __| | '_ \\| | | | | | / __| '_ \\ \n");
|
||||
printf("| __/\\__ \\ |_) | |_| | | | | |_| | |_| \\__ \\ |_) |\n");
|
||||
printf(" \\___||___/ .__/ \\__|_|_| |_|\\__, |\\__,_|___/_.__/ \n");
|
||||
printf(" | |______ __/ | \n");
|
||||
printf(" |_|______| |___/ \n");
|
||||
printf(" _____ _____ _____ _____ \n");
|
||||
printf("|_ _| ___/ ___|_ _| \n");
|
||||
printf(" | | | |__ \\ `--. | | \n");
|
||||
printf(" | | | __| `--. \\ | | \n");
|
||||
printf(" | | | |___/\\__/ / | | \n");
|
||||
printf(" \\_/ \\____/\\____/ \\_/ \n");
|
||||
|
||||
unity_utils_setup_heap_record(80);
|
||||
unity_utils_set_leak_level(128);
|
||||
unity_run_menu();
|
||||
}
|
||||
|
||||
/* setUp runs before every test */
|
||||
void setUp(void)
|
||||
{
|
||||
unity_utils_record_free_mem();
|
||||
}
|
||||
|
||||
/* tearDown runs after every test */
|
||||
void tearDown(void)
|
||||
{
|
||||
unity_utils_evaluate_leaks();
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "soc/soc_caps.h"
|
||||
|
||||
#if SOC_USB_OTG_SUPPORTED
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_system.h"
|
||||
#include "esp_err.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
#include "unity.h"
|
||||
#include "tinyusb.h"
|
||||
|
||||
static const char *TAG = "config_test";
|
||||
|
||||
#define TEARDOWN_DEVICE_ATTACH_TIMEOUT_MS 1000
|
||||
#define TEARDOWN_DEVICE_DETACH_DELAY_MS 1000
|
||||
|
||||
// ========================= TinyUSB descriptors ===============================
|
||||
|
||||
// Here we need to create dual CDC device, to match the CONFIG_TINYUSB_CDC_COUNT from sdkconfig.defaults
|
||||
static const uint16_t cdc_desc_config_len = TUD_CONFIG_DESC_LEN + CFG_TUD_CDC * TUD_CDC_DESC_LEN;
|
||||
static const uint8_t test_fs_configuration_descriptor[] = {
|
||||
// Config number, interface count, string index, total length, attribute, power in mA
|
||||
TUD_CONFIG_DESCRIPTOR(1, CFG_TUD_CDC * 2, 0, cdc_desc_config_len, TUSB_DESC_CONFIG_ATT_SELF_POWERED | TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
|
||||
TUD_CDC_DESCRIPTOR(0, 4, 0x81, 8, 0x02, 0x82, 64),
|
||||
#if CFG_TUD_CDC > 1
|
||||
TUD_CDC_DESCRIPTOR(2, 4, 0x83, 8, 0x04, 0x84, 64),
|
||||
#endif
|
||||
};
|
||||
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
static const uint8_t test_hs_configuration_descriptor[] = {
|
||||
// Config number, interface count, string index, total length, attribute, power in mA
|
||||
TUD_CONFIG_DESCRIPTOR(1, 4, 0, cdc_desc_config_len, TUSB_DESC_CONFIG_ATT_SELF_POWERED | TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
|
||||
TUD_CDC_DESCRIPTOR(0, 4, 0x81, 8, 0x02, 0x82, 512),
|
||||
TUD_CDC_DESCRIPTOR(2, 4, 0x83, 8, 0x04, 0x84, 512),
|
||||
};
|
||||
|
||||
static const tusb_desc_device_qualifier_t device_qualifier = {
|
||||
.bLength = sizeof(tusb_desc_device_qualifier_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
|
||||
.bcdUSB = 0x0200,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.bNumConfigurations = 0x01,
|
||||
.bReserved = 0
|
||||
};
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
|
||||
static const tusb_desc_device_t test_device_descriptor = {
|
||||
.bLength = sizeof(test_device_descriptor),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
.bcdUSB = 0x0200,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.idVendor = 0x303A, // This is Espressif VID. This needs to be changed according to Users / Customers
|
||||
.idProduct = 0x4002,
|
||||
.bcdDevice = 0x100,
|
||||
.iManufacturer = 0x01,
|
||||
.iProduct = 0x02,
|
||||
.iSerialNumber = 0x03,
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
// ========================== Private logic ====================================
|
||||
SemaphoreHandle_t wait_mount = NULL;
|
||||
|
||||
/**
|
||||
* @brief Creates test additional resources.
|
||||
*
|
||||
* Is called before start test to create/init internal resources.
|
||||
*/
|
||||
static bool __test_init(void)
|
||||
{
|
||||
wait_mount = xSemaphoreCreateBinary();
|
||||
return (wait_mount != NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Indicates device connection event.
|
||||
*
|
||||
* Is called in tud_mount callback.
|
||||
*/
|
||||
static void __test_conn(void)
|
||||
{
|
||||
if (wait_mount) {
|
||||
xSemaphoreGive(wait_mount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Awaits device connection event.
|
||||
*
|
||||
* Is used for waiting the event in test logic.
|
||||
* Timeout could be configured via TEARDOWN_DEVICE_ATTACH_TIMEOUT_MS define.
|
||||
*/
|
||||
static esp_err_t __test_wait_conn(void)
|
||||
{
|
||||
if (!wait_mount) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
return ( xSemaphoreTake(wait_mount, pdMS_TO_TICKS(TEARDOWN_DEVICE_ATTACH_TIMEOUT_MS))
|
||||
? ESP_OK
|
||||
: ESP_ERR_TIMEOUT );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Releases test resources.
|
||||
*
|
||||
* Is called in the end of the test to release internal resources.
|
||||
*/
|
||||
static void __test_release(void)
|
||||
{
|
||||
if (wait_mount) {
|
||||
vSemaphoreDelete(wait_mount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief One round of setup configuration for TinyUSB.
|
||||
*
|
||||
* Steps:
|
||||
* 1. Init internal resources
|
||||
* 2. Installs TinyUSB with provided configuration
|
||||
* 3. Waits for the connection event from the Host
|
||||
* 4. Gives some extra time for Host to configure the device (configures via TEARDOWN_DEVICE_DETACH_DELAY_MS)
|
||||
* 5. Uninstall TinyUSB
|
||||
* 6. Release internal resources
|
||||
*
|
||||
* Is called in the end of the test to release internal resources.
|
||||
*/
|
||||
static void __test_tinyusb_set_config(const tinyusb_config_t *tusb_cfg)
|
||||
{
|
||||
TEST_ASSERT_EQUAL(true, __test_init());
|
||||
TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_install(tusb_cfg));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, __test_wait_conn());
|
||||
vTaskDelay(pdMS_TO_TICKS(TEARDOWN_DEVICE_DETACH_DELAY_MS));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall());
|
||||
__test_release();
|
||||
}
|
||||
|
||||
// ========================== Callbacks ========================================
|
||||
/**
|
||||
* @brief TinyUSB callback for device mount.
|
||||
*
|
||||
* @note
|
||||
* For Linux-based Hosts: Reflects the SetConfiguration() request from the Host Driver.
|
||||
* For Win-based Hosts: SetConfiguration() request is present only with available Class in device descriptor.
|
||||
*/
|
||||
void tud_mount_cb(void)
|
||||
{
|
||||
ESP_LOGD(TAG, "%s", __FUNCTION__);
|
||||
__test_conn();
|
||||
}
|
||||
|
||||
// ============================= Tests =========================================
|
||||
/**
|
||||
* @brief TinyUSB Configuration test case.
|
||||
*
|
||||
* Verifies:
|
||||
* Configuration without specifying any parameters.
|
||||
* Configuration & descriptors are provided by esp_tinyusb wrapper.
|
||||
*/
|
||||
TEST_CASE("descriptors_config_all_default", "[esp_tinyusb][usb_device][config]")
|
||||
{
|
||||
const tinyusb_config_t tusb_cfg = {
|
||||
.external_phy = false,
|
||||
.device_descriptor = NULL,
|
||||
.configuration_descriptor = NULL,
|
||||
#if (CONFIG_TINYUSB_RHPORT_HS)
|
||||
.hs_configuration_descriptor = NULL,
|
||||
#endif // CONFIG_TINYUSB_RHPORT_HS
|
||||
};
|
||||
// Install TinyUSB driver
|
||||
__test_tinyusb_set_config(&tusb_cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TinyUSB Configuration test case.
|
||||
*
|
||||
* Verifies:
|
||||
* Configuration with specifying only device descriptor.
|
||||
* For High-speed qualifier descriptor as well.
|
||||
*/
|
||||
TEST_CASE("descriptors_config_device", "[esp_tinyusb][usb_device][config]")
|
||||
{
|
||||
const tinyusb_config_t tusb_cfg = {
|
||||
.external_phy = false,
|
||||
.device_descriptor = &test_device_descriptor,
|
||||
.configuration_descriptor = NULL,
|
||||
#if (CONFIG_TINYUSB_RHPORT_HS)
|
||||
.hs_configuration_descriptor = NULL,
|
||||
.qualifier_descriptor = &device_qualifier,
|
||||
#endif // CONFIG_TINYUSB_RHPORT_HS
|
||||
};
|
||||
__test_tinyusb_set_config(&tusb_cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TinyUSB Configuration test case.
|
||||
*
|
||||
* Verifies:
|
||||
* Configuration with specifying device & configuration descriptors.
|
||||
*
|
||||
* For High-speed:
|
||||
* - HS configuration descriptor is not provided by user (legacy compatibility) and default HS config descriptor is using when possible.
|
||||
* - Qualifier descriptor.
|
||||
*/
|
||||
TEST_CASE("descriptors_config_device_and_config", "[esp_tinyusb][usb_device][config]")
|
||||
{
|
||||
const tinyusb_config_t tusb_cfg = {
|
||||
.external_phy = false,
|
||||
.device_descriptor = &test_device_descriptor,
|
||||
.configuration_descriptor = test_fs_configuration_descriptor,
|
||||
#if (CONFIG_TINYUSB_RHPORT_HS)
|
||||
.hs_configuration_descriptor = NULL,
|
||||
.qualifier_descriptor = &device_qualifier,
|
||||
#endif // CONFIG_TINYUSB_RHPORT_HS
|
||||
};
|
||||
__test_tinyusb_set_config(&tusb_cfg);
|
||||
}
|
||||
|
||||
#if (CONFIG_IDF_TARGET_ESP32P4)
|
||||
/**
|
||||
* @brief TinyUSB High-speed Configuration test case.
|
||||
*
|
||||
* Verifies:
|
||||
* Configuration with specifying device & HS configuration descriptor only.
|
||||
* FS configuration descriptor is not provided by user (legacy compatibility) and default configuration descriptor for FS is using when possible.
|
||||
*/
|
||||
TEST_CASE("descriptors_config_device_and_hs_config_only", "[esp_tinyusb][usb_device][config]")
|
||||
{
|
||||
const tinyusb_config_t tusb_cfg = {
|
||||
.external_phy = false,
|
||||
.device_descriptor = &test_device_descriptor,
|
||||
.configuration_descriptor = NULL,
|
||||
.hs_configuration_descriptor = test_hs_configuration_descriptor,
|
||||
.qualifier_descriptor = &device_qualifier,
|
||||
};
|
||||
__test_tinyusb_set_config(&tusb_cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TinyUSB High-speed Configuration test case.
|
||||
*
|
||||
* Verifies:
|
||||
* Configuration with specifying all descriptors by user.
|
||||
*/
|
||||
TEST_CASE("descriptors_config_all_configured", "[esp_tinyusb][usb_device][config]")
|
||||
{
|
||||
const tinyusb_config_t tusb_cfg = {
|
||||
.external_phy = false,
|
||||
.device_descriptor = &test_device_descriptor,
|
||||
.fs_configuration_descriptor = test_fs_configuration_descriptor,
|
||||
.hs_configuration_descriptor = test_hs_configuration_descriptor,
|
||||
.qualifier_descriptor = &device_qualifier,
|
||||
};
|
||||
__test_tinyusb_set_config(&tusb_cfg);
|
||||
}
|
||||
#endif // CONFIG_IDF_TARGET_ESP32P4
|
||||
|
||||
#endif // SOC_USB_OTG_SUPPORTED
|
||||
@@ -0,0 +1,13 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import pytest
|
||||
from pytest_embedded_idf.dut import IdfDut
|
||||
|
||||
|
||||
@pytest.mark.esp32s2
|
||||
@pytest.mark.esp32s3
|
||||
@pytest.mark.esp32p4
|
||||
@pytest.mark.usb_device
|
||||
def test_usb_device_configuration(dut: IdfDut) -> None:
|
||||
dut.run_all_single_board_cases(group='config')
|
||||
@@ -0,0 +1,16 @@
|
||||
# Configure TinyUSB, it will be used to mock USB devices
|
||||
CONFIG_TINYUSB_CDC_ENABLED=y
|
||||
CONFIG_TINYUSB_CDC_COUNT=2
|
||||
|
||||
# Disable watchdogs, they'd get triggered during unity interactive menu
|
||||
CONFIG_ESP_INT_WDT=n
|
||||
CONFIG_ESP_TASK_WDT=n
|
||||
|
||||
# Run-time checks of Heap and Stack
|
||||
CONFIG_HEAP_POISONING_COMPREHENSIVE=y
|
||||
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
|
||||
CONFIG_COMPILER_STACK_CHECK=y
|
||||
|
||||
CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL=y
|
||||
|
||||
CONFIG_COMPILER_CXX_EXCEPTIONS=y
|
||||
@@ -0,0 +1,9 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
set(COMPONENTS main)
|
||||
|
||||
project(test_app_dconn_detection)
|
||||
@@ -0,0 +1,4 @@
|
||||
idf_component_register(SRC_DIRS .
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES unity
|
||||
WHOLE_ARCHIVE)
|
||||
@@ -0,0 +1,5 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
espressif/esp_tinyusb:
|
||||
version: "*"
|
||||
override_path: "../../../"
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "unity.h"
|
||||
#include "unity_test_runner.h"
|
||||
#include "unity_test_utils_memory.h"
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/*
|
||||
_ _ _
|
||||
| | (_) | |
|
||||
___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__
|
||||
/ _ \/ __| '_ \| __| | '_ \| | | | | | / __| '_ \
|
||||
| __/\__ \ |_) | |_| | | | | |_| | |_| \__ \ |_) |
|
||||
\___||___/ .__/ \__|_|_| |_|\__, |\__,_|___/_.__/
|
||||
| |______ __/ |
|
||||
|_|______| |___/
|
||||
_____ _____ _____ _____
|
||||
|_ _| ___/ ___|_ _|
|
||||
| | | |__ \ `--. | |
|
||||
| | | __| `--. \ | |
|
||||
| | | |___/\__/ / | |
|
||||
\_/ \____/\____/ \_/
|
||||
*/
|
||||
|
||||
printf(" _ _ _ \n");
|
||||
printf(" | | (_) | | \n");
|
||||
printf(" ___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__ \n");
|
||||
printf(" / _ \\/ __| '_ \\| __| | '_ \\| | | | | | / __| '_ \\ \n");
|
||||
printf("| __/\\__ \\ |_) | |_| | | | | |_| | |_| \\__ \\ |_) |\n");
|
||||
printf(" \\___||___/ .__/ \\__|_|_| |_|\\__, |\\__,_|___/_.__/ \n");
|
||||
printf(" | |______ __/ | \n");
|
||||
printf(" |_|______| |___/ \n");
|
||||
printf(" _____ _____ _____ _____ \n");
|
||||
printf("|_ _| ___/ ___|_ _| \n");
|
||||
printf(" | | | |__ \\ `--. | | \n");
|
||||
printf(" | | | __| `--. \\ | | \n");
|
||||
printf(" | | | |___/\\__/ / | | \n");
|
||||
printf(" \\_/ \\____/\\____/ \\_/ \n");
|
||||
|
||||
unity_utils_setup_heap_record(80);
|
||||
unity_utils_set_leak_level(128);
|
||||
unity_run_menu();
|
||||
}
|
||||
|
||||
/* setUp runs before every test */
|
||||
void setUp(void)
|
||||
{
|
||||
unity_utils_record_free_mem();
|
||||
}
|
||||
|
||||
/* tearDown runs after every test */
|
||||
void tearDown(void)
|
||||
{
|
||||
unity_utils_evaluate_leaks();
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "soc/soc_caps.h"
|
||||
|
||||
#if SOC_USB_OTG_SUPPORTED
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_system.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_rom_gpio.h"
|
||||
#include "soc/gpio_sig_map.h"
|
||||
#include "unity.h"
|
||||
#include "tinyusb.h"
|
||||
#include "tusb_tasks.h"
|
||||
|
||||
#define DEVICE_DETACH_TEST_ROUNDS 10
|
||||
#define DEVICE_DETACH_ROUND_DELAY_MS 1000
|
||||
|
||||
#if (CONFIG_IDF_TARGET_ESP32P4)
|
||||
#define USB_SRP_BVALID_IN_IDX USB_SRP_BVALID_PAD_IN_IDX
|
||||
#endif // CONFIG_IDF_TARGET_ESP32P4
|
||||
|
||||
/* TinyUSB descriptors
|
||||
********************************************************************* */
|
||||
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN)
|
||||
|
||||
static unsigned int dev_mounted = 0;
|
||||
static unsigned int dev_umounted = 0;
|
||||
|
||||
static uint8_t const test_configuration_descriptor[] = {
|
||||
// Config number, interface count, string index, total length, attribute, power in mA
|
||||
TUD_CONFIG_DESCRIPTOR(1, 0, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_SELF_POWERED | TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
|
||||
};
|
||||
|
||||
static const tusb_desc_device_t test_device_descriptor = {
|
||||
.bLength = sizeof(test_device_descriptor),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
.bcdUSB = 0x0200,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.idVendor = 0x303A, // This is Espressif VID. This needs to be changed according to Users / Customers
|
||||
.idProduct = 0x4002,
|
||||
.bcdDevice = 0x100,
|
||||
.iManufacturer = 0x01,
|
||||
.iProduct = 0x02,
|
||||
.iSerialNumber = 0x03,
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
static const tusb_desc_device_qualifier_t device_qualifier = {
|
||||
.bLength = sizeof(tusb_desc_device_qualifier_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
|
||||
.bcdUSB = 0x0200,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.bNumConfigurations = 0x01,
|
||||
.bReserved = 0
|
||||
};
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
|
||||
// Invoked when device is mounted
|
||||
void tud_mount_cb(void)
|
||||
{
|
||||
/**
|
||||
* @attention Tests relying on this callback only pass on Linux USB Host!
|
||||
*
|
||||
* This callback is issued after SetConfiguration command from USB Host.
|
||||
* However, Windows issues SetConfiguration only after a USB driver was assigned to the device.
|
||||
* So in case you are implementing a Vendor Specific class, or your device has 0 interfaces, this callback is not issued on Windows host.
|
||||
*/
|
||||
printf("%s\n", __FUNCTION__);
|
||||
dev_mounted++;
|
||||
}
|
||||
|
||||
// Invoked when device is unmounted
|
||||
void tud_umount_cb(void)
|
||||
{
|
||||
printf("%s\n", __FUNCTION__);
|
||||
dev_umounted++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TinyUSB Disconnect Detection test case
|
||||
*
|
||||
* This is specific artificial test for verifying the disconnection detection event.
|
||||
* Normally, this event comes as a result of detaching USB device from the port and disappearing the VBUS voltage.
|
||||
* In this test case, we use GPIO matrix and connect the signal to the ZERO or ONE constant inputs.
|
||||
* Connection to constant ONE input emulates the attachment to the USB Host port (appearing VBUS).
|
||||
* Connection to constant ZERO input emulates the detachment from the USB Host port (removing VBUS).
|
||||
*
|
||||
* Test logic:
|
||||
* - Install TinyUSB Device stack without any class
|
||||
* - In cycle:
|
||||
* - Emulate the detachment, get the tud_umount_cb(), increase the dev_umounted value
|
||||
* - Emulate the attachment, get the tud_mount_cb(), increase the dev_mounted value
|
||||
* - Verify that dev_umounted == dev_mounted
|
||||
* - Verify that dev_mounted == DEVICE_DETACH_TEST_ROUNDS, where DEVICE_DETACH_TEST_ROUNDS - amount of rounds
|
||||
* - Uninstall TinyUSB Device stack
|
||||
*
|
||||
*/
|
||||
TEST_CASE("dconn_detection", "[esp_tinyusb][dconn]")
|
||||
{
|
||||
unsigned int rounds = DEVICE_DETACH_TEST_ROUNDS;
|
||||
|
||||
// Install TinyUSB driver
|
||||
const tinyusb_config_t tusb_cfg = {
|
||||
.device_descriptor = &test_device_descriptor,
|
||||
.string_descriptor = NULL,
|
||||
.string_descriptor_count = 0,
|
||||
.external_phy = false,
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
.fs_configuration_descriptor = test_configuration_descriptor,
|
||||
.hs_configuration_descriptor = test_configuration_descriptor,
|
||||
.qualifier_descriptor = &device_qualifier,
|
||||
#else
|
||||
.configuration_descriptor = test_configuration_descriptor,
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
};
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_install(&tusb_cfg));
|
||||
|
||||
dev_mounted = 0;
|
||||
dev_umounted = 0;
|
||||
|
||||
while (rounds--) {
|
||||
// LOW to emulate disconnect USB device
|
||||
esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, USB_SRP_BVALID_IN_IDX, false);
|
||||
vTaskDelay(pdMS_TO_TICKS(DEVICE_DETACH_ROUND_DELAY_MS));
|
||||
// HIGH to emulate connect USB device
|
||||
esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, USB_SRP_BVALID_IN_IDX, false);
|
||||
vTaskDelay(pdMS_TO_TICKS(DEVICE_DETACH_ROUND_DELAY_MS));
|
||||
}
|
||||
|
||||
// Verify
|
||||
TEST_ASSERT_EQUAL(dev_umounted, dev_mounted);
|
||||
TEST_ASSERT_EQUAL(DEVICE_DETACH_TEST_ROUNDS, dev_mounted);
|
||||
|
||||
// Cleanup
|
||||
TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall());
|
||||
}
|
||||
#endif // SOC_USB_OTG_SUPPORTED
|
||||
@@ -0,0 +1,13 @@
|
||||
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import pytest
|
||||
from pytest_embedded_idf.dut import IdfDut
|
||||
|
||||
|
||||
@pytest.mark.esp32s2
|
||||
@pytest.mark.esp32s3
|
||||
@pytest.mark.esp32p4
|
||||
#@pytest.mark.usb_device Disable in CI: unavailable teardown for P4
|
||||
def test_usb_device_dconn_detection(dut: IdfDut) -> None:
|
||||
dut.run_all_single_board_cases(group='dconn')
|
||||
@@ -0,0 +1,18 @@
|
||||
# Configure TinyUSB, it will be used to mock USB devices
|
||||
CONFIG_TINYUSB_MSC_ENABLED=n
|
||||
CONFIG_TINYUSB_CDC_ENABLED=n
|
||||
CONFIG_TINYUSB_CDC_COUNT=0
|
||||
CONFIG_TINYUSB_HID_COUNT=0
|
||||
|
||||
# Disable watchdogs, they'd get triggered during unity interactive menu
|
||||
CONFIG_ESP_INT_WDT=n
|
||||
CONFIG_ESP_TASK_WDT=n
|
||||
|
||||
# Run-time checks of Heap and Stack
|
||||
CONFIG_HEAP_POISONING_COMPREHENSIVE=y
|
||||
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
|
||||
CONFIG_COMPILER_STACK_CHECK=y
|
||||
|
||||
CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL=y
|
||||
|
||||
CONFIG_COMPILER_CXX_EXCEPTIONS=y
|
||||
@@ -0,0 +1,9 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
set(COMPONENTS main)
|
||||
|
||||
project(test_app_default_task_without_init)
|
||||
@@ -0,0 +1,4 @@
|
||||
idf_component_register(SRC_DIRS .
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES unity
|
||||
WHOLE_ARCHIVE)
|
||||
@@ -0,0 +1,5 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
espressif/esp_tinyusb:
|
||||
version: "*"
|
||||
override_path: "../../../"
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "unity.h"
|
||||
#include "unity_test_runner.h"
|
||||
#include "unity_test_utils_memory.h"
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/*
|
||||
_ _ _
|
||||
| | (_) | |
|
||||
___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__
|
||||
/ _ \/ __| '_ \| __| | '_ \| | | | | | / __| '_ \
|
||||
| __/\__ \ |_) | |_| | | | | |_| | |_| \__ \ |_) |
|
||||
\___||___/ .__/ \__|_|_| |_|\__, |\__,_|___/_.__/
|
||||
| |______ __/ |
|
||||
|_|______| |___/
|
||||
_____ _____ _____ _____
|
||||
|_ _| ___/ ___|_ _|
|
||||
| | | |__ \ `--. | |
|
||||
| | | __| `--. \ | |
|
||||
| | | |___/\__/ / | |
|
||||
\_/ \____/\____/ \_/
|
||||
*/
|
||||
|
||||
printf(" _ _ _ \n");
|
||||
printf(" | | (_) | | \n");
|
||||
printf(" ___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__ \n");
|
||||
printf(" / _ \\/ __| '_ \\| __| | '_ \\| | | | | | / __| '_ \\ \n");
|
||||
printf("| __/\\__ \\ |_) | |_| | | | | |_| | |_| \\__ \\ |_) |\n");
|
||||
printf(" \\___||___/ .__/ \\__|_|_| |_|\\__, |\\__,_|___/_.__/ \n");
|
||||
printf(" | |______ __/ | \n");
|
||||
printf(" |_|______| |___/ \n");
|
||||
printf(" _____ _____ _____ _____ \n");
|
||||
printf("|_ _| ___/ ___|_ _| \n");
|
||||
printf(" | | | |__ \\ `--. | | \n");
|
||||
printf(" | | | __| `--. \\ | | \n");
|
||||
printf(" | | | |___/\\__/ / | | \n");
|
||||
printf(" \\_/ \\____/\\____/ \\_/ \n");
|
||||
|
||||
unity_utils_setup_heap_record(80);
|
||||
unity_utils_set_leak_level(128);
|
||||
unity_run_menu();
|
||||
}
|
||||
|
||||
/* setUp runs before every test */
|
||||
void setUp(void)
|
||||
{
|
||||
unity_utils_record_free_mem();
|
||||
}
|
||||
|
||||
/* tearDown runs after every test */
|
||||
void tearDown(void)
|
||||
{
|
||||
unity_utils_evaluate_leaks();
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "soc/soc_caps.h"
|
||||
#if SOC_USB_OTG_SUPPORTED
|
||||
|
||||
//
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
//
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
//
|
||||
#include "esp_system.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
//
|
||||
#include "unity.h"
|
||||
#include "tinyusb.h"
|
||||
|
||||
static const char *TAG = "default_task";
|
||||
|
||||
SemaphoreHandle_t wait_mount = NULL;
|
||||
|
||||
#define TEARDOWN_DEVICE_DELAY_MS 1000
|
||||
|
||||
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN)
|
||||
static uint8_t const test_configuration_descriptor[] = {
|
||||
// Config number, interface count, string index, total length, attribute, power in mA
|
||||
TUD_CONFIG_DESCRIPTOR(1, 0, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_SELF_POWERED | TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
|
||||
};
|
||||
|
||||
static const tusb_desc_device_t test_device_descriptor = {
|
||||
.bLength = sizeof(test_device_descriptor),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
.bcdUSB = 0x0200,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.idVendor = 0x303A, // This is Espressif VID. This needs to be changed according to Users / Customers
|
||||
.idProduct = 0x4002,
|
||||
.bcdDevice = 0x100,
|
||||
.iManufacturer = 0x01,
|
||||
.iProduct = 0x02,
|
||||
.iSerialNumber = 0x03,
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
static const tusb_desc_device_qualifier_t device_qualifier = {
|
||||
.bLength = sizeof(tusb_desc_device_qualifier_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
|
||||
.bcdUSB = 0x0200,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.bNumConfigurations = 0x01,
|
||||
.bReserved = 0
|
||||
};
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
|
||||
// Invoked when device is mounted
|
||||
void tud_mount_cb(void)
|
||||
{
|
||||
xSemaphoreGive(wait_mount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TinyUSB Task specific testcase
|
||||
*
|
||||
* Scenario:
|
||||
* 1. Install TinyUSB driver
|
||||
* 2. Wait tud_mount_cb() until TUSB_DEVICE_DELAY_MS
|
||||
* 3. Wait TUSB_DEVICE_DELAY_MS
|
||||
* 4. Teardown TinyUSB
|
||||
* 5. Release resources
|
||||
*/
|
||||
TEST_CASE("tinyusb_default_task_with_init", "[esp_tinyusb][tusb_task]")
|
||||
{
|
||||
wait_mount = xSemaphoreCreateBinary();
|
||||
TEST_ASSERT_NOT_EQUAL(NULL, wait_mount);
|
||||
|
||||
// TinyUSB driver configuration
|
||||
const tinyusb_config_t tusb_cfg = {
|
||||
.device_descriptor = &test_device_descriptor,
|
||||
.string_descriptor = NULL,
|
||||
.string_descriptor_count = 0,
|
||||
.external_phy = false,
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
.fs_configuration_descriptor = test_configuration_descriptor,
|
||||
.hs_configuration_descriptor = test_configuration_descriptor,
|
||||
.qualifier_descriptor = &device_qualifier,
|
||||
#else
|
||||
.configuration_descriptor = test_configuration_descriptor,
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
};
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_install(&tusb_cfg));
|
||||
// Wait for the usb event
|
||||
ESP_LOGD(TAG, "wait mount...");
|
||||
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(wait_mount, pdMS_TO_TICKS(TEARDOWN_DEVICE_DELAY_MS)));
|
||||
ESP_LOGD(TAG, "mounted");
|
||||
|
||||
// Teardown
|
||||
vTaskDelay(pdMS_TO_TICKS(TEARDOWN_DEVICE_DELAY_MS));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall());
|
||||
// Remove primitives
|
||||
vSemaphoreDelete(wait_mount);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import pytest
|
||||
from pytest_embedded_idf.dut import IdfDut
|
||||
|
||||
@pytest.mark.esp32s2
|
||||
@pytest.mark.esp32s3
|
||||
@pytest.mark.esp32p4
|
||||
@pytest.mark.usb_device
|
||||
def test_usb_default_task_with_init(dut: IdfDut) -> None:
|
||||
dut.run_all_single_board_cases(group='tusb_task')
|
||||
@@ -0,0 +1,16 @@
|
||||
# Configure TinyUSB
|
||||
CONFIG_TINYUSB_NO_DEFAULT_TASK=n
|
||||
CONFIG_TINYUSB_INIT_IN_DEFAULT_TASK=n
|
||||
|
||||
# Disable watchdogs, they'd get triggered during unity interactive menu
|
||||
CONFIG_ESP_INT_WDT=n
|
||||
CONFIG_ESP_TASK_WDT=n
|
||||
|
||||
# Run-time checks of Heap and Stack
|
||||
CONFIG_HEAP_POISONING_COMPREHENSIVE=y
|
||||
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
|
||||
CONFIG_COMPILER_STACK_CHECK=y
|
||||
|
||||
CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL=y
|
||||
|
||||
CONFIG_COMPILER_CXX_EXCEPTIONS=y
|
||||
@@ -0,0 +1,9 @@
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
set(COMPONENTS main)
|
||||
|
||||
project(test_app_default_task_with_init)
|
||||
@@ -0,0 +1,4 @@
|
||||
idf_component_register(SRC_DIRS .
|
||||
INCLUDE_DIRS .
|
||||
REQUIRES unity
|
||||
WHOLE_ARCHIVE)
|
||||
@@ -0,0 +1,5 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
espressif/esp_tinyusb:
|
||||
version: "*"
|
||||
override_path: "../../../"
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "unity.h"
|
||||
#include "unity_test_runner.h"
|
||||
#include "unity_test_utils_memory.h"
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
/*
|
||||
_ _ _
|
||||
| | (_) | |
|
||||
___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__
|
||||
/ _ \/ __| '_ \| __| | '_ \| | | | | | / __| '_ \
|
||||
| __/\__ \ |_) | |_| | | | | |_| | |_| \__ \ |_) |
|
||||
\___||___/ .__/ \__|_|_| |_|\__, |\__,_|___/_.__/
|
||||
| |______ __/ |
|
||||
|_|______| |___/
|
||||
_____ _____ _____ _____
|
||||
|_ _| ___/ ___|_ _|
|
||||
| | | |__ \ `--. | |
|
||||
| | | __| `--. \ | |
|
||||
| | | |___/\__/ / | |
|
||||
\_/ \____/\____/ \_/
|
||||
*/
|
||||
|
||||
printf(" _ _ _ \n");
|
||||
printf(" | | (_) | | \n");
|
||||
printf(" ___ ___ _ __ | |_ _ _ __ _ _ _ _ ___| |__ \n");
|
||||
printf(" / _ \\/ __| '_ \\| __| | '_ \\| | | | | | / __| '_ \\ \n");
|
||||
printf("| __/\\__ \\ |_) | |_| | | | | |_| | |_| \\__ \\ |_) |\n");
|
||||
printf(" \\___||___/ .__/ \\__|_|_| |_|\\__, |\\__,_|___/_.__/ \n");
|
||||
printf(" | |______ __/ | \n");
|
||||
printf(" |_|______| |___/ \n");
|
||||
printf(" _____ _____ _____ _____ \n");
|
||||
printf("|_ _| ___/ ___|_ _| \n");
|
||||
printf(" | | | |__ \\ `--. | | \n");
|
||||
printf(" | | | __| `--. \\ | | \n");
|
||||
printf(" | | | |___/\\__/ / | | \n");
|
||||
printf(" \\_/ \\____/\\____/ \\_/ \n");
|
||||
|
||||
unity_utils_setup_heap_record(80);
|
||||
unity_utils_set_leak_level(128);
|
||||
unity_run_menu();
|
||||
}
|
||||
|
||||
/* setUp runs before every test */
|
||||
void setUp(void)
|
||||
{
|
||||
unity_utils_record_free_mem();
|
||||
}
|
||||
|
||||
/* tearDown runs after every test */
|
||||
void tearDown(void)
|
||||
{
|
||||
unity_utils_evaluate_leaks();
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "soc/soc_caps.h"
|
||||
#if SOC_USB_OTG_SUPPORTED
|
||||
|
||||
//
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
//
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
//
|
||||
#include "esp_system.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
//
|
||||
#include "unity.h"
|
||||
#include "tinyusb.h"
|
||||
|
||||
static const char *TAG = "default_task_with_init";
|
||||
|
||||
SemaphoreHandle_t wait_mount = NULL;
|
||||
|
||||
#define TEARDOWN_DEVICE_DELAY_MS 1000
|
||||
|
||||
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN)
|
||||
static uint8_t const test_configuration_descriptor[] = {
|
||||
// Config number, interface count, string index, total length, attribute, power in mA
|
||||
TUD_CONFIG_DESCRIPTOR(1, 0, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_SELF_POWERED | TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
|
||||
};
|
||||
|
||||
static const tusb_desc_device_t test_device_descriptor = {
|
||||
.bLength = sizeof(test_device_descriptor),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE,
|
||||
.bcdUSB = 0x0200,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.idVendor = 0x303A, // This is Espressif VID. This needs to be changed according to Users / Customers
|
||||
.idProduct = 0x4002,
|
||||
.bcdDevice = 0x100,
|
||||
.iManufacturer = 0x01,
|
||||
.iProduct = 0x02,
|
||||
.iSerialNumber = 0x03,
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
static const tusb_desc_device_qualifier_t device_qualifier = {
|
||||
.bLength = sizeof(tusb_desc_device_qualifier_t),
|
||||
.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
|
||||
.bcdUSB = 0x0200,
|
||||
.bDeviceClass = TUSB_CLASS_MISC,
|
||||
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
|
||||
.bDeviceProtocol = MISC_PROTOCOL_IAD,
|
||||
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
|
||||
.bNumConfigurations = 0x01,
|
||||
.bReserved = 0
|
||||
};
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
|
||||
// Invoked when device is mounted
|
||||
void tud_mount_cb(void)
|
||||
{
|
||||
xSemaphoreGive(wait_mount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TinyUSB Task specific testcase
|
||||
*
|
||||
* Scenario:
|
||||
* 1. Install TinyUSB driver
|
||||
* 2. Wait tud_mount_cb() until TUSB_DEVICE_DELAY_MS
|
||||
* 3. Wait TUSB_DEVICE_DELAY_MS
|
||||
* 4. Teardown TinyUSB
|
||||
* 5. Release resources
|
||||
*/
|
||||
TEST_CASE("tinyusb_default_task_with_init", "[esp_tinyusb][tusb_task]")
|
||||
{
|
||||
wait_mount = xSemaphoreCreateBinary();
|
||||
TEST_ASSERT_NOT_EQUAL(NULL, wait_mount);
|
||||
|
||||
// TinyUSB driver configuration
|
||||
const tinyusb_config_t tusb_cfg = {
|
||||
.device_descriptor = &test_device_descriptor,
|
||||
.string_descriptor = NULL,
|
||||
.string_descriptor_count = 0,
|
||||
.external_phy = false,
|
||||
#if (TUD_OPT_HIGH_SPEED)
|
||||
.fs_configuration_descriptor = test_configuration_descriptor,
|
||||
.hs_configuration_descriptor = test_configuration_descriptor,
|
||||
.qualifier_descriptor = &device_qualifier,
|
||||
#else
|
||||
.configuration_descriptor = test_configuration_descriptor,
|
||||
#endif // TUD_OPT_HIGH_SPEED
|
||||
};
|
||||
|
||||
TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_install(&tusb_cfg));
|
||||
// Wait for the usb event
|
||||
ESP_LOGD(TAG, "wait mount...");
|
||||
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(wait_mount, pdMS_TO_TICKS(TEARDOWN_DEVICE_DELAY_MS)));
|
||||
ESP_LOGD(TAG, "mounted");
|
||||
|
||||
// Teardown
|
||||
vTaskDelay(pdMS_TO_TICKS(TEARDOWN_DEVICE_DELAY_MS));
|
||||
TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall());
|
||||
// Remove primitives
|
||||
vSemaphoreDelete(wait_mount);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import pytest
|
||||
from pytest_embedded_idf.dut import IdfDut
|
||||
|
||||
@pytest.mark.esp32s2
|
||||
@pytest.mark.esp32s3
|
||||
@pytest.mark.esp32p4
|
||||
@pytest.mark.usb_device
|
||||
def test_usb_default_task_with_init(dut: IdfDut) -> None:
|
||||
dut.run_all_single_board_cases(group='tusb_task')
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user