Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd23b674d6 | ||
|
|
e4fa743654 | ||
|
|
44372c0108 | ||
|
|
5c214f3614 | ||
|
|
f4413b7969 | ||
|
|
cca1254740 | ||
|
|
5bb59c1422 | ||
|
|
cc5638b6e7 | ||
|
|
cc797a16df | ||
|
|
17a1b3f020 | ||
|
|
659aeb934a | ||
|
|
148259040c | ||
|
|
7ffba2b678 | ||
|
|
cfaa8ff637 | ||
|
|
1317de5cbe | ||
|
|
fac0ce6503 | ||
|
|
ae95ae6859 | ||
|
|
3efb0edb61 | ||
|
|
c5fdb1a207 | ||
|
|
28f50a750f | ||
|
|
1dd6630311 |
@@ -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:
|
||||
|
||||
7
.roo/commands/push_release.md
Normal file
7
.roo/commands/push_release.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
description: "This increments our git tag and version number, along with compiling and uploading a new release to our gitea page."
|
||||
---
|
||||
|
||||
Run increment_and_push.sh -r -p, and supply a good git commit message. For example:
|
||||
|
||||
./increment_and_push.sh "Fixed the bug with nip05 implementation"
|
||||
@@ -46,34 +46,48 @@ RUN cd /tmp && \
|
||||
|
||||
# Copy and build nostr_core_lib from project resources
|
||||
COPY resources/nostr_core_lib /build/nostr_core_lib/
|
||||
RUN cd /build/nostr_core_lib && \
|
||||
RUN if [ "$(uname -m)" = "aarch64" ] && ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
|
||||
ln -s "$(command -v gcc)" /usr/local/bin/aarch64-linux-gnu-gcc; \
|
||||
fi && \
|
||||
cd /build/nostr_core_lib && \
|
||||
chmod +x ./build.sh && \
|
||||
./build.sh --nips=1,4,6,19,44
|
||||
|
||||
# Copy source files
|
||||
COPY src/ /build/src/
|
||||
COPY resources/tui_continuous/ /build/resources/tui_continuous/
|
||||
|
||||
# Build nsigner as a fully static binary
|
||||
RUN gcc -static -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -s -Wall -Wextra -std=c99 \
|
||||
-I/build/nostr_core_lib \
|
||||
-I/build/nostr_core_lib/nostr_core \
|
||||
-I/build/nostr_core_lib/cjson \
|
||||
/build/src/main.c \
|
||||
/build/src/secure_mem.c \
|
||||
/build/src/mnemonic.c \
|
||||
/build/src/role_table.c \
|
||||
/build/src/selector.c \
|
||||
/build/src/enforcement.c \
|
||||
/build/src/dispatcher.c \
|
||||
/build/src/policy.c \
|
||||
/build/src/server.c \
|
||||
/build/src/transport_frame.c \
|
||||
/build/src/key_store.c \
|
||||
/build/src/socket_name.c \
|
||||
/build/nostr_core_lib/libnostr_core_x64.a \
|
||||
-o /build/nsigner_static \
|
||||
$(pkg-config --static --libs libcurl openssl) \
|
||||
-lsecp256k1 -lsqlite3 -lz -lpthread -lm
|
||||
RUN ARCH="$(uname -m)"; \
|
||||
case "$ARCH" in \
|
||||
aarch64|arm64) NOSTR_LIB="/build/nostr_core_lib/libnostr_core_arm64.a" ;; \
|
||||
x86_64|amd64) NOSTR_LIB="/build/nostr_core_lib/libnostr_core_x64.a" ;; \
|
||||
*) echo "Unsupported build arch: $ARCH"; exit 1 ;; \
|
||||
esac; \
|
||||
[ -f "$NOSTR_LIB" ] || { echo "Missing nostr core lib: $NOSTR_LIB"; ls -la /build/nostr_core_lib; exit 1; }; \
|
||||
gcc -static -Os -ffunction-sections -fdata-sections -Wl,--gc-sections -s -Wall -Wextra -std=c99 \
|
||||
-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 \
|
||||
/build/src/role_table.c \
|
||||
/build/src/selector.c \
|
||||
/build/src/enforcement.c \
|
||||
/build/src/dispatcher.c \
|
||||
/build/src/policy.c \
|
||||
/build/src/server.c \
|
||||
/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) \
|
||||
-lsecp256k1 -lsqlite3 -lz -lpthread -lm
|
||||
|
||||
RUN strip /build/nsigner_static || true
|
||||
RUN file /build/nsigner_static && \
|
||||
|
||||
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
|
||||
|
||||
@@ -105,12 +105,29 @@ echo "Platform: $PLATFORM"
|
||||
echo "Output: $BUILD_DIR/$OUTPUT_NAME"
|
||||
echo ""
|
||||
|
||||
if [ "$ARCH" != "$HOST_ARCH" ]; then
|
||||
echo "[0/3] Preparing buildx + QEMU for cross-architecture build"
|
||||
if ! docker buildx inspect >/dev/null 2>&1; then
|
||||
echo "ERROR: docker buildx is not available"
|
||||
exit 1
|
||||
fi
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all >/dev/null
|
||||
|
||||
if ! docker buildx inspect nsigner-builder >/dev/null 2>&1; then
|
||||
docker buildx create --name nsigner-builder --driver docker-container --use >/dev/null
|
||||
else
|
||||
docker buildx use nsigner-builder >/dev/null
|
||||
fi
|
||||
docker buildx inspect --bootstrap >/dev/null
|
||||
fi
|
||||
|
||||
echo "[1/3] Building builder stage from project root context"
|
||||
docker build \
|
||||
docker buildx build \
|
||||
--platform "$PLATFORM" \
|
||||
--target builder \
|
||||
-f "$DOCKERFILE" \
|
||||
-t "$IMAGE_TAG" \
|
||||
--load \
|
||||
"$SCRIPT_DIR"
|
||||
|
||||
echo "[2/3] Extracting static binary"
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
56
documents/N_OS_TR_AGENT_CHANGES.md
Normal file
56
documents/N_OS_TR_AGENT_CHANGES.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# n_OS_tr Agent Guide: Handling n_signer v0.0.16 Changes
|
||||
|
||||
This is a short operational guide for the n_OS_tr agent after the approval-model update in `n_signer`.
|
||||
|
||||
## What changed
|
||||
|
||||
- `n_signer` is now **deny-by-default**.
|
||||
- First-time caller/index combinations require approval unless preapproved.
|
||||
- `nostr_index` values can be auto-derived on approval.
|
||||
- Policy decisions are visible in activity labels: `:preapprove`, `:session-grant`, `:prompt`, `:no-match`.
|
||||
|
||||
## What the agent should do
|
||||
|
||||
1. **Always send explicit selector options** in the last `params` item:
|
||||
- Prefer `{"nostr_index": N}` for service identities.
|
||||
2. **Expect `policy_denied` on first contact** if caller/index is not preapproved.
|
||||
3. **Do not retry blindly** on `policy_denied`.
|
||||
- Surface a clear status: "approval needed".
|
||||
4. **Treat `unknown_role` as actionable**:
|
||||
- If using `nostr_index`, request again only after human approval or valid preapprove config.
|
||||
5. **Use stable caller identity** (service name / VM identity) so preapprove rules match reliably.
|
||||
|
||||
## Required startup pattern for n_OS_tr services
|
||||
|
||||
Launch `n_signer` with explicit `--preapprove` entries for each boot service that must sign without prompts.
|
||||
|
||||
Example:
|
||||
|
||||
```ini
|
||||
ExecStart=/usr/bin/n_signer \
|
||||
--preapprove caller=qubes:nostr-relay,role=main \
|
||||
--preapprove caller=qubes:dm-handler,nostr_index=1 \
|
||||
--preapprove caller=qubes:contacts,nostr_index=2 \
|
||||
--preapprove caller=qubes:zaps,nostr_index=3
|
||||
```
|
||||
|
||||
## Agent error-handling policy (recommended)
|
||||
|
||||
- `policy_denied` → stop, mark as "awaiting approval/preapprove".
|
||||
- `unknown_role` with `nostr_index` → stop, mark as "identity not yet approved/created".
|
||||
- `purpose_mismatch` / `curve_mismatch` → configuration bug; fail hard and alert.
|
||||
- `ambiguous_role_selector` → client bug; send exactly one selector field.
|
||||
|
||||
## Observability checks
|
||||
|
||||
When troubleshooting, inspect n_signer activity lines:
|
||||
|
||||
- `ALLOWED:preapprove` → expected for boot services.
|
||||
- `ALLOWED:session-grant` → interactive approval happened earlier this session.
|
||||
- `ALLOWED:prompt` → just approved now.
|
||||
- `DENIED:no-match` → no policy entry matched caller/index.
|
||||
|
||||
## Operational rule of thumb
|
||||
|
||||
For n_OS_tr system services, rely on `--preapprove` at startup.
|
||||
For ad-hoc/manual clients, rely on interactive prompt approvals.
|
||||
@@ -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
|
||||
|
||||
520
documents/SECURITY.md
Normal file
520
documents/SECURITY.md
Normal file
@@ -0,0 +1,520 @@
|
||||
# SECURITY.md
|
||||
|
||||
This document explains how `n_signer` enforces security, written so someone new can understand the model end-to-end without reading the source.
|
||||
|
||||
If you only read one section, read [§3 The three concepts](#3-the-three-concepts-identity-index-approval) and [§4 The deny-by-default rule](#4-the-deny-by-default-rule).
|
||||
|
||||
> This document describes the security model as implemented in the current release. Design rationale is in [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. What `n_signer` is, in security terms
|
||||
|
||||
`n_signer` is a **single foreground program** that holds a BIP-39 mnemonic in locked memory and signs requests on behalf of local clients. It is not a daemon, not a service, and not a key-management database.
|
||||
|
||||
The security posture is intentionally minimalist:
|
||||
|
||||
- **One process. One terminal. One human.** The terminal is the trust anchor and the only out-of-band approval surface.
|
||||
- **No persistence.** Nothing about the running session is written to disk: no config, no logs, no PID files, no socket pathnames, no state recovery.
|
||||
- **Crash equals total wipe.** All sensitive state — mnemonic, derived keys, the policy table, activity buffer — exists only in process RAM (`mlock`'d where applicable) and is unrecoverable after the process ends.
|
||||
- **Always-attended operation.** Every first request from a previously unknown caller requires an explicit human approval at the terminal, unless the caller was pre-approved at startup by the OS distribution. Human presence is part of the threat model, not an inconvenience.
|
||||
|
||||
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
|
||||
|
||||
### 2.1 What `n_signer` is designed to defend against
|
||||
|
||||
- **Untrusted local clients** asking the signer to perform operations they should not be allowed to perform.
|
||||
- **Drive-by signing** — an unattended program quietly producing signatures the user did not consent to.
|
||||
- **Accidental misuse** — a buggy client connecting to the wrong signer instance, or asking for an identity it should not be using.
|
||||
- **Post-mortem key recovery** — someone obtaining the disk after a crash, kill, or shutdown and recovering signing keys.
|
||||
|
||||
### 2.2 What `n_signer` does **not** defend against
|
||||
|
||||
- A compromised kernel or hypervisor (`mlock` cannot save you).
|
||||
- Physical access while the program is running and the terminal is unlocked.
|
||||
- A malicious user who knows the mnemonic — `n_signer` is a runtime gatekeeper, not a vault.
|
||||
- Side channels (timing, power, microarchitectural) that target the underlying crypto libraries.
|
||||
- Network adversaries when transports without authentication are explicitly enabled (e.g. `tcp_local`).
|
||||
- An OS distribution that ships a malicious `--preapprove` list in its system unit files. The OS is in scope of trust if it pre-approves anything; see [§7](#7-pre-approvals-the-os-distribution-pathway).
|
||||
|
||||
### 2.3 Trust anchors
|
||||
|
||||
| Anchor | Why it is trusted |
|
||||
|---|---|
|
||||
| The terminal `n_signer` is attached to | The user types the mnemonic into it and answers approval prompts on it. If the terminal is compromised, the session is compromised. |
|
||||
| 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.
|
||||
|
||||
---
|
||||
|
||||
## 3. The three concepts: identity, index, approval
|
||||
|
||||
This is the entire user-facing security model. There are exactly three things to know.
|
||||
|
||||
```text
|
||||
+----------------+ +-----------------------+ +---------------+
|
||||
| IDENTITY | | INDEX (or PATH) | | APPROVAL |
|
||||
| who is asking | +---- | which key to use | ----+ | is this OK? |
|
||||
+----------------+ | +-----------------------+ | +---------------+
|
||||
| |
|
||||
| request resolves to |
|
||||
+-------> (identity, index/path) -----+
|
||||
|
|
||||
v
|
||||
+----------------------------+
|
||||
| Approved entry exists? |
|
||||
+----------------------------+
|
||||
|yes |no
|
||||
v v
|
||||
+-----------------+ +----------------+
|
||||
| Sign / encrypt | | Prompt human |
|
||||
+-----------------+ +----------------+
|
||||
|
|
||||
[y]/[a] | [n]
|
||||
allow now | deny
|
||||
/save |
|
||||
v v
|
||||
+-----------------+ +----------------+
|
||||
| Sign / encrypt | | policy_denied |
|
||||
+-----------------+ +----------------+
|
||||
```
|
||||
|
||||
### Identity — *who is asking*
|
||||
|
||||
An identity is a tagged caller record built from the transport. Different transports produce different identity tags; the signer never invents identity, it always reads it from the kernel or framework.
|
||||
|
||||
| Kind | What it carries | Source of truth |
|
||||
|---|---|---|
|
||||
| `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` | 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. `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*
|
||||
|
||||
The signer can derive many keys from one mnemonic. Clients select one in two equivalent ways:
|
||||
|
||||
- `nostr_index = N` — shorthand for the Nostr derivation `m/44'/1237'/N'/0/0` per NIP-06.
|
||||
- `role_path = "<full BIP-32 path>"` — used for non-Nostr key trees (Bitcoin, SSH, etc.). Pre-registered only; see [§9.2](#92-pre-registration-of-role_path).
|
||||
|
||||
The three legacy selector words from the wire protocol — `role`, `nostr_index`, `role_path` — all resolve to a single internal record. From the user's perspective, what matters is "which Nostr identity (by index)" or "which advanced derivation (by path)."
|
||||
|
||||
|
||||
### Approval — *is this OK?*
|
||||
|
||||
An approval is an in-memory entry that says: *"this identity may use this index/path."* Approvals come from exactly two sources:
|
||||
|
||||
1. **The human at the prompt.** When a request arrives that has no matching approval, the signer blocks the request and asks the user. On `[a]` ("allow this caller for this index/path for the rest of the session") the signer adds a session approval. On `[y]` it allows once without saving. On `[n]` it denies.
|
||||
2. **Pre-approvals.** When the OS distribution starts `n_signer` with `--preapprove caller=...,nostr_index=...` flags, those become approvals that exist before any request arrives. They never trigger prompts. See [§7](#7-pre-approvals-the-os-distribution-pathway).
|
||||
|
||||
Approvals are stored in an in-memory table and vanish on process exit. There is no persistent approval database.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. The deny-by-default rule
|
||||
|
||||
`n_signer` denies any request whose `(identity, index)` pair has no approval. Period.
|
||||
|
||||
There is no special case for "same-uid" callers. There is no implicit grant for the user running their own client. Every first request from every caller goes through an approval — either granted by the user at the prompt, or pre-approved by the OS distribution at startup.
|
||||
|
||||
This is the single most important property:
|
||||
|
||||
- It removes the assumption that "same-uid means trusted" — an assumption that was always more convenience than security.
|
||||
- It collapses several mechanisms (policy, role pre-registration, the `[a]` flag) into one (the in-memory approval table).
|
||||
- It makes the audit trail trivial: every signed operation maps to either a pre-approval declaration or a logged human approval.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. The two checks every request must pass
|
||||
|
||||
Every signing request runs through this short pipeline. Failure at any step returns an error response and is logged.
|
||||
|
||||
```text
|
||||
+----------------------------------------------------------------+
|
||||
| 1. CLIENT |
|
||||
| sends length-prefixed JSON-RPC request |
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 2. TRANSPORT |
|
||||
| - reads framed bytes off the socket |
|
||||
| - builds caller_identity_t (uid / qubes vm / tcp peer / ...)|
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 3. RESOLVE INDEX |
|
||||
| - parse selector options from request |
|
||||
| - resolve to a specific (index or path) |
|
||||
| - reject ambiguous selectors |
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 4. APPROVAL CHECK |
|
||||
| - is there an approval for (this identity, this index)? |
|
||||
| yes -> ALLOW |
|
||||
| no -> PROMPT human (or DENY if non-interactive) |
|
||||
| - on [a], append a session approval and continue |
|
||||
| - on [y], continue this request only |
|
||||
| - on [n] / EOF, deny |
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 5. ENFORCEMENT (verb x purpose x curve) |
|
||||
| - is this verb compatible with the key's declared purpose? |
|
||||
| - reject with purpose_mismatch / curve_mismatch otherwise |
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 6. KEY DERIVATION + SIGN |
|
||||
| - derive key from mnemonic at the resolved path |
|
||||
| - perform crypto operation |
|
||||
| - return signed result |
|
||||
+----------------------------------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------------------------------+
|
||||
| 7. RESPONSE FRAMED + SENT |
|
||||
| - activity log line appended with the approval source label |
|
||||
+----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
There are two independent gates: the **approval check** (does this caller have permission to use this key?) and the **enforcement check** (is this operation compatible with this key?). Both must pass.
|
||||
|
||||
### 5.1 Approval check — *does this caller have permission?*
|
||||
|
||||
Implemented as a lookup in the in-memory approval table. First match wins; the catch-all is `*` → deny. No approval entry, no signature.
|
||||
|
||||
The approval check answers *who* and *which key*. It does not check what the request will do with the key — that is the enforcement check.
|
||||
|
||||
### 5.2 Enforcement check — *is this operation compatible with this key?*
|
||||
|
||||
The signer enforces a strict `(verb, purpose, curve)` matrix:
|
||||
|
||||
| Verb | Required purpose | Required curve |
|
||||
|---|---|---|
|
||||
| `sign_event` | `nostr` | `secp256k1` |
|
||||
| `get_public_key` | `nostr` | `secp256k1` |
|
||||
| `nip04_encrypt` / `nip04_decrypt` | `nostr` | `secp256k1` |
|
||||
| `nip44_encrypt` / `nip44_decrypt` | `nostr` | `secp256k1` |
|
||||
| Any other verb | rejected | rejected |
|
||||
|
||||
A pre-approval to use a Bitcoin-purposed key for `sign_event` does **not** override the enforcement matrix. The approval grants access to the key; enforcement still gates the verb. **Fail-closed**: unknown verbs are rejected, never passed through.
|
||||
|
||||
This is the layer that prevents (for example) a `bitcoin/secp256k1` key from being used to sign a Nostr event even if some pre-approval entry mistakenly named it. The key's *purpose* is part of its identity; you cannot reuse it across domains.
|
||||
|
||||
Test coverage: [`tests/test_enforcement.c`](../tests/test_enforcement.c).
|
||||
|
||||
---
|
||||
|
||||
## 6. The interactive approval prompt
|
||||
|
||||
When a request has no matching approval and `n_signer` is running attached to a terminal, the signer blocks the request, prints a prompt, and waits for a local keystroke.
|
||||
|
||||
Target prompt format:
|
||||
|
||||
```
|
||||
Approval required
|
||||
caller: qubes:nostr-relay
|
||||
verb: sign_event
|
||||
index: nostr_index=0
|
||||
purpose: nostr / secp256k1
|
||||
|
||||
[y] allow once
|
||||
[a] allow this caller for index 0 for the rest of the session
|
||||
[n] deny
|
||||
```
|
||||
|
||||
The keys mean:
|
||||
|
||||
- `[y]` — Allow this single request. No approval is saved; the next request from the same caller will prompt again.
|
||||
- `[a]` — Allow, and add a session approval so the same `(caller, index)` pair is silently allowed for the rest of the session.
|
||||
- `[n]` — Deny this request. Returns `policy_denied` to the client.
|
||||
- EOF / non-interactive — Treated as deny. There is no implicit "allow when nobody is at the terminal" mode. OS distributions that need silent operation use [`--preapprove`](#7-pre-approvals-the-os-distribution-pathway).
|
||||
|
||||
### 6.1 What `[a]` does and does not do
|
||||
|
||||
- It approves the **specific caller** (e.g. `qubes:nostr-relay`).
|
||||
- It approves the **specific index/path** that triggered the prompt.
|
||||
- It does **not** approve other callers.
|
||||
- It does **not** approve other indices for the same caller — a different `nostr_index` from the same caller will prompt again.
|
||||
- It does **not** persist past the process lifetime.
|
||||
|
||||
|
||||
### 6.2 New-identity warning
|
||||
|
||||
When a request specifies an unregistered `nostr_index`, the prompt says so plainly:
|
||||
|
||||
```
|
||||
Approval required
|
||||
caller: qubes:nostr-relay
|
||||
verb: get_public_key
|
||||
index: nostr_index=7 (NEW IDENTITY — will be derived if approved)
|
||||
|
||||
[y] allow once [a] allow caller for this new identity [n] deny
|
||||
```
|
||||
|
||||
A new identity is just a new derivation of the same mnemonic. It is not a separate key file or a separate seed; it is `m/44'/1237'/7'/0/0` of the existing mnemonic. The user should still see explicitly that they are *creating* a previously-unused identity.
|
||||
|
||||
### 6.3 Race-condition behavior
|
||||
|
||||
Requests are served one at a time. If a malicious client tries to flood requests during a prompt, only the request being prompted is held; subsequent requests queue or are dropped per the transport's framing. The prompt always names the request being approved. It is not possible for a queued request to "ride along" on an approval intended for an earlier request.
|
||||
|
||||
---
|
||||
|
||||
## 7. Pre-approvals: the OS distribution pathway
|
||||
|
||||
For deployments where multiple system services need keys at boot — `n_OS_tr` and similar — the OS distribution starts `n_signer` with one `--preapprove` flag per service:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=/usr/bin/n_signer \
|
||||
--preapprove caller=qubes:nostr-relay,role=main \
|
||||
--preapprove caller=qubes:dm-handler,nostr_index=1 \
|
||||
--preapprove caller=qubes:contacts,nostr_index=2 \
|
||||
--preapprove caller=qubes:zaps,nostr_index=3
|
||||
```
|
||||
|
||||
At startup, `n_signer`:
|
||||
|
||||
1. Parses each flag into an approval entry with prompt mode `NEVER`.
|
||||
2. Auto-derives any keys those entries depend on (so services don't see "ready" until keys exist).
|
||||
3. Writes one log line per pre-approval to stderr so the OS unit's journal records what was authorized.
|
||||
4. Begins listening normally. Pre-approved requests are served silently; everything else still hits the prompt or the catch-all deny.
|
||||
|
||||
### 7.1 What pre-approvals can do
|
||||
|
||||
- Allow a specific named caller to use a specific named key for any verb that key's purpose/curve permits.
|
||||
- Auto-create a Nostr identity at a specified `nostr_index` if it does not yet exist.
|
||||
- Be inspected: anyone reading the systemd unit (or `ps` output) can see exactly what was authorized.
|
||||
|
||||
### 7.2 What pre-approvals cannot do
|
||||
|
||||
- **Cannot deny.** Pre-approvals only widen authorization, never narrow it. The user at the terminal is always the source of denial. (The plan deliberately rejects "preapprove deny" entries; see [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md) §C.3.)
|
||||
- **Cannot wildcard the caller.** Each entry must name exactly one caller. There is no `qubes:*` rule. The OS distribution must know who its services are.
|
||||
- **Cannot bypass enforcement.** A pre-approval to use a Nostr key for a hypothetical Bitcoin verb still fails the enforcement matrix in [§5.2](#52-enforcement-check--is-this-operation-compatible-with-this-key).
|
||||
- **Cannot pre-register `role_path` derivations.** Only `role=<name>` (an existing role) and `nostr_index=<n>` are allowed in pre-approval specs. Free-form `role_path` derivations remain pre-registration-only by code path. See [§9.2](#92-pre-registration-of-role_path).
|
||||
|
||||
### 7.3 Trust implications
|
||||
|
||||
Pre-approvals shift the trust boundary. Whoever wrote the systemd unit now decides what the signer will allow without asking. Concretely:
|
||||
|
||||
- The unit file's integrity matters. If an attacker can rewrite it, they can pre-approve themselves.
|
||||
- The OS launcher's identity-stamping must be trustworthy. If something else can spoof a `qubes:nostr-relay` tag, it inherits that pre-approval.
|
||||
- The user retains final authority at the running terminal. Anything *not* pre-approved still goes through the human-approved prompt.
|
||||
|
||||
In `n_OS_tr` these properties are part of the OS itself: the unit files are owned by root, the qrexec framework stamps caller identity at the hypervisor level, and the user's terminal is the ultimate arbiter. The trust chain is short and visible.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 8. The audit log
|
||||
|
||||
Every served (or denied) request appends a line to the in-memory activity buffer that the running TUI displays. Each line includes the **source label** that explains why the verdict was reached.
|
||||
|
||||
Target line format:
|
||||
|
||||
```
|
||||
[2026-05-04 16:03:11] qubes:nostr-relay sign_event(nostr_index=0) ALLOWED:preapprove
|
||||
[2026-05-04 16:03:14] uid:1000 sign_event(nostr_index=0) ALLOWED:session-grant
|
||||
[2026-05-04 16:03:18] tcp:[::1]:54122 get_public_key(nostr_index=7) ALLOWED:prompt
|
||||
[2026-05-04 16:03:21] uid:1001 sign_event(nostr_index=0) DENIED:no-match
|
||||
```
|
||||
|
||||
Source labels:
|
||||
|
||||
| Label | Meaning |
|
||||
|---|---|
|
||||
| `:preapprove` | Approval came from a `--preapprove` startup flag. |
|
||||
| `:session-grant` | Approval was added earlier this session by a prompt `[a]`. |
|
||||
| `:prompt` | Approval was given just now by the user pressing `[y]` or `[a]` at the prompt. |
|
||||
| `:no-match` | No approval existed and (in non-interactive mode) the request was denied. |
|
||||
|
||||
This makes forensic review fast: every signed operation maps to either a pre-approval (audit the unit file), a session grant (audit the session's earlier prompt activity), or a fresh prompt approval (audit human attention at that moment).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 9. The role abstraction (internal plumbing)
|
||||
|
||||
Internally `n_signer` uses a structure called a **role** to bundle three things together:
|
||||
|
||||
- A derivation path (BIP-32).
|
||||
- A purpose label (`nostr`, `bitcoin`, `ssh`, `age`).
|
||||
- A curve label (`secp256k1`, `ed25519`, `x25519`).
|
||||
|
||||
Roles are the unit of *enforcement* (the matrix in [§5.2](#52-enforcement-check--is-this-operation-compatible-with-this-key) is keyed on role purpose and curve) and the unit of *audit* (every log line names the role used).
|
||||
|
||||
### 9.1 Why roles exist as plumbing rather than user-facing concept
|
||||
|
||||
- A future where one mnemonic produces Nostr, Bitcoin, SSH, and age keys requires *something* to track per-key purpose. Roles are that thing. Keeping them in the codebase preserves the option without forcing the user to think about them today.
|
||||
- Two different identities can use the same role (the same key). The role is the audit unit shared between them, so the activity log is consistent regardless of caller.
|
||||
- Enforcement against a role's declared purpose/curve is a defense-in-depth: a Nostr key cannot be coerced into producing a Bitcoin signature even if some approval entry mistakenly named it.
|
||||
|
||||
A user reading prompts and the activity log will see "index 0" and "index 7" most of the time, not "role main" or "role nostr_idx_7." Roles surface as labels only in advanced views and the source code.
|
||||
|
||||
### 9.2 Pre-registration of `role_path`
|
||||
|
||||
The `role_path` selector lets a client specify a BIP-32 path directly, including paths outside the Nostr derivation scheme. These remain **pre-registration-only**:
|
||||
|
||||
- The signer ships with an empty `role_path` table.
|
||||
- Operators who want non-Nostr keys must register them at startup (mechanism TBD).
|
||||
- The interactive prompt does **not** auto-derive `role_path` keys, even with `[a]`. The narrowing in [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md) §5 explicitly applies only to `nostr_index`.
|
||||
|
||||
The reasoning is that `role_path` is free-form. A malicious or buggy client could request paths that overlap with sensitive domains (bitcoin wallet roots, SSH host keys, age recipients). Auto-derivation in those domains would be a footgun. `nostr_index` is locked to a specific scheme with locked purpose and curve, so auto-derivation there is safe to gate on a single human keystroke.
|
||||
|
||||
### 9.3 Role table size limits
|
||||
|
||||
`ROLE_TABLE_MAX_ENTRIES` caps the number of distinct roles per session. The plan raises this to 256. Overflow is reported as a clear `role_table_full` error rather than silent truncation or memory unsafety.
|
||||
|
||||
---
|
||||
|
||||
## 10. Memory safety and zeroization
|
||||
|
||||
### 10.1 Locked memory
|
||||
|
||||
- The mnemonic and derived private keys live in `mlock`'d allocations managed by [`src/secure_mem.c`](../src/secure_mem.c).
|
||||
- These pages are pinned in RAM (no swap to disk) for the process lifetime.
|
||||
- On normal shutdown (`q`, `SIGINT`, `SIGTERM`), buffers are explicitly zeroized before unmap.
|
||||
|
||||
### 10.2 Crash semantics
|
||||
|
||||
If the process dies abnormally, the kernel reclaims its pages. Anything previously `mlock`'d is not flushed to disk because it never could be. There is no swap residue to scrape, no core dump containing keys (if your environment disables core dumps for setuid-equivalent processes — verify locally), and no recovery path.
|
||||
|
||||
This is the meaning of "crash equals total wipe is a security feature" in [`plans/nsigner.md:154`](../plans/nsigner.md:154).
|
||||
|
||||
### 10.3 No persistence by design
|
||||
|
||||
There is no file under `~/.nsigner`, no `/var/lib/nsigner`, no `~/.config/nsigner`, no approval database. The signer is a process whose authority begins when you type the mnemonic and ends when you quit.
|
||||
|
||||
If you want something durable: store the mnemonic on paper or hardware, not in `n_signer`.
|
||||
|
||||
The only file inputs are `--preapprove` flags from the launcher's command line, evaluated once at startup and never re-read. There is no live config reload.
|
||||
|
||||
---
|
||||
|
||||
## 11. Transports and their identity quality
|
||||
|
||||
Different transports give different identity quality. The wire format (4-byte length prefix + JSON-RPC) and the security checks ([§5](#5-the-two-checks-every-request-must-pass)) are identical across transports.
|
||||
|
||||
### 11.1 Abstract namespace Unix sockets (default)
|
||||
|
||||
- Address: `@nsigner_<word1>_<word2>` per [`plans/nsigner.md:96`](../plans/nsigner.md:96).
|
||||
- Identity: kernel-vouched `uid`, `pid`. Cannot be forged by the client.
|
||||
- Risk surface: any local process on the same host. Deny-by-default + per-(caller, index) approvals handle this.
|
||||
|
||||
### 11.2 Stdio / qrexec mode
|
||||
|
||||
- One framed request, one framed response, then exit.
|
||||
- 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 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
|
||||
|
||||
The roadmap ([`plans/nsigner.md:178`](../plans/nsigner.md:178)) describes additional transports (USB serial, FIPS-style relay). The contract is:
|
||||
|
||||
- The transport produces a `caller_identity_t`.
|
||||
- The same approval / enforcement / dispatch stack runs unchanged.
|
||||
- New transports add new identity *kinds*, not new bypass paths.
|
||||
|
||||
---
|
||||
|
||||
## 12. Operational guidance
|
||||
|
||||
### 12.1 Running interactively (typical desktop use)
|
||||
|
||||
- Start `n_signer` only on a terminal you control. No `--preapprove` flags.
|
||||
- Type or generate the mnemonic. Generated mnemonics are shown once with no confirmation step ([`plans/nsigner.md:84`](../plans/nsigner.md:84)) — write them down before pressing a key.
|
||||
- Stay near the terminal for the first couple of minutes after launch. New clients will prompt for approval. Press `[a]` to grant for the session.
|
||||
- Once your usual clients are approved, prompts stop appearing.
|
||||
|
||||
### 12.2 Running as a system signer (e.g. `n_OS_tr`)
|
||||
|
||||
- Configure pre-approvals in the systemd (or equivalent) unit file. One `--preapprove` flag per service.
|
||||
- Verify each pre-approval's caller identity exactly matches what the launcher actually produces. Mismatches cause silent deny (`:no-match` in the log).
|
||||
- Treat the unit file as security-sensitive. Restrict write access to root or its equivalent.
|
||||
- Keep a terminal attached or piped to where prompts can still be seen. Anything not pre-approved still prompts.
|
||||
|
||||
### 12.3 Stopping
|
||||
|
||||
- Press `q` for clean shutdown.
|
||||
- `Ctrl+C` and `SIGTERM` are equivalent — they zeroize and exit.
|
||||
- Closing the terminal is also equivalent. There is no "detach" mode by design.
|
||||
|
||||
### 12.4 Auditing
|
||||
|
||||
- The activity buffer in the running TUI is the session log. It does not survive the process.
|
||||
- For persistent audit, run `n_signer` under a logging supervisor that captures stderr/stdout, knowing the activity buffer reflects post-approval decisions only.
|
||||
|
||||
### 12.5 Rotating
|
||||
|
||||
- "Rotate" in `n_signer` means: quit, change mnemonic source, restart. There is no in-place rotation because there is no persistent state to migrate.
|
||||
|
||||
---
|
||||
|
||||
## 13. Relationship to the current codebase
|
||||
|
||||
All stages from [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md) are implemented in the current codebase. This document therefore describes shipped behavior, not a future target state.
|
||||
|
||||
---
|
||||
|
||||
## 14. Things that look like bugs but are security features
|
||||
|
||||
| Symptom | Reason |
|
||||
|---|---|
|
||||
| First request from a new client prompts even though it's the same uid | Deny-by-default. Same-uid trust is gone. Press `[a]` once and you won't see prompts from this client again this session. |
|
||||
| Killing the process loses all approvals and key derivations | Crash-equals-wipe. Restart with the same mnemonic to rebuild deterministically; re-approve clients. |
|
||||
| `purpose_mismatch` when calling `sign_event` against a non-Nostr key | Roles are scoped by purpose. Approvals do not bypass enforcement. |
|
||||
| `unknown_role` for an unregistered `role_path` | `role_path` keys are pre-registration-only by design. Use `nostr_index` for ad-hoc Nostr identities. |
|
||||
| Pre-approval entry doesn't match incoming requests | Caller identity must match exactly — no wildcards. Verify the launcher tags requests the way the unit file expects. |
|
||||
| `role=unknown` in the activity log (current code) | Selector did not resolve. Future versions will prompt to create the identity; current code rejects with `unknown_role` after policy passes. |
|
||||
|
||||
---
|
||||
|
||||
## 15. Quick reference — the security guarantee in one paragraph
|
||||
|
||||
`n_signer` will never sign with a key whose use has not been explicitly approved — either by a human keystroke at the running terminal or by a `--preapprove` flag declared at startup. It will never use a key outside its declared `(purpose, curve)` scope. It will never expose a private key to a client. It will never write key material to disk. Every request is checked twice: once for approval, once for enforcement. Default policy is deny. Approvals vanish on process exit.
|
||||
|
||||
If any of these statements becomes false in code, that is a security bug worth filing immediately.
|
||||
|
||||
---
|
||||
|
||||
## 16. References
|
||||
|
||||
- [`README.md`](../README.md) — authoritative behavior spec.
|
||||
- [`plans/nsigner.md`](../plans/nsigner.md) — root design plan and decisions log.
|
||||
- [`plans/deny_by_default_approvals.md`](../plans/deny_by_default_approvals.md) — the approval model plan this document tracks.
|
||||
- [`documents/CLIENT_IMPLEMENTATION.md`](CLIENT_IMPLEMENTATION.md) — wire contract for clients.
|
||||
- [`documents/QUBES_OS.md`](QUBES_OS.md) — Qubes RPC integration.
|
||||
- [`documents/FIPS_DEPLOYMENT.md`](FIPS_DEPLOYMENT.md) — FIPS-mode deployment notes.
|
||||
- [`plans/seed_phrase_uses.md`](../plans/seed_phrase_uses.md) — what one mnemonic can become.
|
||||
- [`src/policy.c`](../src/policy.c), [`src/server.c`](../src/server.c), [`src/dispatcher.c`](../src/dispatcher.c), [`src/role_table.c`](../src/role_table.c), [`src/selector.c`](../src/selector.c), [`src/enforcement.c`](../src/enforcement.c) — the security-related code.
|
||||
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())
|
||||
527
examples/feather_webusb_demo.html
Normal file
527
examples/feather_webusb_demo.html
Normal file
@@ -0,0 +1,527 @@
|
||||
<!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>
|
||||
:root {
|
||||
--bg: #0b0f14;
|
||||
--panel: #121821;
|
||||
--panel-2: #182231;
|
||||
--text: #e6edf3;
|
||||
--muted: #9fb0c3;
|
||||
--accent: #58a6ff;
|
||||
--good: #3fb950;
|
||||
--bad: #f85149;
|
||||
--border: #263448;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 980px;
|
||||
margin: 20px auto;
|
||||
padding: 0 14px 24px;
|
||||
}
|
||||
h1 { margin: 0 0 8px; font-size: 1.45rem; }
|
||||
p.note { margin: 0 0 14px; color: var(--muted); }
|
||||
|
||||
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.card {
|
||||
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
.card h2 {
|
||||
font-size: 1rem;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.86rem;
|
||||
color: var(--muted);
|
||||
display: block;
|
||||
margin: 6px 0 4px;
|
||||
}
|
||||
input, textarea, button {
|
||||
font: inherit;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
background: #0c131d;
|
||||
color: var(--text);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
textarea { min-height: 84px; resize: vertical; }
|
||||
input[type="number"] { max-width: 130px; }
|
||||
|
||||
button {
|
||||
background: #1f6feb;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
}
|
||||
button[disabled] {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.secondary { background: #334155; }
|
||||
|
||||
.status {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: #2a3648;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.status.ok { color: var(--good); border-color: #2f5a3a; }
|
||||
.status.err { color: var(--bad); border-color: #6a3131; }
|
||||
|
||||
pre {
|
||||
margin: 8px 0 0;
|
||||
background: #0a1018;
|
||||
border: 1px solid #1b2636;
|
||||
color: #d7e2ee;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
max-height: 220px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>n_signer Feather WebUSB Demo</h1>
|
||||
<p class="note">Connect to the board, choose a key index, fetch a pubkey, sign a kind 1 event, and test NIP-04 / NIP-44 encrypt + decrypt RPCs.</p>
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<button id="connectBtn">Connect WebUSB</button>
|
||||
<span id="connStatus" class="status">Disconnected</span>
|
||||
</div>
|
||||
<pre id="log" class="mono"></pre>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<section class="card">
|
||||
<h2>Public Key</h2>
|
||||
<label for="keyIndex">Key index (nostr_index)</label>
|
||||
<input id="keyIndex" type="text" inputmode="numeric" pattern="[0-9]*" value="0" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="pubkeyBtn" disabled>Get Public Key</button>
|
||||
</div>
|
||||
<pre id="pubkeyOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Sign Kind 1 Event</h2>
|
||||
<label for="kind1Content">Content</label>
|
||||
<textarea id="kind1Content">hello from feather webusb demo</textarea>
|
||||
<label for="kind1Tags">Tags JSON (array)</label>
|
||||
<input id="kind1Tags" value="[]" />
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="signKind1Btn" disabled>Create + Sign kind 1</button>
|
||||
</div>
|
||||
<pre id="signOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-04 Encrypt</h2>
|
||||
<label for="nip04Peer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip04Peer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip04Msg">Plaintext</label>
|
||||
<textarea id="nip04Msg">hello via nip04</textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip04EncBtn" disabled>Encrypt (nip04_encrypt)</button>
|
||||
</div>
|
||||
<pre id="nip04Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-04 Decrypt</h2>
|
||||
<label for="nip04DecPeer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip04DecPeer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip04Cipher">Ciphertext</label>
|
||||
<textarea id="nip04Cipher" placeholder="ciphertext?iv=..."></textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip04DecBtn" disabled>Decrypt (nip04_decrypt)</button>
|
||||
</div>
|
||||
<pre id="nip04DecOut" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-44 Encrypt</h2>
|
||||
<label for="nip44Peer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip44Peer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip44Msg">Plaintext</label>
|
||||
<textarea id="nip44Msg">hello via nip44</textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip44EncBtn" disabled>Encrypt (nip44_encrypt)</button>
|
||||
</div>
|
||||
<pre id="nip44Out" class="mono"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>NIP-44 Decrypt</h2>
|
||||
<label for="nip44DecPeer">Peer pubkey (hex, 32-byte x-only)</label>
|
||||
<input id="nip44DecPeer" placeholder="e.g. 64 hex chars" />
|
||||
<label for="nip44Cipher">Ciphertext</label>
|
||||
<textarea id="nip44Cipher" placeholder="base64 payload"></textarea>
|
||||
<div class="row" style="margin-top:10px">
|
||||
<button id="nip44DecBtn" disabled>Decrypt (nip44_decrypt)</button>
|
||||
</div>
|
||||
<pre id="nip44DecOut" class="mono"></pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { schnorr } from "https://esm.sh/@noble/curves@1.5.0/secp256k1?bundle";
|
||||
|
||||
const logEl = document.getElementById("log");
|
||||
const connStatusEl = document.getElementById("connStatus");
|
||||
|
||||
const connectBtn = document.getElementById("connectBtn");
|
||||
const pubkeyBtn = document.getElementById("pubkeyBtn");
|
||||
const signKind1Btn = document.getElementById("signKind1Btn");
|
||||
const nip04EncBtn = document.getElementById("nip04EncBtn");
|
||||
const nip04DecBtn = document.getElementById("nip04DecBtn");
|
||||
const nip44EncBtn = document.getElementById("nip44EncBtn");
|
||||
const nip44DecBtn = document.getElementById("nip44DecBtn");
|
||||
|
||||
const keyIndexEl = document.getElementById("keyIndex");
|
||||
const pubkeyOutEl = document.getElementById("pubkeyOut");
|
||||
|
||||
const kind1ContentEl = document.getElementById("kind1Content");
|
||||
const kind1TagsEl = document.getElementById("kind1Tags");
|
||||
const signOutEl = document.getElementById("signOut");
|
||||
|
||||
const nip04PeerEl = document.getElementById("nip04Peer");
|
||||
const nip04MsgEl = document.getElementById("nip04Msg");
|
||||
const nip04OutEl = document.getElementById("nip04Out");
|
||||
const nip04DecPeerEl = document.getElementById("nip04DecPeer");
|
||||
const nip04CipherEl = document.getElementById("nip04Cipher");
|
||||
const nip04DecOutEl = document.getElementById("nip04DecOut");
|
||||
|
||||
const nip44PeerEl = document.getElementById("nip44Peer");
|
||||
const nip44MsgEl = document.getElementById("nip44Msg");
|
||||
const nip44OutEl = document.getElementById("nip44Out");
|
||||
const nip44DecPeerEl = document.getElementById("nip44DecPeer");
|
||||
const nip44CipherEl = document.getElementById("nip44Cipher");
|
||||
const nip44DecOutEl = document.getElementById("nip44DecOut");
|
||||
|
||||
let dev = null;
|
||||
let iface = null;
|
||||
let ownPubkey = "";
|
||||
const EP_OUT = 1;
|
||||
const EP_IN = 1;
|
||||
|
||||
function log(...args) {
|
||||
logEl.textContent += args.join(" ") + "\n";
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function setStatus(text, mode = "") {
|
||||
connStatusEl.textContent = text;
|
||||
connStatusEl.className = `status ${mode}`.trim();
|
||||
}
|
||||
|
||||
function hex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function utf8(s) {
|
||||
return new TextEncoder().encode(s);
|
||||
}
|
||||
|
||||
function be32(n) {
|
||||
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
|
||||
}
|
||||
|
||||
async function sha256Hex(dataBytes) {
|
||||
const h = await crypto.subtle.digest("SHA-256", dataBytes);
|
||||
return hex(new Uint8Array(h));
|
||||
}
|
||||
|
||||
function getIndexOptions() {
|
||||
const raw = Number.parseInt(String(keyIndexEl.value ?? "0"), 10);
|
||||
const index = Number.isFinite(raw) && raw >= 0 ? raw : 0;
|
||||
return { nostr_index: index };
|
||||
}
|
||||
|
||||
function pretty(value) {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAuth(method, params) {
|
||||
// Demo caller key only (not secret in browser demo).
|
||||
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 rpcCall(method, params, id = "web-1") {
|
||||
const auth = await buildAuth(method, params);
|
||||
const req = { jsonrpc: "2.0", id, method, params, auth };
|
||||
log("→", method, JSON.stringify(params));
|
||||
const resp = await sendRpc(req);
|
||||
log("←", method, JSON.stringify(resp));
|
||||
return resp;
|
||||
}
|
||||
|
||||
function requirePeerHex(peer) {
|
||||
const v = String(peer || "").trim().toLowerCase();
|
||||
if (!/^[0-9a-f]{64}$/.test(v)) {
|
||||
throw new Error("Peer pubkey must be exactly 64 hex chars (x-only pubkey)");
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function requireStringResult(resp, what) {
|
||||
if (resp && typeof resp.result === "string") {
|
||||
return resp.result;
|
||||
}
|
||||
throw new Error(`${what} failed: ${pretty(resp)}`);
|
||||
}
|
||||
|
||||
async function fetchOwnPubkeyAndFillPeers() {
|
||||
const params = [getIndexOptions()];
|
||||
const resp = await rpcCall("get_public_key", params, "web-own-pubkey");
|
||||
if (resp && typeof resp.result === "string") {
|
||||
ownPubkey = resp.result.trim().toLowerCase();
|
||||
pubkeyOutEl.textContent = pretty(resp);
|
||||
if (/^[0-9a-f]{64}$/.test(ownPubkey)) {
|
||||
nip04PeerEl.value = ownPubkey;
|
||||
nip04DecPeerEl.value = ownPubkey;
|
||||
nip44PeerEl.value = ownPubkey;
|
||||
nip44DecPeerEl.value = ownPubkey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
signKind1Btn.disabled = false;
|
||||
nip04EncBtn.disabled = false;
|
||||
nip04DecBtn.disabled = false;
|
||||
nip44EncBtn.disabled = false;
|
||||
nip44DecBtn.disabled = false;
|
||||
setStatus(`Connected (iface ${iface})`, "ok");
|
||||
log("Connected. Interface", String(iface));
|
||||
|
||||
try {
|
||||
await fetchOwnPubkeyAndFillPeers();
|
||||
log("Default peer pubkeys set to selected signer pubkey");
|
||||
} catch (e) {
|
||||
log("Auto pubkey fetch failed:", String(e));
|
||||
}
|
||||
}
|
||||
|
||||
connectBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await connect();
|
||||
} catch (e) {
|
||||
setStatus("Connect failed", "err");
|
||||
log("Connect failed:", String(e));
|
||||
}
|
||||
});
|
||||
|
||||
pubkeyBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await fetchOwnPubkeyAndFillPeers();
|
||||
} catch (e) {
|
||||
pubkeyOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
signKind1Btn.addEventListener("click", async () => {
|
||||
try {
|
||||
let tags = [];
|
||||
try {
|
||||
tags = JSON.parse(kind1TagsEl.value || "[]");
|
||||
if (!Array.isArray(tags)) throw new Error("tags must be an array");
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid tags JSON: ${String(e)}`);
|
||||
}
|
||||
|
||||
const unsignedEvent = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: String(kind1ContentEl.value || ""),
|
||||
};
|
||||
|
||||
const params = [unsignedEvent, getIndexOptions()];
|
||||
const resp = await rpcCall("sign_event", params, "web-sign-kind1");
|
||||
signOutEl.textContent = pretty(resp?.result ?? resp);
|
||||
} catch (e) {
|
||||
signOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip04EncBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip04PeerEl.value);
|
||||
const msg = String(nip04MsgEl.value || "");
|
||||
const params = [peer, msg, getIndexOptions()];
|
||||
const resp = await rpcCall("nip04_encrypt", params, "web-nip04-enc");
|
||||
nip04OutEl.textContent = pretty(resp?.result ?? resp);
|
||||
if (resp && typeof resp.result === "string") {
|
||||
nip04DecPeerEl.value = peer;
|
||||
nip04CipherEl.value = resp.result;
|
||||
}
|
||||
} catch (e) {
|
||||
nip04OutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip04DecBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip04DecPeerEl.value);
|
||||
const ciphertext = String(nip04CipherEl.value || "");
|
||||
const params = [peer, ciphertext, getIndexOptions()];
|
||||
const resp = await rpcCall("nip04_decrypt", params, "web-nip04-dec");
|
||||
nip04DecOutEl.textContent = requireStringResult(resp, "NIP-04 decrypt");
|
||||
} catch (e) {
|
||||
nip04DecOutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip44EncBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip44PeerEl.value);
|
||||
const msg = String(nip44MsgEl.value || "");
|
||||
const params = [peer, msg, getIndexOptions()];
|
||||
const resp = await rpcCall("nip44_encrypt", params, "web-nip44-enc");
|
||||
nip44OutEl.textContent = pretty(resp?.result ?? resp);
|
||||
if (resp && typeof resp.result === "string") {
|
||||
nip44DecPeerEl.value = peer;
|
||||
nip44CipherEl.value = resp.result;
|
||||
}
|
||||
} catch (e) {
|
||||
nip44OutEl.textContent = String(e);
|
||||
}
|
||||
});
|
||||
|
||||
nip44DecBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
const peer = requirePeerHex(nip44DecPeerEl.value);
|
||||
const ciphertext = String(nip44CipherEl.value || "");
|
||||
const params = [peer, ciphertext, getIndexOptions()];
|
||||
const resp = await rpcCall("nip44_decrypt", params, "web-nip44-dec");
|
||||
nip44DecOutEl.textContent = requireStringResult(resp, "NIP-44 decrypt");
|
||||
} catch (e) {
|
||||
nip44DecOutEl.textContent = 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.
|
||||
|
||||
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
|
||||
BIN
firmware/feather_s3_tft/esp32-s3-reverse-tft-feather.pdf
Normal file
BIN
firmware/feather_s3_tft/esp32-s3-reverse-tft-feather.pdf
Normal file
Binary file not shown.
32
firmware/feather_s3_tft/main/CMakeLists.txt
Normal file
32
firmware/feather_s3_tft/main/CMakeLists.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"main.c"
|
||||
"display.c"
|
||||
"mnemonic.c"
|
||||
"key_derivation.c"
|
||||
"bech32.c"
|
||||
"usb_transport.c"
|
||||
"buttons.c"
|
||||
"ui.c"
|
||||
"secure_mem.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/nip004.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/nip044.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/nostr_common.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/utils.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/crypto/nostr_aes.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/crypto/nostr_chacha20.c"
|
||||
"../../../resources/nostr_core_lib/nostr_core/crypto/nostr_secp256k1.c"
|
||||
"../../../resources/nostr_core_lib/platform/esp32/nostr_platform_esp32.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
"../../../resources/nostr_core_lib/nostr_core"
|
||||
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);
|
||||
155
firmware/feather_s3_tft/main/buttons.c
Normal file
155
firmware/feather_s3_tft/main/buttons.c
Normal file
@@ -0,0 +1,155 @@
|
||||
#include "buttons.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#define BTN_D0_GPIO 0 /* D0/BOOT, active LOW */
|
||||
#define BTN_D1_GPIO 1 /* D1, active HIGH */
|
||||
#define BTN_D2_GPIO 2 /* D2, active HIGH */
|
||||
|
||||
#define BTN_DEBOUNCE_MS 30
|
||||
#define BTN_LONG_MS 1000
|
||||
#define BTN_POLL_MS 10
|
||||
|
||||
static bool s_buttons_inited = false;
|
||||
|
||||
static bool button_pressed(btn_id_t id)
|
||||
{
|
||||
if (id == BTN_D0) {
|
||||
return gpio_get_level(BTN_D0_GPIO) == 0;
|
||||
}
|
||||
if (id == BTN_D1) {
|
||||
return gpio_get_level(BTN_D1_GPIO) == 1;
|
||||
}
|
||||
if (id == BTN_D2) {
|
||||
return gpio_get_level(BTN_D2_GPIO) == 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool any_pressed(void)
|
||||
{
|
||||
return button_pressed(BTN_D0) || button_pressed(BTN_D1) || button_pressed(BTN_D2);
|
||||
}
|
||||
|
||||
void buttons_init(void)
|
||||
{
|
||||
if (s_buttons_inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
gpio_config_t d0_cfg = {
|
||||
.pin_bit_mask = 1ULL << BTN_D0_GPIO,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&d0_cfg);
|
||||
|
||||
gpio_config_t d1d2_cfg = {
|
||||
.pin_bit_mask = (1ULL << BTN_D1_GPIO) | (1ULL << BTN_D2_GPIO),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_ENABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&d1d2_cfg);
|
||||
|
||||
s_buttons_inited = true;
|
||||
}
|
||||
|
||||
btn_event_t buttons_poll(void)
|
||||
{
|
||||
btn_event_t evt = {.id = BTN_NONE, .kind = BTN_EVT_NONE};
|
||||
|
||||
if (!s_buttons_inited) {
|
||||
buttons_init();
|
||||
}
|
||||
|
||||
if (button_pressed(BTN_D0)) {
|
||||
evt.id = BTN_D0;
|
||||
evt.kind = BTN_EVT_PRESS;
|
||||
return evt;
|
||||
}
|
||||
if (button_pressed(BTN_D1)) {
|
||||
evt.id = BTN_D1;
|
||||
evt.kind = BTN_EVT_PRESS;
|
||||
return evt;
|
||||
}
|
||||
if (button_pressed(BTN_D2)) {
|
||||
evt.id = BTN_D2;
|
||||
evt.kind = BTN_EVT_PRESS;
|
||||
return evt;
|
||||
}
|
||||
|
||||
return evt;
|
||||
}
|
||||
|
||||
btn_event_t buttons_wait(uint32_t timeout_ms)
|
||||
{
|
||||
btn_event_t evt = {.id = BTN_NONE, .kind = BTN_EVT_NONE};
|
||||
uint32_t elapsed = 0;
|
||||
const bool wait_forever = (timeout_ms == 0xFFFFFFFFu);
|
||||
|
||||
if (!s_buttons_inited) {
|
||||
buttons_init();
|
||||
}
|
||||
|
||||
while (any_pressed()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(BTN_POLL_MS));
|
||||
if (!wait_forever) {
|
||||
elapsed += BTN_POLL_MS;
|
||||
if (elapsed >= timeout_ms) {
|
||||
return evt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (wait_forever || elapsed < timeout_ms) {
|
||||
btn_id_t id = BTN_NONE;
|
||||
|
||||
if (button_pressed(BTN_D0)) {
|
||||
id = BTN_D0;
|
||||
} else if (button_pressed(BTN_D1)) {
|
||||
id = BTN_D1;
|
||||
} else if (button_pressed(BTN_D2)) {
|
||||
id = BTN_D2;
|
||||
}
|
||||
|
||||
if (id != BTN_NONE) {
|
||||
uint32_t held_ms = 0;
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(BTN_DEBOUNCE_MS));
|
||||
if (!button_pressed(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while (button_pressed(id)) {
|
||||
vTaskDelay(pdMS_TO_TICKS(BTN_POLL_MS));
|
||||
held_ms += BTN_POLL_MS;
|
||||
if (!wait_forever) {
|
||||
elapsed += BTN_POLL_MS;
|
||||
if (elapsed >= timeout_ms) {
|
||||
return evt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(BTN_DEBOUNCE_MS));
|
||||
evt.id = id;
|
||||
evt.kind = (held_ms >= BTN_LONG_MS) ? BTN_EVT_LONG_PRESS : BTN_EVT_PRESS;
|
||||
return evt;
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(BTN_POLL_MS));
|
||||
if (!wait_forever) {
|
||||
elapsed += BTN_POLL_MS;
|
||||
}
|
||||
}
|
||||
|
||||
return evt;
|
||||
}
|
||||
33
firmware/feather_s3_tft/main/buttons.h
Normal file
33
firmware/feather_s3_tft/main/buttons.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
BTN_NONE = 0,
|
||||
BTN_D0,
|
||||
BTN_D1,
|
||||
BTN_D2,
|
||||
} btn_id_t;
|
||||
|
||||
typedef enum {
|
||||
BTN_EVT_NONE = 0,
|
||||
BTN_EVT_PRESS,
|
||||
BTN_EVT_LONG_PRESS,
|
||||
} btn_event_kind_t;
|
||||
|
||||
typedef struct {
|
||||
btn_id_t id;
|
||||
btn_event_kind_t kind;
|
||||
} btn_event_t;
|
||||
|
||||
void buttons_init(void);
|
||||
btn_event_t buttons_wait(uint32_t timeout_ms);
|
||||
btn_event_t buttons_poll(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
386
firmware/feather_s3_tft/main/display.c
Normal file
386
firmware/feather_s3_tft/main/display.c
Normal file
@@ -0,0 +1,386 @@
|
||||
#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)
|
||||
#define CHAR_SCALE_SMALL 1
|
||||
#define CHAR_W_SMALL (6 * CHAR_SCALE_SMALL)
|
||||
#define CHAR_H_SMALL (8 * CHAR_SCALE_SMALL)
|
||||
|
||||
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_scaled(int x, int y, char c, int scale, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
if (c < 0x20 || c > 0x7F) {
|
||||
c = '?';
|
||||
}
|
||||
|
||||
if (scale <= 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
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 * scale), y + (row * scale), scale, scale, on ? fg : bg),
|
||||
TAG,
|
||||
"pixel draw failed");
|
||||
}
|
||||
}
|
||||
|
||||
/* spacing column + bottom row */
|
||||
ESP_RETURN_ON_ERROR(display_fill_rect(x + (5 * scale), y, scale, 7 * scale, bg), TAG, "spacing failed");
|
||||
ESP_RETURN_ON_ERROR(display_fill_rect(x, y + (7 * scale), 6 * scale, scale, bg), TAG, "baseline failed");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t draw_text_scaled(int x, int y, const char *text, int scale, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
if (!s_initialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (!text || scale <= 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const int char_w = 6 * scale;
|
||||
const int char_h = 8 * scale;
|
||||
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_scaled(cursor_x, cursor_y, c, scale, fg, bg), TAG, "draw_char failed");
|
||||
cursor_x += char_w;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
return draw_text_scaled(x, y, text, CHAR_SCALE, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_small(int x, int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
return draw_text_scaled(x, y, text, CHAR_SCALE_SMALL, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_centered(int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
int x;
|
||||
|
||||
if (text == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
x = (DISPLAY_WIDTH - ((int)strlen(text) * CHAR_W)) / 2;
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
return display_draw_text(x, y, text, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_small_centered(int y, const char *text, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
int x;
|
||||
|
||||
if (text == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
x = (DISPLAY_WIDTH - ((int)strlen(text) * CHAR_W_SMALL)) / 2;
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
return display_draw_text_small(x, y, text, fg, bg);
|
||||
}
|
||||
|
||||
esp_err_t display_draw_text_inverse(int x, int y, const char *text, uint16_t bg, uint16_t fg)
|
||||
{
|
||||
int w;
|
||||
int h = CHAR_H;
|
||||
|
||||
if (text == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
w = (int)strlen(text) * CHAR_W;
|
||||
ESP_RETURN_ON_ERROR(display_fill_rect(x, y, w, h, bg), TAG, "inverse bg fill failed");
|
||||
return display_draw_text(x, y, text, fg, bg);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
36
firmware/feather_s3_tft/main/display.h
Normal file
36
firmware/feather_s3_tft/main/display.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#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
|
||||
#define RGB565_DIM_GRAY 0x4208
|
||||
|
||||
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_text_small(int x, int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_text_centered(int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_text_small_centered(int y, const char *text, uint16_t fg, uint16_t bg);
|
||||
esp_err_t display_draw_text_inverse(int x, int y, const char *text, uint16_t bg, uint16_t fg);
|
||||
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"
|
||||
255
firmware/feather_s3_tft/main/key_derivation.c
Normal file
255
firmware/feather_s3_tft/main/key_derivation.c
Normal file
@@ -0,0 +1,255 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
static int derive_nostr_key_internal(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32])
|
||||
{
|
||||
const uint32_t path[5] = {
|
||||
44u | BIP32_HARDENED_FLAG,
|
||||
1237u | BIP32_HARDENED_FLAG,
|
||||
0u | BIP32_HARDENED_FLAG,
|
||||
0u,
|
||||
nostr_index,
|
||||
};
|
||||
|
||||
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 derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32])
|
||||
{
|
||||
return derive_nostr_key_internal(seed, 0u, privkey, pubkey);
|
||||
}
|
||||
|
||||
int derive_nostr_key_index(const uint8_t seed[64], uint32_t nostr_index, uint8_t privkey[32], uint8_t pubkey[32])
|
||||
{
|
||||
return derive_nostr_key_internal(seed, nostr_index, privkey, pubkey);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
7
firmware/feather_s3_tft/main/key_derivation.h
Normal file
7
firmware/feather_s3_tft/main/key_derivation.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32]);
|
||||
int derive_nostr_key_index(const uint8_t seed[64], uint32_t nostr_index, 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]);
|
||||
1293
firmware/feather_s3_tft/main/main.c
Normal file
1293
firmware/feather_s3_tft/main/main.c
Normal file
File diff suppressed because it is too large
Load Diff
324
firmware/feather_s3_tft/main/mnemonic.c
Normal file
324
firmware/feather_s3_tft/main/mnemonic.c
Normal file
@@ -0,0 +1,324 @@
|
||||
#include "mnemonic.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ctype.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 s_letter_index_built = false;
|
||||
static size_t s_letter_start[26] = {0};
|
||||
static size_t s_letter_count[26] = {0};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static int bip39_word_index(const char *word)
|
||||
{
|
||||
if (word == NULL || word[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
if (strcmp(word, BIP39_WORDLIST[i]) == 0) {
|
||||
return (int)i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool next_token(const char **cursor, char *out_word, size_t out_len)
|
||||
{
|
||||
size_t n = 0;
|
||||
const char *p = *cursor;
|
||||
|
||||
while (*p != '\0' && isspace((unsigned char)*p)) {
|
||||
++p;
|
||||
}
|
||||
|
||||
if (*p == '\0') {
|
||||
*cursor = p;
|
||||
return false;
|
||||
}
|
||||
|
||||
while (*p != '\0' && !isspace((unsigned char)*p)) {
|
||||
if (n + 1 >= out_len) {
|
||||
return false;
|
||||
}
|
||||
out_word[n++] = *p++;
|
||||
}
|
||||
|
||||
out_word[n] = '\0';
|
||||
*cursor = p;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void build_letter_index(void)
|
||||
{
|
||||
if (s_letter_index_built) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 26; ++i) {
|
||||
s_letter_start[i] = 0;
|
||||
s_letter_count[i] = 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
char c = BIP39_WORDLIST[i][0];
|
||||
if (c < 'a' || c > 'z') {
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t li = (size_t)(c - 'a');
|
||||
if (s_letter_count[li] == 0) {
|
||||
s_letter_start[li] = i;
|
||||
}
|
||||
s_letter_count[li]++;
|
||||
}
|
||||
|
||||
s_letter_index_built = true;
|
||||
}
|
||||
|
||||
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_validate(const char *mnemonic)
|
||||
{
|
||||
uint16_t word_indices[MNEMONIC_12_WORDS] = {0};
|
||||
uint8_t entropy[ENTROPY_12_BYTES] = {0};
|
||||
uint8_t hash[32] = {0};
|
||||
const char *cursor = mnemonic;
|
||||
char token[16];
|
||||
size_t words = 0;
|
||||
size_t bit_pos = 0;
|
||||
|
||||
if (mnemonic == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (next_token(&cursor, token, sizeof(token))) {
|
||||
int idx;
|
||||
|
||||
if (words >= MNEMONIC_12_WORDS) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
|
||||
idx = bip39_word_index(token);
|
||||
if (idx < 0) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
|
||||
word_indices[words++] = (uint16_t)idx;
|
||||
}
|
||||
|
||||
if (words != MNEMONIC_12_WORDS) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (size_t w = 0; w < MNEMONIC_12_WORDS; ++w) {
|
||||
for (int b = 10; b >= 0; --b) {
|
||||
uint8_t bit = (uint8_t)((word_indices[w] >> b) & 0x01);
|
||||
if (bit_pos < ENTROPY_12_BYTES * 8) {
|
||||
size_t byte_index = bit_pos / 8;
|
||||
size_t bit_in_byte = 7 - (bit_pos % 8);
|
||||
entropy[byte_index] |= (uint8_t)(bit << bit_in_byte);
|
||||
}
|
||||
bit_pos++;
|
||||
}
|
||||
}
|
||||
|
||||
mbedtls_sha256(entropy, sizeof(entropy), hash, 0);
|
||||
|
||||
for (size_t i = 0; i < CHECKSUM_12_BITS; ++i) {
|
||||
size_t checksum_bit_pos = ENTROPY_12_BYTES * 8 + i;
|
||||
size_t word_idx = checksum_bit_pos / 11;
|
||||
size_t bit_in_word = 10 - (checksum_bit_pos % 11);
|
||||
uint8_t expected = (uint8_t)((hash[0] >> (7 - i)) & 0x01);
|
||||
uint8_t actual = (uint8_t)((word_indices[word_idx] >> bit_in_word) & 0x01);
|
||||
if (actual != expected) {
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
memset(entropy, 0, sizeof(entropy));
|
||||
memset(hash, 0, sizeof(hash));
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t mnemonic_words_for_letter(char letter, size_t *out_start)
|
||||
{
|
||||
int idx;
|
||||
|
||||
if (out_start == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (letter >= 'A' && letter <= 'Z') {
|
||||
letter = (char)(letter - 'A' + 'a');
|
||||
}
|
||||
|
||||
if (letter < 'a' || letter > 'z') {
|
||||
*out_start = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
build_letter_index();
|
||||
|
||||
idx = letter - 'a';
|
||||
*out_start = s_letter_start[idx];
|
||||
return s_letter_count[idx];
|
||||
}
|
||||
|
||||
const char *mnemonic_word_at(size_t index)
|
||||
{
|
||||
if (index >= 2048) {
|
||||
return NULL;
|
||||
}
|
||||
return BIP39_WORDLIST[index];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
10
firmware/feather_s3_tft/main/mnemonic.h
Normal file
10
firmware/feather_s3_tft/main/mnemonic.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#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]);
|
||||
int mnemonic_validate(const char *mnemonic);
|
||||
size_t mnemonic_words_for_letter(char letter, size_t *out_start);
|
||||
const char *mnemonic_word_at(size_t index);
|
||||
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"
|
||||
};
|
||||
15
firmware/feather_s3_tft/main/secure_mem.c
Normal file
15
firmware/feather_s3_tft/main/secure_mem.c
Normal file
@@ -0,0 +1,15 @@
|
||||
#include "secure_mem.h"
|
||||
|
||||
void secure_memzero(void *ptr, size_t len)
|
||||
{
|
||||
volatile unsigned char *p = (volatile unsigned char *)ptr;
|
||||
|
||||
if (p == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (len > 0) {
|
||||
*p++ = 0;
|
||||
len--;
|
||||
}
|
||||
}
|
||||
13
firmware/feather_s3_tft/main/secure_mem.h
Normal file
13
firmware/feather_s3_tft/main/secure_mem.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void secure_memzero(void *ptr, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
680
firmware/feather_s3_tft/main/ui.c
Normal file
680
firmware/feather_s3_tft/main/ui.c
Normal file
@@ -0,0 +1,680 @@
|
||||
#include "ui.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "buttons.h"
|
||||
#include "display.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "mnemonic.h"
|
||||
#include "secure_mem.h"
|
||||
|
||||
typedef enum {
|
||||
UI_MAIN_GENERATE = 0,
|
||||
UI_MAIN_ENTER,
|
||||
UI_MAIN_VIEW,
|
||||
} ui_main_menu_item_t;
|
||||
|
||||
#define UI_CHAR_W_SMALL 6
|
||||
#define UI_CHAR_W_LARGE 12
|
||||
|
||||
typedef struct {
|
||||
size_t count;
|
||||
size_t first_idx[3];
|
||||
int exact_idx;
|
||||
} prefix_matches_t;
|
||||
|
||||
typedef struct {
|
||||
char phrase[256];
|
||||
bool has_phrase;
|
||||
|
||||
ui_main_menu_item_t main_sel;
|
||||
|
||||
int letter_idx; /* 0..25 */
|
||||
char prefix[12];
|
||||
size_t prefix_len;
|
||||
|
||||
char entered_words[12][12];
|
||||
size_t entered_count;
|
||||
} ui_state_t;
|
||||
|
||||
static btn_event_t wait_any_button(void)
|
||||
{
|
||||
btn_event_t evt;
|
||||
do {
|
||||
evt = buttons_wait(0xFFFFFFFFu);
|
||||
} while (evt.kind == BTN_EVT_NONE);
|
||||
return evt;
|
||||
}
|
||||
|
||||
static void draw_splash(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(60, "N_SIGNER", RGB565_RED, RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void draw_main_menu_row(bool has_phrase, int row_idx, bool selected)
|
||||
{
|
||||
static const char *items[3] = {"Generate", "Enter", "View"};
|
||||
static const int ys[3] = {28, 52, 76};
|
||||
bool disabled = (row_idx == UI_MAIN_VIEW && !has_phrase);
|
||||
char line[24];
|
||||
|
||||
snprintf(line, sizeof(line), "%s%s", selected ? "> " : " ", items[row_idx]);
|
||||
ESP_ERROR_CHECK(display_fill_rect(24, ys[row_idx], 200, 16, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(
|
||||
24,
|
||||
ys[row_idx],
|
||||
line,
|
||||
disabled ? RGB565_DIM_GRAY : (selected ? RGB565_RED : RGB565_WHITE),
|
||||
RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void draw_main_menu_full(const ui_state_t *st)
|
||||
{
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(4, "MAIN MENU", RGB565_WHITE, RGB565_BLACK));
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
draw_main_menu_row(st->has_phrase, i, ((int)st->main_sel == i));
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(display_draw_text_small(12, 120, "D2 up D1 dn D0 ok", RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void draw_main_menu_selection_change(bool has_phrase, int prev_sel, int new_sel)
|
||||
{
|
||||
draw_main_menu_row(has_phrase, prev_sel, false);
|
||||
draw_main_menu_row(has_phrase, new_sel, true);
|
||||
}
|
||||
|
||||
static void split_phrase_words(const char *phrase, char words[12][12])
|
||||
{
|
||||
char buf[256];
|
||||
char *tok;
|
||||
size_t i = 0;
|
||||
|
||||
memset(words, 0, 12 * 12);
|
||||
|
||||
if (phrase == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
strncpy(buf, phrase, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
|
||||
tok = strtok(buf, " \t\r\n");
|
||||
while (tok != NULL && i < 12) {
|
||||
strncpy(words[i], tok, 11);
|
||||
words[i][11] = '\0';
|
||||
i++;
|
||||
tok = strtok(NULL, " \t\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_word_grid(const char words[12][12], int highlight_idx)
|
||||
{
|
||||
const int col_w = 80;
|
||||
const int row_h = 24;
|
||||
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
int row = i / 3;
|
||||
int col = i % 3;
|
||||
int x = col * col_w + 2;
|
||||
int y = 14 + row * row_h;
|
||||
char line[20];
|
||||
|
||||
snprintf(line, sizeof(line), "%2d. %.11s", i + 1, words[i]);
|
||||
ESP_ERROR_CHECK(display_draw_text_small(
|
||||
x,
|
||||
y,
|
||||
line,
|
||||
(i == highlight_idx) ? RGB565_RED : RGB565_WHITE,
|
||||
RGB565_BLACK));
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_view_page(const ui_state_t *st)
|
||||
{
|
||||
char words[12][12];
|
||||
|
||||
split_phrase_words(st->phrase, words);
|
||||
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_small_centered(2, "YOUR MNEMONIC", RGB565_WHITE, RGB565_BLACK));
|
||||
draw_word_grid(words, -1);
|
||||
ESP_ERROR_CHECK(display_draw_text_small_centered(120, "any key = back", RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void draw_generate_page(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(56, "GENERATING...", RGB565_WHITE, RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void clear_enter_words(ui_state_t *st)
|
||||
{
|
||||
secure_memzero(st->entered_words, sizeof(st->entered_words));
|
||||
st->entered_count = 0;
|
||||
st->prefix_len = 0;
|
||||
st->prefix[0] = '\0';
|
||||
}
|
||||
|
||||
static void letter_cell_pos(int idx, int *x, int *y)
|
||||
{
|
||||
int row = (idx < 13) ? 0 : 1;
|
||||
int col = (idx < 13) ? idx : (idx - 13);
|
||||
*x = 10 + col * 16;
|
||||
*y = row ? 68 : 44;
|
||||
}
|
||||
|
||||
static void compute_allowed_next_letters(const ui_state_t *st, bool allowed[26])
|
||||
{
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
allowed[i] = false;
|
||||
}
|
||||
|
||||
if (st->prefix_len == 0) {
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
allowed[i] = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
const char *w = mnemonic_word_at(i);
|
||||
size_t wlen;
|
||||
char c;
|
||||
int idx;
|
||||
|
||||
if (w == NULL) {
|
||||
continue;
|
||||
}
|
||||
if (strncmp(w, st->prefix, st->prefix_len) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
wlen = strlen(w);
|
||||
if (wlen <= st->prefix_len) {
|
||||
continue;
|
||||
}
|
||||
|
||||
c = w[st->prefix_len];
|
||||
if (c < 'a' || c > 'z') {
|
||||
continue;
|
||||
}
|
||||
|
||||
idx = (int)(c - 'a');
|
||||
allowed[idx] = true;
|
||||
}
|
||||
}
|
||||
|
||||
static int find_next_allowed_letter(int current, int delta, const bool allowed[26])
|
||||
{
|
||||
int idx = current;
|
||||
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
idx = (idx + delta + 26) % 26;
|
||||
if (allowed[idx]) {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
static int first_allowed_letter(const bool allowed[26])
|
||||
{
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
if (allowed[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void draw_enter_letter_cell(int idx, bool selected, const bool allowed[26])
|
||||
{
|
||||
int x;
|
||||
int y;
|
||||
char c[2] = {(char)('A' + idx), '\0'};
|
||||
uint16_t color = RGB565_WHITE;
|
||||
|
||||
letter_cell_pos(idx, &x, &y);
|
||||
ESP_ERROR_CHECK(display_fill_rect(x, y, 12, 16, RGB565_BLACK));
|
||||
|
||||
if (!allowed[idx]) {
|
||||
color = RGB565_DIM_GRAY;
|
||||
}
|
||||
if (selected && allowed[idx]) {
|
||||
color = RGB565_RED;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(display_draw_text(x, y, c, color, RGB565_BLACK));
|
||||
}
|
||||
|
||||
static void draw_enter_letter_cursor_change(int prev_idx, int new_idx, const bool allowed[26])
|
||||
{
|
||||
draw_enter_letter_cell(prev_idx, false, allowed);
|
||||
draw_enter_letter_cell(new_idx, true, allowed);
|
||||
}
|
||||
|
||||
static void compute_prefix_matches(const ui_state_t *st, prefix_matches_t *m)
|
||||
{
|
||||
m->count = 0;
|
||||
m->first_idx[0] = 0;
|
||||
m->first_idx[1] = 0;
|
||||
m->first_idx[2] = 0;
|
||||
m->exact_idx = -1;
|
||||
|
||||
if (st->prefix_len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
const char *w = mnemonic_word_at(i);
|
||||
if (w == NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strncmp(w, st->prefix, st->prefix_len) == 0) {
|
||||
if (m->count < 3) {
|
||||
m->first_idx[m->count] = i;
|
||||
}
|
||||
m->count++;
|
||||
if (strcmp(w, st->prefix) == 0 && m->exact_idx < 0) {
|
||||
m->exact_idx = (int)i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_enter_compact_static(const ui_state_t *st, const bool allowed[26])
|
||||
{
|
||||
char hdr[24];
|
||||
unsigned current_word = (unsigned)(st->entered_count + 1);
|
||||
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
|
||||
if (current_word > 12) {
|
||||
current_word = 12;
|
||||
}
|
||||
snprintf(hdr, sizeof(hdr), "WORD %u/12", current_word);
|
||||
ESP_ERROR_CHECK(display_draw_text(10, 8, hdr, RGB565_WHITE, RGB565_BLACK));
|
||||
|
||||
for (int i = 0; i < 26; ++i) {
|
||||
draw_enter_letter_cell(i, i == st->letter_idx, allowed);
|
||||
}
|
||||
}
|
||||
|
||||
static int draw_word_with_prefix_large(int x, int y, const char *word, size_t prefix_len)
|
||||
{
|
||||
char pref[12];
|
||||
char rest[12];
|
||||
size_t wlen;
|
||||
size_t plen;
|
||||
|
||||
if (word == NULL) {
|
||||
return x;
|
||||
}
|
||||
|
||||
wlen = strlen(word);
|
||||
plen = (prefix_len > wlen) ? wlen : prefix_len;
|
||||
|
||||
memset(pref, 0, sizeof(pref));
|
||||
memset(rest, 0, sizeof(rest));
|
||||
|
||||
if (plen > 0) {
|
||||
memcpy(pref, word, plen);
|
||||
}
|
||||
if (wlen > plen) {
|
||||
memcpy(rest, word + plen, wlen - plen);
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(display_draw_text(x, y, pref, RGB565_RED, RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text(x + ((int)plen * UI_CHAR_W_LARGE), y, rest, RGB565_WHITE, RGB565_BLACK));
|
||||
|
||||
return x + ((int)wlen * UI_CHAR_W_LARGE);
|
||||
}
|
||||
|
||||
static void draw_match_row_large(const ui_state_t *st)
|
||||
{
|
||||
int x = 4;
|
||||
|
||||
for (size_t i = 0; i < 2048; ++i) {
|
||||
const char *w = mnemonic_word_at(i);
|
||||
size_t wlen;
|
||||
int needed;
|
||||
|
||||
if (w == NULL) {
|
||||
continue;
|
||||
}
|
||||
if (strncmp(w, st->prefix, st->prefix_len) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
wlen = strlen(w);
|
||||
needed = (int)(wlen * UI_CHAR_W_LARGE) + UI_CHAR_W_LARGE;
|
||||
if (x + needed > DISPLAY_WIDTH) {
|
||||
break;
|
||||
}
|
||||
|
||||
x = draw_word_with_prefix_large(x, 88, w, st->prefix_len);
|
||||
ESP_ERROR_CHECK(display_draw_text(x, 88, " ", RGB565_WHITE, RGB565_BLACK));
|
||||
x += UI_CHAR_W_LARGE;
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_enter_compact_dynamic(const ui_state_t *st, const prefix_matches_t *m)
|
||||
{
|
||||
ESP_ERROR_CHECK(display_fill_rect(0, 86, DISPLAY_WIDTH, 49, RGB565_BLACK));
|
||||
|
||||
if (st->prefix_len == 0) {
|
||||
ESP_ERROR_CHECK(display_draw_text_small(4, 118, "D0=letter D1h=back D2h=del", RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m->count == 0) {
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(88, "NO MATCH", RGB565_RED, RGB565_BLACK));
|
||||
} else {
|
||||
draw_match_row_large(st);
|
||||
}
|
||||
|
||||
if (m->count == 1) {
|
||||
ESP_ERROR_CHECK(display_draw_text_small(4, 108, "One word left: D0=YES D2=NO", RGB565_RED, RGB565_BLACK));
|
||||
}
|
||||
|
||||
if (st->entered_count < 12) {
|
||||
ESP_ERROR_CHECK(display_draw_text_small(4, 118, "D0=letter D1h=back D2h=del", RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
} else {
|
||||
ESP_ERROR_CHECK(display_draw_text_small(4, 118, "12/12: D0 hold=save D2 hold=del", RGB565_DIM_GRAY, RGB565_BLACK));
|
||||
}
|
||||
}
|
||||
|
||||
static int build_phrase_from_entered(ui_state_t *st)
|
||||
{
|
||||
size_t cursor = 0;
|
||||
|
||||
st->phrase[0] = '\0';
|
||||
for (size_t i = 0; i < st->entered_count; ++i) {
|
||||
size_t wlen = strlen(st->entered_words[i]);
|
||||
|
||||
if (i > 0) {
|
||||
if (cursor + 1 >= sizeof(st->phrase)) {
|
||||
return -1;
|
||||
}
|
||||
st->phrase[cursor++] = ' ';
|
||||
}
|
||||
|
||||
if (cursor + wlen >= sizeof(st->phrase)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(st->phrase + cursor, st->entered_words[i], wlen);
|
||||
cursor += wlen;
|
||||
st->phrase[cursor] = '\0';
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ui_run_until_working(char out_mnemonic[256])
|
||||
{
|
||||
ui_state_t st;
|
||||
bool main_menu_drawn = false;
|
||||
|
||||
if (out_mnemonic == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&st, 0, sizeof(st));
|
||||
st.main_sel = UI_MAIN_GENERATE;
|
||||
|
||||
draw_splash();
|
||||
(void)wait_any_button();
|
||||
|
||||
while (1) {
|
||||
btn_event_t evt;
|
||||
|
||||
if (!main_menu_drawn) {
|
||||
draw_main_menu_full(&st);
|
||||
main_menu_drawn = true;
|
||||
}
|
||||
|
||||
evt = wait_any_button();
|
||||
|
||||
if (evt.id == BTN_D2) {
|
||||
int prev = (int)st.main_sel;
|
||||
if (st.has_phrase) {
|
||||
st.main_sel = (st.main_sel == UI_MAIN_GENERATE) ? UI_MAIN_VIEW : (ui_main_menu_item_t)((int)st.main_sel - 1);
|
||||
} else {
|
||||
st.main_sel = (st.main_sel == UI_MAIN_GENERATE) ? UI_MAIN_ENTER : UI_MAIN_GENERATE;
|
||||
}
|
||||
if ((int)st.main_sel != prev) {
|
||||
draw_main_menu_selection_change(st.has_phrase, prev, (int)st.main_sel);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D1) {
|
||||
int prev = (int)st.main_sel;
|
||||
if (st.has_phrase) {
|
||||
st.main_sel = (st.main_sel == UI_MAIN_VIEW) ? UI_MAIN_GENERATE : (ui_main_menu_item_t)((int)st.main_sel + 1);
|
||||
} else {
|
||||
st.main_sel = (st.main_sel == UI_MAIN_ENTER) ? UI_MAIN_GENERATE : UI_MAIN_ENTER;
|
||||
}
|
||||
if ((int)st.main_sel != prev) {
|
||||
draw_main_menu_selection_change(st.has_phrase, prev, (int)st.main_sel);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id != BTN_D0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (st.main_sel == UI_MAIN_VIEW) {
|
||||
if (!st.has_phrase) {
|
||||
continue;
|
||||
}
|
||||
|
||||
draw_view_page(&st);
|
||||
(void)wait_any_button();
|
||||
main_menu_drawn = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (st.main_sel == UI_MAIN_GENERATE) {
|
||||
draw_generate_page();
|
||||
if (generate_mnemonic_12(st.phrase, sizeof(st.phrase)) != 0) {
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(56, "GEN FAILED", RGB565_RED, RGB565_BLACK));
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
main_menu_drawn = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
st.has_phrase = true;
|
||||
draw_view_page(&st);
|
||||
(void)wait_any_button();
|
||||
|
||||
strncpy(out_mnemonic, st.phrase, 255);
|
||||
out_mnemonic[255] = '\0';
|
||||
secure_memzero(&st, sizeof(st));
|
||||
return 0;
|
||||
}
|
||||
|
||||
clear_enter_words(&st);
|
||||
st.letter_idx = 0;
|
||||
|
||||
{
|
||||
prefix_matches_t matches;
|
||||
bool allowed_letters[26];
|
||||
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
|
||||
while (1) {
|
||||
evt = wait_any_button();
|
||||
|
||||
if (evt.id == BTN_D2 && evt.kind == BTN_EVT_PRESS) {
|
||||
if (matches.count == 1 && st.prefix_len > 0) {
|
||||
st.prefix_len = 0;
|
||||
st.prefix[0] = '\0';
|
||||
compute_prefix_matches(&st, &matches);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
} else {
|
||||
int prev_idx = st.letter_idx;
|
||||
st.letter_idx = find_next_allowed_letter(st.letter_idx, -1, allowed_letters);
|
||||
draw_enter_letter_cursor_change(prev_idx, st.letter_idx, allowed_letters);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D1 && evt.kind == BTN_EVT_PRESS) {
|
||||
int prev_idx = st.letter_idx;
|
||||
st.letter_idx = find_next_allowed_letter(st.letter_idx, +1, allowed_letters);
|
||||
draw_enter_letter_cursor_change(prev_idx, st.letter_idx, allowed_letters);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D1 && evt.kind == BTN_EVT_LONG_PRESS) {
|
||||
if (st.prefix_len > 0) {
|
||||
st.prefix_len--;
|
||||
st.prefix[st.prefix_len] = '\0';
|
||||
}
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D2 && evt.kind == BTN_EVT_LONG_PRESS) {
|
||||
if (st.prefix_len > 0) {
|
||||
st.prefix_len = 0;
|
||||
st.prefix[0] = '\0';
|
||||
} else if (st.entered_count > 0) {
|
||||
secure_memzero(st.entered_words[st.entered_count - 1], sizeof(st.entered_words[0]));
|
||||
st.entered_count--;
|
||||
}
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D0 && evt.kind == BTN_EVT_PRESS) {
|
||||
if (st.entered_count >= 12) {
|
||||
if (build_phrase_from_entered(&st) == 0 && mnemonic_validate(st.phrase) == 0) {
|
||||
draw_view_page(&st);
|
||||
(void)wait_any_button();
|
||||
strncpy(out_mnemonic, st.phrase, 255);
|
||||
out_mnemonic[255] = '\0';
|
||||
secure_memzero(&st, sizeof(st));
|
||||
return 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matches.count == 1 && st.prefix_len > 0) {
|
||||
const char *w = mnemonic_word_at(matches.first_idx[0]);
|
||||
if (w != NULL) {
|
||||
strncpy(st.entered_words[st.entered_count], w, 11);
|
||||
st.entered_words[st.entered_count][11] = '\0';
|
||||
st.entered_count++;
|
||||
}
|
||||
st.prefix_len = 0;
|
||||
st.prefix[0] = '\0';
|
||||
|
||||
if (st.entered_count == 12) {
|
||||
if (build_phrase_from_entered(&st) != 0 || mnemonic_validate(st.phrase) != 0) {
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(56, "BAD CHECKSUM", RGB565_RED, RGB565_BLACK));
|
||||
vTaskDelay(pdMS_TO_TICKS(1200));
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
draw_view_page(&st);
|
||||
(void)wait_any_button();
|
||||
strncpy(out_mnemonic, st.phrase, 255);
|
||||
out_mnemonic[255] = '\0';
|
||||
secure_memzero(&st, sizeof(st));
|
||||
return 0;
|
||||
}
|
||||
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (st.prefix_len + 1 < sizeof(st.prefix)) {
|
||||
st.prefix[st.prefix_len++] = (char)('a' + st.letter_idx);
|
||||
st.prefix[st.prefix_len] = '\0';
|
||||
|
||||
compute_prefix_matches(&st, &matches);
|
||||
if (matches.count == 0) {
|
||||
st.prefix_len--;
|
||||
st.prefix[st.prefix_len] = '\0';
|
||||
compute_prefix_matches(&st, &matches);
|
||||
}
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (evt.id == BTN_D0 && evt.kind == BTN_EVT_LONG_PRESS) {
|
||||
compute_prefix_matches(&st, &matches);
|
||||
|
||||
if (st.entered_count == 12 && st.prefix_len == 0) {
|
||||
if (build_phrase_from_entered(&st) != 0 || mnemonic_validate(st.phrase) != 0) {
|
||||
ESP_ERROR_CHECK(display_clear(RGB565_BLACK));
|
||||
ESP_ERROR_CHECK(display_draw_text_centered(56, "BAD CHECKSUM", RGB565_RED, RGB565_BLACK));
|
||||
vTaskDelay(pdMS_TO_TICKS(1200));
|
||||
compute_prefix_matches(&st, &matches);
|
||||
compute_allowed_next_letters(&st, allowed_letters);
|
||||
st.letter_idx = allowed_letters[st.letter_idx] ? st.letter_idx : first_allowed_letter(allowed_letters);
|
||||
draw_enter_compact_static(&st, allowed_letters);
|
||||
draw_enter_compact_dynamic(&st, &matches);
|
||||
continue;
|
||||
}
|
||||
|
||||
strncpy(out_mnemonic, st.phrase, 255);
|
||||
out_mnemonic[255] = '\0';
|
||||
secure_memzero(&st, sizeof(st));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (st.prefix_len == 0) {
|
||||
clear_enter_words(&st);
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main_menu_drawn = false;
|
||||
}
|
||||
}
|
||||
13
firmware/feather_s3_tft/main/ui.h
Normal file
13
firmware/feather_s3_tft/main/ui.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int ui_run_until_working(char out_mnemonic[256]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
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();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user