Compare commits

..

27 Commits

Author SHA1 Message Date
Laan Tungir
e4fa743654 v0.0.28 - Finalize Feather TinyUSB migration and remove cardputer artifacts 2026-05-09 16:22:50 -04:00
Laan Tungir
44372c0108 v0.0.27 - Refine TUI login flow, fix menu hotkey underlining, and pin connection info header 2026-05-08 11:36:26 -04:00
Laan Tungir
5c214f3614 v0.0.26 - Add mnemonic-stdin and mnemonic-fd startup input modes with tests/docs 2026-05-06 17:38:50 -04:00
Laan Tungir
f4413b7969 v0.0.25 - Add --allow-all flag and short aliases for CLI options 2026-05-06 12:09:08 -04:00
Laan Tungir
cca1254740 v0.0.24 - Fix stale release installs by validating installer version and hardening release artifact build/version checks 2026-05-05 12:21:26 -04:00
Laan Tungir
5bb59c1422 v0.0.22 - Fix integration warning regressions and stabilize client-backed test flow 2026-05-05 12:07:51 -04:00
Laan Tungir
cc5638b6e7 v0.0.21 - Add C reference client library, examples, and integration test migration 2026-05-05 11:44:43 -04:00
Laan Tungir
cc797a16df v0.0.20 - Implement TCP auth envelope verification with signed caller pubkey identity, replay protection, tests, and deferred NIP-46 bunker plan 2026-05-05 11:22:58 -04:00
Laan Tungir
17a1b3f020 v0.0.19 - Publish release with prompt options for verb-only vs all-verbs session approval 2026-05-05 07:39:40 -04:00
Laan Tungir
659aeb934a v0.0.18 - Add prompt options for verb-only session grant vs all-verbs session grant 2026-05-05 07:33:22 -04:00
Laan Tungir
148259040c v0.0.17 - Add n_OS_tr agent migration guide for deny-by-default approval workflow 2026-05-04 19:02:41 -04:00
Laan Tungir
7ffba2b678 v0.0.16 - Implement deny-by-default approval model with on-demand nostr_index derivation and --preapprove CLI flag 2026-05-04 18:44:40 -04:00
Laan Tungir
cfaa8ff637 v0.0.15 - Publish downloadable release assets for selector parsing fix 2026-05-04 08:06:32 -04:00
Laan Tungir
1317de5cbe v0.0.14 - Create downloadable release for selector parsing fix 2026-05-04 08:05:38 -04:00
Laan Tungir
fac0ce6503 v0.0.13 - Fix selector parsing to use last params element for nostr_index options 2026-05-04 08:03:17 -04:00
Laan Tungir
ae95ae6859 v0.0.12 - Fix release flow: always increment version in -r mode; include startup confirmations and latest-version installer behavior 2026-05-04 07:21:30 -04:00
Laan Tungir
3efb0edb61 v0.0.11 - Improve startup UX messaging and installer defaults: show mnemonic acceptance, readiness state, and auto-resolve latest release version 2026-05-04 07:18:27 -04:00
Laan Tungir
c5fdb1a207 v0.0.11 - Improve startup UX: print version, visible mnemonic entry, retry up to 10 attempts, and support q/x quit 2026-05-04 07:02:40 -04:00
Laan Tungir
28f50a750f v0.0.10 - Fix ARM64 static release build via buildx/QEMU and arch-aware nostr_core linking 2026-05-04 05:57:46 -04:00
Laan Tungir
1dd6630311 v0.0.9 - Add timestamped stdout activity logging for TCP mode and show full .fips signer URL in startup helper 2026-05-03 20:37:11 -04:00
Laan Tungir
252d026991 v0.0.8 - Add FIPS npub-aware approval prompt and simplify qube installer to nsigner-only startup flow 2026-05-03 19:50:29 -04:00
Laan Tungir
f9d7b94962 v0.0.7 - Allow TCP non-loopback IPv4/IPv6 binds for FIPS reachability 2026-05-03 11:41:42 -04:00
Laan Tungir
b089bf36e3 v0.0.6 - Tier-1 TCP listener + FIPS deployment documentation 2026-05-02 18:14:20 -04:00
Laan Tungir
3e86e539e0 v0.0.5 - Add CLIENT_IMPLEMENTATION.md agent integration spec 2026-05-02 16:19:05 -04:00
Laan Tungir
ee0c60c343 v0.0.4 - Fix interactive smoke test JSON escaping and socket-name client invocation order 2026-05-02 14:11:51 -04:00
Laan Tungir
cdfd8d8d7e v0.0.3 - Refactor key-store module and remove redundant crypto wrappers 2026-05-02 14:04:35 -04:00
Laan Tungir
21f1258844 v0.0.2 - Add random mnemonic startup flow and multi-instance random socket naming 2026-05-02 13:46:01 -04:00
2264 changed files with 379929 additions and 4592 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
build/
.git/
*.o
/*.a
!resources/
!resources/**

3
.gitignore vendored
View File

@@ -4,3 +4,6 @@ build/
*.tar.gz
.gitea_token
resources/
.test_mnemonic
Trash/

7
.roo/commands/push.md Normal file
View File

@@ -0,0 +1,7 @@
---
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:
./increment_and_push.sh "Fixed the bug with nip05 implementation"

View 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"

View File

@@ -1,77 +1,97 @@
# Alpine-based MUSL static binary builder for nsigner
# Produces portable binaries with zero runtime dependencies
ARG DEBUG_BUILD=false
FROM alpine:3.19 AS builder
ARG DEBUG_BUILD=false
RUN apk add --no-cache \
build-base \
musl-dev \
linux-headers \
openssl-dev \
openssl-libs-static \
curl-dev \
curl-static \
zlib-static \
sqlite-dev \
sqlite-static \
brotli-static \
c-ares-static \
nghttp2-static \
libidn2-static \
libpsl-static \
libunistring-static \
zstd-static \
git \
cmake \
pkgconfig \
autoconf \
automake \
libtool \
openssl-dev \
openssl-libs-static \
zlib-dev \
zlib-static \
curl-dev \
curl-static \
linux-headers \
wget \
pkgconfig \
bash
WORKDIR /build
# Build libsecp256k1 static for nostr_core_lib
# Build libsecp256k1 from source with schnorr support
RUN cd /tmp && \
git clone https://github.com/bitcoin-core/secp256k1.git && \
git clone --depth 1 https://github.com/bitcoin-core/secp256k1.git && \
cd secp256k1 && \
./autogen.sh && \
./configure --enable-static --disable-shared --prefix=/usr CFLAGS="-fPIC" && \
make -j$(nproc) && \
./configure \
--prefix=/usr \
--enable-static \
--disable-shared \
--enable-module-schnorrsig \
--enable-module-extrakeys \
--enable-module-recovery && \
make -j"$(nproc)" && \
make install && \
rm -rf /tmp/secp256k1
# Copy and build nostr_core_lib with signer-focused NIPs
COPY nostr_core_lib /build/nostr_core_lib/
RUN cd nostr_core_lib && \
chmod +x build.sh && \
sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
rm -f *.o *.a 2>/dev/null || true && \
./build.sh --nips=1,6,13,17,19,44,46,59
# Copy project source
COPY src/ /build/src/
# Build nsigner static binary
RUN if [ "$DEBUG_BUILD" = "true" ]; then \
CFLAGS="-g -O2 -DDEBUG -fno-omit-frame-pointer"; \
STRIP_CMD="echo 'Keeping debug symbols'"; \
echo "Building debug binary"; \
else \
CFLAGS="-O2"; \
STRIP_CMD="strip /build/nsigner_static"; \
echo "Building production binary"; \
# Copy and build nostr_core_lib from project resources
COPY resources/nostr_core_lib /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 && \
gcc -static $CFLAGS -Wall -Wextra -std=c99 \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
-Isrc \
src/main.c \
-o /build/nsigner_static && \
eval "$STRIP_CMD"
cd /build/nostr_core_lib && \
chmod +x ./build.sh && \
./build.sh --nips=1,4,6,19,44
RUN echo "=== Binary Information ===" && \
file /build/nsigner_static && \
ls -lh /build/nsigner_static && \
echo "=== Checking for dynamic dependencies ===" && \
(ldd /build/nsigner_static 2>&1 || echo "Binary is static") && \
echo "=== Build complete ==="
# Copy source files
COPY src/ /build/src/
COPY resources/tui_continuous/ /build/resources/tui_continuous/
# Build nsigner as a fully static binary
RUN ARCH="$(uname -m)"; \
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 && \
(ldd /build/nsigner_static 2>&1 || true)
FROM scratch AS output
COPY --from=builder /build/nsigner_static /nsigner_static

103
Makefile
View File

@@ -1,10 +1,12 @@
CC := gcc
CFLAGS := -Wall -Wextra -std=c99 -O2 -Isrc -Isrc/cjson
LDFLAGS :=
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
@@ -18,19 +20,14 @@ SOURCES := \
$(SRC_DIR)/dispatcher.c \
$(SRC_DIR)/policy.c \
$(SRC_DIR)/server.c \
$(SRC_DIR)/cjson/cJSON.c
$(SRC_DIR)/transport_frame.c \
$(SRC_DIR)/key_store.c \
$(SRC_DIR)/socket_name.c \
$(SRC_DIR)/auth_envelope.c \
resources/tui_continuous/tui_continuous.c
HEADERS :=
HEADERS := \
$(SRC_DIR)/main.h \
$(SRC_DIR)/secure_mem.h \
$(SRC_DIR)/mnemonic.h \
$(SRC_DIR)/role_table.h \
$(SRC_DIR)/selector.h \
$(SRC_DIR)/enforcement.h \
$(SRC_DIR)/dispatcher.h \
$(SRC_DIR)/policy.h \
$(SRC_DIR)/server.h \
$(SRC_DIR)/cjson/cJSON.h
TEST_MNEMONIC_TARGET := $(BUILD_DIR)/test_mnemonic
TEST_ROLE_TARGET := $(BUILD_DIR)/test_role_table
@@ -39,14 +36,23 @@ TEST_ENFORCEMENT_TARGET := $(BUILD_DIR)/test_enforcement
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 dev static static-debug static-arm64 test test-integration test-mnemonic test-role test-selector test-enforcement test-dispatcher test-policy 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
dev: $(TARGET_DEV)
lib:
cd resources/nostr_core_lib && ./build.sh --nips=1,4,6,19,44
$(TARGET_DEV): $(SOURCES) $(HEADERS)
dev: lib $(TARGET_DEV)
$(TARGET_DEV): $(SOURCES)
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(SOURCES) -o $(TARGET_DEV) $(LDFLAGS)
@@ -62,7 +68,10 @@ static-arm64:
chmod +x ./build_static.sh
./build_static.sh --arch arm64
test: test-mnemonic test-role test-selector test-enforcement test-dispatcher test-policy
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)
@@ -70,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,33 +97,70 @@ test-dispatcher: $(TEST_DISPATCHER_TARGET)
test-policy: $(TEST_POLICY_TARGET)
./$(TEST_POLICY_TARGET)
$(TEST_MNEMONIC_TARGET): $(TEST_DIR)/test_mnemonic.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/mnemonic.h $(SRC_DIR)/secure_mem.h
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_ROLE_TARGET): $(TEST_DIR)/test_role_table.c $(SRC_DIR)/role_table.c $(SRC_DIR)/role_table.h
$(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)
$(TEST_SELECTOR_TARGET): $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC_DIR)/role_table.c $(SRC_DIR)/selector.h $(SRC_DIR)/role_table.h
$(TEST_SELECTOR_TARGET): $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC_DIR)/role_table.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_selector.c $(SRC_DIR)/selector.c $(SRC_DIR)/role_table.c -o $(TEST_SELECTOR_TARGET) $(LDFLAGS)
$(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/enforcement.h $(SRC_DIR)/role_table.h
$(TEST_ENFORCEMENT_TARGET): $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_enforcement.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c -o $(TEST_ENFORCEMENT_TARGET) $(LDFLAGS)
$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/cjson/cJSON.c $(SRC_DIR)/dispatcher.h $(SRC_DIR)/selector.h $(SRC_DIR)/enforcement.h $(SRC_DIR)/role_table.h $(SRC_DIR)/mnemonic.h $(SRC_DIR)/secure_mem.h $(SRC_DIR)/cjson/cJSON.h
$(TEST_DISPATCHER_TARGET): $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c $(SRC_DIR)/cjson/cJSON.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
$(CC) $(CFLAGS) $(TEST_DIR)/test_dispatcher.c $(SRC_DIR)/dispatcher.c $(SRC_DIR)/key_store.c $(SRC_DIR)/selector.c $(SRC_DIR)/enforcement.c $(SRC_DIR)/role_table.c $(SRC_DIR)/mnemonic.c $(SRC_DIR)/secure_mem.c -o $(TEST_DISPATCHER_TARGET) $(LDFLAGS)
$(TEST_POLICY_TARGET): $(TEST_DIR)/test_policy.c $(SRC_DIR)/policy.c $(SRC_DIR)/policy.h $(SRC_DIR)/role_table.h
$(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)

146
README.md
View File

@@ -70,13 +70,22 @@ This reduces deployment variability and shrinks the runtime trust surface.
When started, `n_signer` immediately enters terminal input mode:
1. Prompt for mnemonic (terminal echo disabled).
2. Parse and validate mnemonic.
3. Build in-memory role/selector state.
4. Initialize transport endpoints.
5. Switch to running status display.
1. Choose mnemonic source: `[E]nter existing mnemonic` (default) or `[G]enerate new mnemonic`.
- On `E`: prompt for mnemonic with terminal echo disabled, then validate.
- On `G`: generate a fresh 12-word BIP-39 mnemonic from `getrandom(2)`, display it numbered with a "WRITE THIS DOWN — IT WILL NOT BE SHOWN AGAIN" warning, then continue. There is no confirmation step.
2. Build in-memory role/selector state from the mnemonic.
3. Pick the abstract socket name (random BIP-39 pair, or `--socket-name` / `--name` / `-n` override).
4. Initialize transport endpoints and bind the socket.
5. Switch to running status display, with the signer name and socket address shown in the banner.
No startup files are read or written.
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)
@@ -106,9 +115,9 @@ Activity (latest first)
Hotkeys
-------
a add role
a toggle auto-approve(prompt) for this session
r refresh
l lock session
l lock/reunlock session
q quit
```
@@ -125,7 +134,7 @@ method: sign_event
selector: role=ops
purpose/curve: nostr/secp256k1
[y] allow once [n] deny [a] always allow for this session
[y] allow once [n] deny [a] always allow this session
```
No response is emitted to caller until the local user decides.
@@ -171,6 +180,13 @@ Request shape is JSON-RPC with NIP-46-style methods and optional trailing select
{ "id": "4", "method": "sign_event", "params": ["<event_json>", { "role_path": "m/84'/0'/0'/0/5", "purpose": "bitcoin", "curve": "secp256k1" }] }
```
Implemented signer verbs in this build:
- `get_public_key`
- `sign_event`
- `nip04_encrypt` / `nip04_decrypt`
- `nip44_encrypt` / `nip44_decrypt`
Selector resolution order:
1. `role`
@@ -207,7 +223,9 @@ This prevents cross-protocol misuse inside one mnemonic-rooted signer process.
### 7.1 Linux desktop: abstract namespace Unix socket
Primary local transport is AF_UNIX abstract namespace (for example `@nsigner`).
Primary local transport is AF_UNIX abstract namespace.
Each running `nsigner` process binds to a unique abstract name of the form `@nsigner_<word1>_<word2>`, where the two words are picked at random from the BIP-39 English wordlist at startup (e.g. `@nsigner_hairy_dog`). This lets multiple signers coexist on one host.
Properties:
@@ -215,12 +233,29 @@ Properties:
- endpoint lifetime bound to process/kernel namespace
- no stale socket files
- caller identity via peer credentials (`SO_PEERCRED`)
- per-launch random name avoids collisions between concurrent instances and leaks no seed-derived identifier
### 7.2 ESP32 MCU: USB-CDC serial
Naming rules:
On MCU targets, the same core modules run with a different transport adapter: USB-CDC serial framing instead of AF_UNIX.
- Default: random pick at startup, displayed in the TUI banner.
- Override: `--socket-name <name>` (alias: `--name <name>` / `-n <name>`) forces a specific name (useful for scripts and tests).
- Collision: if the chosen random name is already bound by another process, `nsigner` retries with a fresh pair up to 8 times before erroring out with a hint to use an explicit name override.
Core logic stays the same; only transport/UI bindings change.
Discovery:
- `nsigner list` enumerates currently bound `nsigner_*` abstract sockets by reading `/proc/net/unix`.
- `nsigner --listen stdio` runs one framed JSON-RPC request/response over stdin/stdout.
- `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: TinyUSB composite (CDC + WebUSB)
On Feather S3 TFT, MCU targets run the same core signer modules behind a TinyUSB composite transport:
- 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)
@@ -244,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
@@ -258,23 +293,62 @@ Qubes deployment runs `n_signer` in a dedicated signer qube as a foreground proc
nsigner
```
Program starts in attached foreground mode and prompts for mnemonic.
Program starts in attached foreground mode and prompts for mnemonic. After mnemonic acceptance, the TUI banner shows the randomly assigned signer name and its abstract socket address.
To force a specific socket name (e.g. for scripted clients):
```bash
nsigner --name my_test_signer
```
Qubes/qrexec service mode (single framed request over stdin/stdout):
```bash
nsigner --listen qrexec
```
Generic stdio transport mode (single framed request over stdin/stdout):
```bash
nsigner --listen stdio
```
TCP transport mode (no TUI; serves requests until terminated):
```bash
nsigner --listen tcp:[::]:8080
```
### 9.2 Send a request (client mode)
From another terminal:
From another terminal, target the signer by its socket name:
```bash
nsigner client '{"id":"1","method":"get_public_key","params":[]}'
nsigner --socket-name nsigner_hairy_dog client '{"id":"1","method":"get_public_key","params":[]}'
```
If only one signer is running you can omit the override and the client will use the default discovery rule.
Example signing request:
```bash
nsigner client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
nsigner -n nsigner_hairy_dog client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
```
### 9.3 Example session
### 9.3 List running signers
```bash
nsigner list
```
Prints the abstract socket names of any currently running `nsigner` instances, e.g.:
```text
@nsigner_hairy_dog
@nsigner_brave_canyon
```
### 9.4 Example session
Terminal A:
@@ -282,14 +356,16 @@ Terminal A:
$ nsigner
[unlock] enter mnemonic:
[ok] session unlocked
[listen] unix-abstract:@nsigner
signer name : hairy dog
socket : @nsigner_hairy_dog
[listen] unix-abstract:@nsigner_hairy_dog
[prompt] caller=uid:1000 method=sign_event role=main -> allow? (y/n)
```
Terminal B:
```text
$ nsigner client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
$ nsigner --socket-name nsigner_hairy_dog client '{"id":"2","method":"sign_event","params":["<event_json>",{"role":"main"}]}'
{"id":"2","result":"<signed_event_json>"}
```
@@ -300,14 +376,15 @@ 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.h`](src/main.h): version macros
- [`src/main.c`](src/main.c): version macros (`NSIGNER_VERSION*`)
Local dev build:
```bash
make dev
./build/nsigner_dev --version
./build/nsigner --version
```
Static build:
@@ -317,9 +394,30 @@ 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
- [`documents/QUBES_OS.md`](documents/QUBES_OS.md): Qubes OS deployment/integration checklist for dedicated signer qubes
- [`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

View File

@@ -1,35 +1,30 @@
#!/bin/bash
# Build fully static MUSL binaries for nsigner using Alpine Docker
# Build fully static MUSL binary for nsigner using Alpine Docker
set -e
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="$SCRIPT_DIR/build"
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
DEBUG_BUILD=false
TARGET_ARCH=""
while [[ $# -gt 0 ]]; do
case "$1" in
--debug)
DEBUG_BUILD=true
shift
;;
--arch)
if [[ -z "$2" ]]; then
if [[ -z "${2:-}" ]]; then
echo "ERROR: --arch requires a value"
echo "Usage: $0 [--debug] [--arch <arm64|armv7|x86_64>]"
echo "Usage: $0 [--arch <x86_64|arm64|armv7>]"
exit 1
fi
case "$2" in
arm64|armv7|x86_64)
x86_64|arm64|armv7)
TARGET_ARCH="$2"
;;
*)
echo "ERROR: Unsupported architecture '$2'"
echo "Supported values: arm64, armv7, x86_64"
echo "Supported values: x86_64, arm64, armv7"
exit 1
;;
esac
@@ -37,48 +32,24 @@ while [[ $# -gt 0 ]]; do
;;
*)
echo "ERROR: Unknown argument '$1'"
echo "Usage: $0 [--debug] [--arch <arm64|armv7|x86_64>]"
echo "Usage: $0 [--arch <x86_64|arm64|armv7>]"
exit 1
;;
esac
done
if [ "$DEBUG_BUILD" = true ]; then
echo "=========================================="
echo "nsigner MUSL Static Binary Builder (DEBUG MODE)"
echo "=========================================="
else
echo "=========================================="
echo "nsigner MUSL Static Binary Builder (PRODUCTION MODE)"
echo "=========================================="
fi
echo "Project directory: $SCRIPT_DIR"
echo "Build directory: $BUILD_DIR"
echo "Debug build: $DEBUG_BUILD"
if [[ -n "$TARGET_ARCH" ]]; then
echo "Requested target architecture: $TARGET_ARCH"
fi
echo ""
mkdir -p "$BUILD_DIR"
if ! command -v docker &> /dev/null; then
if ! command -v docker >/dev/null 2>&1; then
echo "ERROR: Docker is not installed or not in PATH"
exit 1
fi
if ! docker info &> /dev/null; then
echo "ERROR: Docker daemon is not running or user not in docker group"
if ! docker info >/dev/null 2>&1; then
echo "ERROR: Docker daemon is not running or user lacks permissions"
exit 1
fi
DOCKER_CMD="docker"
echo "✓ Docker is available and running"
echo ""
ARCH=$(uname -m)
case "$ARCH" in
HOST_UNAME="$(uname -m)"
case "$HOST_UNAME" in
x86_64)
HOST_ARCH="x86_64"
;;
@@ -89,147 +60,92 @@ case "$ARCH" in
HOST_ARCH="armv7"
;;
*)
HOST_ARCH="unknown"
HOST_ARCH="x86_64"
;;
esac
if [[ -n "$TARGET_ARCH" ]]; then
ARCH="$TARGET_ARCH"
fi
ARCH="${TARGET_ARCH:-$HOST_ARCH}"
case "$ARCH" in
x86_64)
PLATFORM="linux/amd64"
OUTPUT_NAME="nsigner_static_x86_64"
;;
aarch64|arm64)
ARCH="arm64"
arm64)
PLATFORM="linux/arm64"
OUTPUT_NAME="nsigner_static_arm64"
;;
armv7l|armv7)
ARCH="armv7"
armv7)
PLATFORM="linux/arm/v7"
OUTPUT_NAME="nsigner_static_armv7"
;;
*)
echo "WARNING: Unknown architecture: $ARCH"
echo "Defaulting to linux/amd64"
PLATFORM="linux/amd64"
OUTPUT_NAME="nsigner_static_${ARCH}"
echo "ERROR: Unsupported target architecture '$ARCH'"
exit 1
;;
esac
if [[ "$HOST_ARCH" != "unknown" && "$ARCH" != "$HOST_ARCH" ]]; then
echo "NOTE: Cross-building from host '$HOST_ARCH' to target '$ARCH'."
if ! docker buildx version >/dev/null 2>&1; then
echo "ERROR: docker buildx is required for cross-architecture builds but is not available."
mkdir -p "$BUILD_DIR"
IMAGE_TAG="nsigner-static-builder:${ARCH}"
CONTAINER_NAME=""
cleanup() {
if [[ -n "$CONTAINER_NAME" ]]; then
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
echo "=========================================="
echo "nsigner static build"
echo "=========================================="
echo "Project root: $SCRIPT_DIR"
echo "Dockerfile: $DOCKERFILE"
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
echo "✓ docker buildx is available for cross-architecture build"
echo ""
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
if [ "$DEBUG_BUILD" = true ]; then
OUTPUT_NAME="${OUTPUT_NAME}_debug"
fi
echo "Building for platform: $PLATFORM"
echo "Output binary: $OUTPUT_NAME"
echo ""
echo "=========================================="
echo "Step 1: Building Alpine Docker image"
echo "=========================================="
$DOCKER_CMD build \
echo "[1/3] Building builder stage from project root context"
docker buildx build \
--platform "$PLATFORM" \
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
-f "$DOCKERFILE" \
-t nsigner-musl-builder:latest \
--progress=plain \
.
echo ""
echo "✓ Docker image built successfully"
echo ""
echo "=========================================="
echo "Step 2: Extracting static binary"
echo "=========================================="
$DOCKER_CMD build \
--platform "$PLATFORM" \
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
--target builder \
-f "$DOCKERFILE" \
-t nsigner-static-builder-stage:latest \
. > /dev/null 2>&1
CONTAINER_ID=$($DOCKER_CMD create nsigner-static-builder-stage:latest)
$DOCKER_CMD cp "$CONTAINER_ID:/build/nsigner_static" "$BUILD_DIR/$OUTPUT_NAME"
$DOCKER_CMD rm "$CONTAINER_ID" > /dev/null
-t "$IMAGE_TAG" \
--load \
"$SCRIPT_DIR"
echo "[2/3] Extracting static binary"
CONTAINER_NAME="$(docker create "$IMAGE_TAG")"
docker cp "$CONTAINER_NAME:/build/nsigner_static" "$BUILD_DIR/$OUTPUT_NAME"
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
strip "$BUILD_DIR/$OUTPUT_NAME" >/dev/null 2>&1 || true
echo "✓ Binary extracted to: $BUILD_DIR/$OUTPUT_NAME"
echo ""
echo "[3/3] Verifying static binary"
file "$BUILD_DIR/$OUTPUT_NAME"
echo "=========================================="
echo "Step 3: Verifying static binary"
echo "=========================================="
echo ""
if LDD_OUTPUT=$(timeout 5 ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1); then
if echo "$LDD_OUTPUT" | grep -q "not a dynamic executable"; then
echo "✓ Binary is fully static"
TRULY_STATIC=true
elif echo "$LDD_OUTPUT" | grep -q "statically linked"; then
echo "✓ Binary is statically linked"
TRULY_STATIC=true
else
echo "⚠ WARNING: Binary may have dynamic dependencies:"
echo "$LDD_OUTPUT"
TRULY_STATIC=false
fi
LDD_OUTPUT="$(ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 || true)"
echo "$LDD_OUTPUT"
if echo "$LDD_OUTPUT" | grep -Eq "not a dynamic executable|statically linked"; then
echo "Static check: PASS"
else
if file "$BUILD_DIR/$OUTPUT_NAME" | grep -q "statically linked"; then
echo "✓ Binary is statically linked (verified with file command)"
TRULY_STATIC=true
else
echo "⚠ Could not verify static linking (ldd check failed)"
TRULY_STATIC=false
fi
echo "Static check: WARNING (verify manually)"
fi
echo ""
echo "File size: $(ls -lh "$BUILD_DIR/$OUTPUT_NAME" | awk '{print $5}')"
echo ""
echo "Testing binary execution:"
if "$BUILD_DIR/$OUTPUT_NAME" --version 2>&1 | head -5; then
echo "✓ Binary executes successfully"
else
echo "⚠ Binary execution test failed"
fi
echo ""
echo "=========================================="
echo "Build Summary"
echo "=========================================="
echo "Binary: $BUILD_DIR/$OUTPUT_NAME"
echo "Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"
echo "Platform: $PLATFORM"
if [ "$DEBUG_BUILD" = true ]; then
echo "Build Type: DEBUG"
else
echo "Build Type: PRODUCTION"
fi
if [ "$TRULY_STATIC" = true ]; then
echo "Linkage: Fully static binary"
else
echo "Linkage: Static binary (verification inconclusive)"
fi
echo ""
echo "✓ Build complete!"
echo "Build complete: $BUILD_DIR/$OUTPUT_NAME"

47
client/README.md Normal file
View 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
View 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
View 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

View File

@@ -0,0 +1,492 @@
# CLIENT_IMPLEMENTATION.md
## 1. Purpose
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 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:
- Socket names are exposed in `/proc/net/unix` with a leading `@`.
- Typical runtime names: `@nsigner_hairy_dog`, `@nsigner_brave_canyon`.
- Clients pass the socket name **without** `@` to CLI flags (example: `nsigner_hairy_dog`).
### 2.1 Discovery rules
Use one of these patterns:
1. Explicit target (recommended): pass `--socket-name` / `--name` / `-n`.
2. Enumerate with `nsigner list` and select one.
3. Auto-discovery only when exactly one signer is running.
### 2.2 Enumerating running signers
```bash
nsigner list
```
Expected output format (one per line):
```text
@nsigner
@nsigner_hairy_dog
@nsigner_brave_canyon
```
Clients should accept both `@nsigner` and `@nsigner_*` names.
### 2.3 Stdio / qrexec mode
In server mode:
- `nsigner --listen stdio`: reads exactly one framed request from stdin and writes one framed response to stdout.
- `nsigner --listen qrexec`: same behavior, but caller identity may be tagged from `QREXEC_REMOTE_DOMAIN` as `qubes:<vm-name>`.
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
Signer requests/responses use a length-prefixed frame format over the socket:
- Prefix: 4-byte unsigned big-endian length `N`
- Payload: `N` bytes UTF-8 JSON text
- One JSON-RPC object per frame
### 3.1 Framing pseudocode
Write:
1. Serialize JSON to bytes
2. Compute `len(payload)`
3. Send `uint32_be(length)` then payload bytes
Read:
1. Read exactly 4 bytes
2. Parse big-endian payload length
3. Read exactly `length` bytes
4. Parse JSON
Do not assume line-delimited JSON.
---
## 4. JSON-RPC contract
### 4.1 Request shape
```json
{ "id": "1", "method": "get_public_key", "params": [] }
```
Methods are NIP-46 style verbs.
### 4.2 Implemented methods
- `get_public_key`
- `sign_event`
- `nip04_encrypt`
- `nip04_decrypt`
- `nip44_encrypt`
- `nip44_decrypt`
### 4.3 Selector options
The last param may include selector options:
- `role`
- `nostr_index`
- `role_path`
Resolution order:
1. `role`
2. `nostr_index`
3. `role_path`
4. default role `main`
Conflicting selector fields must be rejected as `ambiguous_role_selector`.
### 4.4 Selector example
```json
{
"id": "2",
"method": "sign_event",
"params": [
"<event_json>",
{ "role": "main" }
]
}
```
---
## 5. Error handling contract
Representative error names clients must handle:
- `invalid_request`
- `method_not_found`
- `ambiguous_role_selector`
- `unknown_role`
- `purpose_mismatch`
- `curve_mismatch`
- `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
- `invalid_request`: client bug or malformed payload; fix request and retry.
- `method_not_found`: version mismatch; feature-detect and downgrade behavior.
- `ambiguous_role_selector`: send exactly one selector strategy.
- `unknown_role`: selector did not resolve; verify role inventory/config.
- `purpose_mismatch` / `curve_mismatch`: selected key is incompatible with method; pick compatible selector.
- `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.
---
## 6. Approval semantics
Approval has two layers:
1. Policy/identity checks (caller and method authorization)
2. User prompt (interactive allow/deny)
Important behavior:
- Passing identity checks does **not** bypass prompts.
- Non-interactive/no-TTY contexts can resolve to deny by policy/test configuration.
- Clients must treat `approval_denied` as a normal, expected outcome.
### 6.1 UX recommendation
If a signing call is denied, return control to the user and let them explicitly retry.
---
## 7. Multi-instance, concurrency, and timeouts
### 7.1 Multi-instance safety
- Multiple signers can coexist using different socket names.
- Always pin requests to a selected signer name once chosen.
- Avoid “discover per request” after initial bind in long-running clients.
### 7.2 Connection strategy
- Keep one connection per in-flight request path, or serialize requests if your client runtime is simple.
- Validate response `id` correlation before completing promise/future.
### 7.3 Timeout guidance
Use separate timeouts:
- connect timeout (short)
- write timeout (short)
- read timeout (longer, because human approval may be required)
For prompt-requiring methods, use a read timeout that accounts for user interaction.
---
## 8. Security expectations for clients
- Treat socket access as sensitive local capability.
- Do not log plaintext secrets, private keys, or full decrypted payloads.
- Redact request params for encrypt/decrypt methods in normal logs.
- Validate method-level expectations before sending (selector + purpose compatibility).
- Use least-privilege execution context for any process that can reach the signer socket.
---
## 9. Reference code snippets
## 9.1 C (frame write/read skeleton)
```c
#include <arpa/inet.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
static int write_all(int fd, const void *buf, size_t len) {
const unsigned char *p = (const unsigned char *)buf;
while (len > 0) {
ssize_t n = write(fd, p, len);
if (n <= 0) return -1;
p += (size_t)n;
len -= (size_t)n;
}
return 0;
}
static int read_all(int fd, void *buf, size_t len) {
unsigned char *p = (unsigned char *)buf;
while (len > 0) {
ssize_t n = read(fd, p, len);
if (n <= 0) return -1;
p += (size_t)n;
len -= (size_t)n;
}
return 0;
}
int send_json_frame(int fd, const char *json) {
uint32_t n = (uint32_t)strlen(json);
uint32_t be = htonl(n);
if (write_all(fd, &be, 4) != 0) return -1;
if (write_all(fd, json, n) != 0) return -1;
return 0;
}
```
## 9.2 Python (Unix abstract socket + frame)
```python
import json
import socket
import struct
def send_rpc(socket_name: str, obj: dict) -> dict:
payload = json.dumps(obj, separators=(",", ":")).encode("utf-8")
frame = struct.pack(">I", len(payload)) + payload
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
s.connect("\0" + socket_name) # abstract namespace
s.sendall(frame)
hdr = recv_exact(s, 4)
ln = struct.unpack(">I", hdr)[0]
body = recv_exact(s, ln)
return json.loads(body.decode("utf-8"))
finally:
s.close()
def recv_exact(s: socket.socket, n: int) -> bytes:
out = bytearray()
while len(out) < n:
chunk = s.recv(n - len(out))
if not chunk:
raise ConnectionError("unexpected EOF")
out.extend(chunk)
return bytes(out)
```
## 9.3 TypeScript (Node.js net + frame)
```ts
import net from "node:net";
export async function sendRpc(socketName: string, request: unknown): Promise<any> {
const payload = Buffer.from(JSON.stringify(request), "utf8");
const header = Buffer.alloc(4);
header.writeUInt32BE(payload.length, 0);
return await new Promise((resolve, reject) => {
const socket = net.createConnection({ path: `\u0000${socketName}` });
let chunks: Buffer[] = [];
let needed = 4;
let mode: "header" | "body" = "header";
socket.on("connect", () => {
socket.write(Buffer.concat([header, payload]));
});
socket.on("data", (data) => {
chunks.push(data);
let buf = Buffer.concat(chunks);
while (buf.length >= needed) {
const part = buf.subarray(0, needed);
buf = buf.subarray(needed);
if (mode === "header") {
needed = part.readUInt32BE(0);
mode = "body";
} else {
socket.end();
resolve(JSON.parse(part.toString("utf8")));
return;
}
}
chunks = [buf];
});
socket.on("error", reject);
socket.on("end", () => {
// no-op; resolution occurs when full body is parsed
});
});
}
```
---
## 10. End-to-end transcripts
## 10.1 Happy path: get public key
Request:
```json
{ "id": "1", "method": "get_public_key", "params": [] }
```
Response:
```json
{ "id": "1", "result": "<hex_pubkey>" }
```
## 10.2 Happy path: sign event with explicit role
Request:
```json
{
"id": "2",
"method": "sign_event",
"params": ["<event_json>", { "role": "main" }]
}
```
Response:
```json
{ "id": "2", "result": "<signed_event_json>" }
```
## 10.3 Error path: unknown role
Request:
```json
{
"id": "3",
"method": "sign_event",
"params": ["<event_json>", { "role": "does_not_exist" }]
}
```
Response:
```json
{ "id": "3", "error": { "code": 1002, "message": "unknown_role" } }
```
## 10.4 Error path: ambiguous selector
Request:
```json
{
"id": "4",
"method": "sign_event",
"params": [
"<event_json>",
{ "role": "main", "nostr_index": 0 }
]
}
```
Response:
```json
{ "id": "4", "error": { "message": "ambiguous_role_selector" } }
```
## 10.5 Encrypt/decrypt round trip (NIP-44)
Encrypt request:
```json
{
"id": "5",
"method": "nip44_encrypt",
"params": ["<peer_pubkey>", "hello", { "role": "main" }]
}
```
Encrypt response:
```json
{ "id": "5", "result": "<nip44_ciphertext>" }
```
Decrypt request:
```json
{
"id": "6",
"method": "nip44_decrypt",
"params": ["<peer_pubkey>", "<nip44_ciphertext>", { "role": "main" }]
}
```
Decrypt response:
```json
{ "id": "6", "result": "hello" }
```
---
## 11. Compatibility notes
- If you are writing an autonomous agent client, pin to explicit socket name and explicit role selector.
- Keep method support feature-detected (`method_not_found` fallback).
- Treat approval as asynchronous human gating even for local calls.
- Track and surface signer name, request id, and method for auditability.

View File

@@ -0,0 +1,177 @@
# FIPS_DEPLOYMENT.md
## 1. Scope
This runbook covers a practical Tier-1 deployment of `nsigner` over a TCP listener, with connectivity provided by FIPS as the network substrate.
Tier-1 objective:
- Keep `nsigner` transport simple (`--listen tcp:IPv4:PORT` or `--listen tcp:[IPv6]:PORT`).
- Use FIPS to carry traffic between peers.
- Do not add FIPS runtime dependencies into `nsigner`.
Out of scope in this document:
- Mandatory transport-level TLS/authentication hardening (still planned for a later phase).
- Automatic caller->npub enrichment from FIPS session metadata.
---
## 2. Architecture
Two cooperating layers:
1. **Signer process layer** (`nsigner`)
- Listens on operator-selected TCP endpoint (IPv4 or IPv6).
- Uses existing 4-byte big-endian framed JSON-RPC protocol.
- Keeps existing policy/prompt behavior.
2. **Network substrate layer** (FIPS)
- Establishes peer connectivity between nodes/qubes.
- Carries application traffic to the configured signer TCP endpoint.
Conceptually:
`client app -> local FIPS endpoint -> FIPS mesh -> remote FIPS endpoint -> signer TCP endpoint -> nsigner`
---
## 3. Prerequisites
On signer host/qube:
- Built `nsigner` binary.
- FIPS installed and running.
- Local firewall policy that keeps signer listener local-only.
On caller host/qube:
- FIPS installed and peered with signer host/qube.
- A client implementation that speaks `nsigner` framed JSON-RPC (see `documents/CLIENT_IMPLEMENTATION.md`).
Operational assumptions:
- Operator controls both endpoints.
- Manual verification of peer identity is performed in FIPS tooling before enabling signer traffic.
---
## 4. Start signer in Tier-1 TCP mode
Run `nsigner` in TCP listen mode:
```bash
./build/nsigner --listen tcp:[::]:8080
```
Or bind to a specific FIPS ULA address:
```bash
./build/nsigner --listen tcp:[fd00::1234]:8080
```
Behavior notes:
- Bind target is operator-controlled; pick the narrowest reachable address that satisfies your topology.
- No TUI hotkey loop is required in TCP mode; process serves requests until terminated.
- Caller identity is shown as a TCP endpoint descriptor in activity/prompt context.
---
## 5. FIPS substrate wiring pattern
Because FIPS deployment topologies vary, use this generic pattern:
1. Bind `nsigner` on a deliberate signer endpoint (`[::]:PORT` for broad reach, or specific `fd..` for tighter scope).
2. Configure FIPS service/forwarding so remote authenticated peer traffic is delivered to that signer endpoint.
3. On caller side, direct client traffic to the local FIPS ingress endpoint for that remote service.
Validation checklist:
- FIPS session is established between caller and signer nodes.
- Transport path from caller -> configured signer endpoint succeeds.
- `nsigner` receives framed request and returns framed response.
---
## 6. Minimal validation flow
### 6.1 Liveness check
From caller side, send a framed `get_public_key` request through the FIPS-backed endpoint.
Request JSON:
```json
{"id":"1","method":"get_public_key","params":[]}
```
Expected response:
```json
{"id":"1","result":"<hex_pubkey>"}
```
### 6.2 Signing check
Send `sign_event` with explicit role selector:
```json
{
"id": "2",
"method": "sign_event",
"params": ["<event_json>", {"role":"main"}]
}
```
Expected result: signed event JSON in `result`.
### 6.3 Negative check (policy)
Trigger a request path that requires prompt/denial and confirm client handles policy denial as a normal result path.
---
## 7. Security guardrails
- Prefer binding to a specific FIPS IPv6 ULA (`fd..`) rather than wildcard (`[::]`) when possible.
- Do not expose signer port directly on LAN/WAN.
- Keep FIPS peer allowlist tight; avoid broad trust domains.
- Treat FIPS connectivity as transport, not authorization bypass.
- Preserve interactive approval where required by policy.
---
## 8. Troubleshooting
### 8.1 `invalid tcp listen target`
Cause:
- `--listen` argument does not match `tcp:HOST:PORT` or `tcp:[IPv6]:PORT`.
Fix:
- Use valid numeric port and valid IPv4/IPv6 literal host.
### 8.2 Framing parse failures (`parse_error`)
Cause:
- Client sent line-delimited/raw JSON instead of framed JSON.
Fix:
- Send 4-byte big-endian length prefix followed by exact UTF-8 JSON payload bytes.
### 8.3 FIPS path up, signer path down
Cause:
- FIPS session exists but forwarding/service mapping to signer loopback endpoint is missing.
Fix:
- Verify substrate service routing config and local endpoint mapping.
---
## 9. Next hardening steps (post Tier-1)
- Add automated two-node validation script for operator smoke checks.
- Add optional identity enrichment from FIPS session metadata (`peer_npub`).
- Introduce remote TCP mode only with mandatory TLS + authenticated caller key flow.

View 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.

196
documents/QUBES_OS.md Normal file
View File

@@ -0,0 +1,196 @@
# QUBES_OS.md
## 1. Goal
Run `n_signer` inside a dedicated Qubes OS qube (for example `vault`-like behavior), and let caller qubes access signing via qrexec with explicit policy control.
This doc outlines what must be implemented/packaged for a reliable Qubes deployment path.
---
## 2. Current status (where we are now)
Implemented in current codebase:
- `nsigner` supports `--listen qrexec` and `--listen stdio`.
- Framing is transport-agnostic and shared via length-prefixed JSON (`4-byte big-endian length + payload`).
- In qrexec/stdio mode, server handles one framed request-response exchange.
- Caller identity extraction supports `QREXEC_REMOTE_DOMAIN`, surfaced as `qubes:<source-vm>` when available.
Still missing for complete Qubes integration:
- qrexec service file + wrapper script artifacts.
- dom0 qrexec policy artifacts with sane defaults.
- install/uninstall guidance and verification flow for real Qubes deployment.
- packaging path (`packaging/qubes/`) and docs wired into README map.
---
## 3. Architecture in Qubes
### 3.1 Components
- **Signer qube** (target): runs `nsigner` service entrypoint.
- **Caller qube(s)**: apps/tools invoking qrexec service.
- **dom0 policy**: controls which caller qubes may invoke signer service.
### 3.2 Request path
1. Caller qube invokes qrexec service (e.g. `qubes.NsignerRpc`).
2. qrexec starts service command inside signer qube.
3. Service command runs `nsigner --listen qrexec`.
4. Caller sends framed JSON-RPC request over qrexec stdio channel.
5. `nsigner` returns framed JSON-RPC response.
### 3.3 Trust and identity
- Source qube identity comes from `QREXEC_REMOTE_DOMAIN`.
- `n_signer` maps caller as `qubes:<source-vm>` where available.
- 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
## 4.1 Service entrypoint artifacts ✅ Implemented
Implemented repo artifacts:
- `packaging/qubes/rpc/qubes.NsignerRpc`
- `packaging/qubes/install-service.sh`
`qubes.NsignerRpc` runs:
- `exec /usr/local/bin/nsigner --listen qrexec`
Install inside the signer qube:
```bash
sudo sh packaging/qubes/install-service.sh
```
This installs the qrexec service to `/etc/qubes-rpc/qubes.NsignerRpc` with executable permissions.
## 4.2 dom0 policy artifacts ✅ Implemented
Implemented repo artifacts:
- `packaging/qubes/policy.d/40-nsigner.policy`
- `packaging/qubes/install-policy.sh`
Policy defaults now use explicit `ask` plus deny catch-all:
- `qubes.NsignerRpc * @anyvm @tag:nsigner-signer ask default_target=nsigner-vault`
- `qubes.NsignerRpc * @anyvm @anyvm deny`
Install in dom0:
```bash
sudo sh packaging/qubes/install-policy.sh
```
This installs `/etc/qubes/policy.d/40-nsigner.policy` and prints signer-tag guidance.
## 4.3 Policy model inside n_signer for qubes callers ✅ Implemented
Current code reads caller as `qubes:<vm>` and qrexec default behavior is hardened.
In qrexec mode, default prompt behavior is now:
- `PROMPT_EVERY_REQUEST`
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:
- `documents/qubes_client_examples.md`
Includes:
- shell helper example invoking `qrexec-client-vm` with framed request/response handling
- Python helper example implementing frame encode/decode over qrexec stdio channel
- reference to `documents/CLIENT_IMPLEMENTATION.md` for full protocol details
---
## 5. Operational runbook
## 5.1 Setup signer qube
- install `nsigner` binary at `/usr/local/bin/nsigner`
- run `sudo sh packaging/qubes/install-service.sh`
- verify `/etc/qubes-rpc/qubes.NsignerRpc` exists and is executable
## 5.2 Setup dom0 policy
- run `sudo sh packaging/qubes/install-policy.sh`
- tag signer qube (example): `qvm-tags nsigner-vault add nsigner-signer`
- reload qrexec policy per Qubes procedure/version
## 5.3 Verification
- 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
- malformed frame -> parse error response
- missing policy -> deny path
- missing `QREXEC_REMOTE_DOMAIN` -> fallback identity path
---
## 6. Security requirements
- Never run signer service in disposable qube if mnemonic persistence is expected.
- Prefer dedicated minimal template for signer qube.
- Keep qrexec policy narrowly scoped (explicit source + target).
- Require user approval for sensitive methods unless explicitly intended otherwise.
- Log caller identity and method (without secret payload logging).
---
## 7. Documentation tasks
Update these after packaging lands:
- `README.md`
- add Qubes deployment subsection under transport/usage
- add `documents/QUBES_OS.md` and moved `documents/CLIENT_IMPLEMENTATION.md` in document map
- `plans/nsigner.md`
- mark T1 done with packaging status clearly separated
---
## 8. Definition of done (Qubes)
Qubes integration is considered complete when:
1. qrexec service artifact exists and is installable.
2. dom0 policy artifact exists with secure default pattern.
3. End-to-end call from allowed caller qube succeeds.
4. Call from unauthorized qube is denied.
5. Caller displayed as `qubes:<vm>` in activity.
6. README + docs include full setup and troubleshooting.

520
documents/SECURITY.md Normal file
View 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.

View File

@@ -0,0 +1,93 @@
# qubes_client_examples.md
This document shows minimal caller-qube examples for invoking `nsigner --listen qrexec` through Qubes qrexec.
For complete protocol details (framing, JSON-RPC, error handling), see `documents/CLIENT_IMPLEMENTATION.md`.
---
## 1) Shell example (`qrexec-client-vm` + framed JSON)
This sends one `get_public_key` request and decodes one framed response.
```bash
#!/bin/sh
set -eu
TARGET_QUBE="nsigner-vault"
SERVICE="qubes.NsignerRpc"
REQ='{"id":"1","method":"get_public_key","params":[]}'
python3 - "$TARGET_QUBE" "$SERVICE" "$REQ" <<'PY'
import json
import struct
import subprocess
import sys
target, service, req_json = sys.argv[1], sys.argv[2], sys.argv[3]
frame = struct.pack(">I", len(req_json.encode("utf-8"))) + req_json.encode("utf-8")
p = subprocess.Popen(
["qrexec-client-vm", target, service],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
out, _ = p.communicate(frame)
if p.returncode != 0:
raise SystemExit(f"qrexec-client-vm failed: {p.returncode}")
if len(out) < 4:
raise SystemExit("short response (missing frame header)")
n = struct.unpack(">I", out[:4])[0]
payload = out[4:4+n]
if len(payload) != n:
raise SystemExit("short response payload")
print(json.dumps(json.loads(payload.decode("utf-8")), indent=2))
PY
```
---
## 2) Python example (explicit frame helpers over qrexec stdio)
```python
#!/usr/bin/env python3
import json
import struct
import subprocess
def frame_encode(obj: dict) -> bytes:
payload = json.dumps(obj, separators=(",", ":")).encode("utf-8")
return struct.pack(">I", len(payload)) + payload
def frame_decode(buf: bytes) -> dict:
if len(buf) < 4:
raise ValueError("missing frame header")
n = struct.unpack(">I", buf[:4])[0]
payload = buf[4:4 + n]
if len(payload) != n:
raise ValueError("short frame payload")
return json.loads(payload.decode("utf-8"))
def call_nsigner_qrexec(target_qube: str, request: dict) -> dict:
proc = subprocess.Popen(
["qrexec-client-vm", target_qube, "qubes.NsignerRpc"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = proc.communicate(frame_encode(request))
if proc.returncode != 0:
raise RuntimeError(f"qrexec failed ({proc.returncode}): {err.decode('utf-8', 'replace')}")
return frame_decode(out)
if __name__ == "__main__":
req = {"id": "1", "method": "get_public_key", "params": []}
resp = call_nsigner_qrexec("nsigner-vault", req)
print(json.dumps(resp, indent=2))
```

View 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())

View 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())

View File

@@ -0,0 +1,162 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>n_signer Feather WebUSB Demo</title>
<style>
body { font-family: sans-serif; margin: 16px; max-width: 840px; }
button { margin-right: 8px; margin-bottom: 8px; }
pre { background: #111; color: #ddd; padding: 12px; border-radius: 6px; overflow: auto; }
</style>
</head>
<body>
<h1>n_signer Feather WebUSB Demo</h1>
<p>Connect to the ESP32-S3 WebUSB interface, send authenticated <code>get_public_key</code>, and display the result.</p>
<button id="connectBtn">Connect</button>
<button id="pubkeyBtn" disabled>Get Public Key</button>
<pre id="log"></pre>
<script type="module">
import { schnorr } from "https://esm.sh/@noble/curves@1.5.0/secp256k1?bundle";
const logEl = document.getElementById("log");
const connectBtn = document.getElementById("connectBtn");
const pubkeyBtn = document.getElementById("pubkeyBtn");
let dev = null;
let iface = null;
const EP_OUT = 1;
const EP_IN = 1;
function log(...args) {
logEl.textContent += args.join(" ") + "\n";
logEl.scrollTop = logEl.scrollHeight;
}
function hex(bytes) {
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
}
function utf8(s) {
return new TextEncoder().encode(s);
}
async function sha256Hex(dataBytes) {
const h = await crypto.subtle.digest("SHA-256", dataBytes);
return hex(new Uint8Array(h));
}
function be32(n) {
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
}
async function buildAuth(method, params) {
const callerPriv = Uint8Array.from({ length: 32 }, (_, i) => i + 1);
const callerPubX = hex(schnorr.getPublicKey(callerPriv));
const createdAt = Math.floor(Date.now() / 1000);
const paramsJson = JSON.stringify(params);
const bodyHash = await sha256Hex(utf8(paramsJson));
const tags = [
["nsigner_rpc", "1"],
["nsigner_method", method],
["nsigner_body_hash", bodyHash],
];
const content = "webusb-demo";
const ser = JSON.stringify([0, callerPubX, createdAt, 27235, tags, content]);
const id = await sha256Hex(utf8(ser));
const sigBytes = await schnorr.sign(id, callerPriv, new Uint8Array(32));
const sigHex = typeof sigBytes === "string" ? sigBytes : hex(sigBytes);
return { id, pubkey: callerPubX, created_at: createdAt, kind: 27235, tags, content, sig: sigHex };
}
async function sendRpc(reqObj) {
const body = utf8(JSON.stringify(reqObj));
const frame = new Uint8Array(4 + body.length);
frame.set(be32(body.length), 0);
frame.set(body, 4);
await dev.transferOut(EP_OUT, frame);
const deadline = Date.now() + 10000;
let ring = new Uint8Array(0);
while (Date.now() < deadline) {
const r = await dev.transferIn(EP_IN, 512);
if (!r.data || r.data.byteLength === 0) {
continue;
}
const chunk = new Uint8Array(r.data.buffer, r.data.byteOffset, r.data.byteLength);
const next = new Uint8Array(ring.length + chunk.length);
next.set(ring, 0);
next.set(chunk, ring.length);
ring = next;
while (ring.length >= 4) {
const n = (ring[0] << 24) | (ring[1] << 16) | (ring[2] << 8) | ring[3];
if (n <= 0 || n > 1_000_000) {
ring = ring.slice(1);
continue;
}
if (ring.length < 4 + n) {
break;
}
const payload = ring.slice(4, 4 + n);
ring = ring.slice(4 + n);
const txt = new TextDecoder().decode(payload);
return JSON.parse(txt);
}
}
throw new Error("Timed out waiting for framed response");
}
async function connect() {
dev = await navigator.usb.requestDevice({ filters: [{ vendorId: 0x303a }] });
await dev.open();
if (dev.configuration === null) {
await dev.selectConfiguration(1);
}
const intf = dev.configuration.interfaces.find(i =>
i.alternates.some(a => a.interfaceClass === 0xff)
);
if (!intf) throw new Error("No vendor WebUSB interface found");
iface = intf.interfaceNumber;
await dev.claimInterface(iface);
const alt = intf.alternates.find(a => a.interfaceClass === 0xff);
await dev.selectAlternateInterface(iface, alt.alternateSetting);
await dev.controlTransferOut({
requestType: "class",
recipient: "interface",
request: 0x22,
value: 1,
index: iface,
});
pubkeyBtn.disabled = false;
log("Connected. Interface", String(iface));
}
connectBtn.addEventListener("click", async () => {
try {
await connect();
} catch (e) {
log("Connect failed:", String(e));
}
});
pubkeyBtn.addEventListener("click", async () => {
try {
const params = [];
const auth = await buildAuth("get_public_key", params);
const req = { jsonrpc: "2.0", id: "web-1", method: "get_public_key", params, auth };
const resp = await sendRpc(req);
log("Response:", JSON.stringify(resp));
} catch (e) {
log("RPC failed:", String(e));
}
});
</script>
</body>
</html>

View 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"])

View 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;
}

View 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;
}

View File

@@ -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.

View File

@@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(feather_s3_tft)

Submodule firmware/feather_s3_tft/components/secp256k1 added at 7262adb4b4

View 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

View File

@@ -0,0 +1,20 @@
idf_component_register(
SRCS
"main.c"
"display.c"
"mnemonic.c"
"key_derivation.c"
"bech32.c"
"usb_transport.c"
INCLUDE_DIRS
"."
REQUIRES
mbedtls
secp256k1
json
espressif__esp_tinyusb
espressif__tinyusb
PRIV_REQUIRES
esp_driver_gpio
esp_driver_spi
)

View 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;
}

View 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);

View File

@@ -0,0 +1,323 @@
#include "display.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_check.h"
#include "esp_log.h"
#include "esp_rom_sys.h"
#include "font5x7.h"
#define TAG "display"
#define PIN_NUM_MOSI 35
#define PIN_NUM_CLK 36
#define PIN_NUM_CS 42
#define PIN_NUM_DC 40
#define PIN_NUM_RST 41
#define PIN_NUM_BL 45
#define PIN_NUM_TFT_POWER 7
#define LCD_HOST SPI2_HOST
#define ST7789_SWRESET 0x01
#define ST7789_SLPOUT 0x11
#define ST7789_COLMOD 0x3A
#define ST7789_MADCTL 0x36
#define ST7789_CASET 0x2A
#define ST7789_RASET 0x2B
#define ST7789_RAMWR 0x2C
#define ST7789_DISPON 0x29
#define ST7789_INVON 0x21
#define X_OFFSET 40
#define Y_OFFSET 52
#define CHAR_SCALE 2
#define CHAR_W (6 * CHAR_SCALE)
#define CHAR_H (8 * CHAR_SCALE)
static spi_device_handle_t s_lcd = NULL;
static bool s_initialized = false;
static esp_err_t lcd_send_cmd(uint8_t cmd)
{
gpio_set_level(PIN_NUM_DC, 0);
spi_transaction_t t = {
.length = 8,
.tx_buffer = &cmd,
};
return spi_device_transmit(s_lcd, &t);
}
static esp_err_t lcd_send_data(const void *data, size_t len)
{
if (len == 0) {
return ESP_OK;
}
gpio_set_level(PIN_NUM_DC, 1);
spi_transaction_t t = {
.length = len * 8,
.tx_buffer = data,
};
return spi_device_transmit(s_lcd, &t);
}
static esp_err_t lcd_set_window(int x, int y, int w, int h)
{
if (w <= 0 || h <= 0) {
return ESP_ERR_INVALID_ARG;
}
uint16_t x0 = (uint16_t)(x + X_OFFSET);
uint16_t x1 = (uint16_t)(x + w - 1 + X_OFFSET);
uint16_t y0 = (uint16_t)(y + Y_OFFSET);
uint16_t y1 = (uint16_t)(y + h - 1 + Y_OFFSET);
uint8_t col[4] = { x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF };
uint8_t row[4] = { y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF };
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_CASET), TAG, "CASET failed");
ESP_RETURN_ON_ERROR(lcd_send_data(col, sizeof(col)), TAG, "CASET data failed");
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_RASET), TAG, "RASET failed");
ESP_RETURN_ON_ERROR(lcd_send_data(row, sizeof(row)), TAG, "RASET data failed");
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_RAMWR), TAG, "RAMWR failed");
return ESP_OK;
}
static esp_err_t lcd_hw_reset(void)
{
gpio_set_level(PIN_NUM_RST, 1);
esp_rom_delay_us(10000);
gpio_set_level(PIN_NUM_RST, 0);
esp_rom_delay_us(10000);
gpio_set_level(PIN_NUM_RST, 1);
esp_rom_delay_us(120000);
return ESP_OK;
}
static esp_err_t lcd_panel_init(void)
{
ESP_RETURN_ON_ERROR(lcd_hw_reset(), TAG, "HW reset failed");
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_SWRESET), TAG, "SWRESET failed");
esp_rom_delay_us(150000);
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_SLPOUT), TAG, "SLPOUT failed");
esp_rom_delay_us(120000);
const uint8_t colmod = 0x55; // 16-bit
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_COLMOD), TAG, "COLMOD cmd failed");
ESP_RETURN_ON_ERROR(lcd_send_data(&colmod, 1), TAG, "COLMOD data failed");
esp_rom_delay_us(10000);
const uint8_t madctl = 0xA0; // MV | MY, landscape for Reverse TFT Feather
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_MADCTL), TAG, "MADCTL cmd failed");
ESP_RETURN_ON_ERROR(lcd_send_data(&madctl, 1), TAG, "MADCTL data failed");
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_INVON), TAG, "INVON failed");
esp_rom_delay_us(10000);
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_DISPON), TAG, "DISPON failed");
esp_rom_delay_us(120000);
gpio_set_level(PIN_NUM_BL, 1);
return ESP_OK;
}
static inline uint16_t clamp_u16_color(uint16_t c)
{
return (uint16_t)((c >> 8) | (c << 8)); // swap endian for wire order
}
esp_err_t display_init(void)
{
if (s_initialized) {
return ESP_OK;
}
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << PIN_NUM_DC) | (1ULL << PIN_NUM_RST) | (1ULL << PIN_NUM_BL) |
(1ULL << PIN_NUM_TFT_POWER),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_RETURN_ON_ERROR(gpio_config(&io_conf), TAG, "gpio_config failed");
// Reverse TFT Feather requires TFT/I2C power gate enabled for panel logic.
gpio_set_level(PIN_NUM_TFT_POWER, 1);
esp_rom_delay_us(10000);
spi_bus_config_t buscfg = {
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = -1,
.sclk_io_num = PIN_NUM_CLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = DISPLAY_WIDTH * 20 * sizeof(uint16_t),
};
ESP_RETURN_ON_ERROR(spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO), TAG, "spi_bus_initialize failed");
spi_device_interface_config_t devcfg = {
.clock_speed_hz = 40 * 1000 * 1000,
.mode = 0,
.spics_io_num = PIN_NUM_CS,
.queue_size = 4,
};
ESP_RETURN_ON_ERROR(spi_bus_add_device(LCD_HOST, &devcfg, &s_lcd), TAG, "spi_bus_add_device failed");
ESP_RETURN_ON_ERROR(lcd_panel_init(), TAG, "panel init failed");
s_initialized = true;
ESP_RETURN_ON_ERROR(display_clear(RGB565_BLACK), TAG, "clear failed");
ESP_LOGI(TAG, "ST7789 initialized");
return ESP_OK;
}
esp_err_t display_fill_rect(int x, int y, int w, int h, uint16_t color)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (x >= DISPLAY_WIDTH || y >= DISPLAY_HEIGHT || w <= 0 || h <= 0) {
return ESP_OK;
}
if (x < 0) {
w += x;
x = 0;
}
if (y < 0) {
h += y;
y = 0;
}
if (x + w > DISPLAY_WIDTH) {
w = DISPLAY_WIDTH - x;
}
if (y + h > DISPLAY_HEIGHT) {
h = DISPLAY_HEIGHT - y;
}
if (w <= 0 || h <= 0) {
return ESP_OK;
}
const uint16_t wire_color = clamp_u16_color(color);
const int chunk_pixels = DISPLAY_WIDTH * 8;
static uint16_t line_buf[DISPLAY_WIDTH * 8];
for (int i = 0; i < chunk_pixels; ++i) {
line_buf[i] = wire_color;
}
ESP_RETURN_ON_ERROR(lcd_set_window(x, y, w, h), TAG, "set window failed");
int total = w * h;
while (total > 0) {
int batch = total > chunk_pixels ? chunk_pixels : total;
ESP_RETURN_ON_ERROR(lcd_send_data(line_buf, batch * sizeof(uint16_t)), TAG, "fill data failed");
total -= batch;
}
return ESP_OK;
}
esp_err_t display_clear(uint16_t color)
{
return display_fill_rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, color);
}
static esp_err_t draw_char(int x, int y, char c, uint16_t fg, uint16_t bg)
{
if (c < 0x20 || c > 0x7F) {
c = '?';
}
const uint8_t *glyph = font5x7[(uint8_t)c - 0x20];
for (int col = 0; col < 5; ++col) {
uint8_t bits = glyph[col];
for (int row = 0; row < 7; ++row) {
bool on = (bits >> row) & 0x1;
ESP_RETURN_ON_ERROR(
display_fill_rect(x + (col * CHAR_SCALE), y + (row * CHAR_SCALE), CHAR_SCALE, CHAR_SCALE, on ? fg : bg),
TAG,
"pixel draw failed");
}
}
// spacing column + bottom row
ESP_RETURN_ON_ERROR(display_fill_rect(x + (5 * CHAR_SCALE), y, CHAR_SCALE, 7 * CHAR_SCALE, bg), TAG, "spacing failed");
ESP_RETURN_ON_ERROR(display_fill_rect(x, y + (7 * CHAR_SCALE), 6 * CHAR_SCALE, CHAR_SCALE, bg), TAG, "baseline failed");
return ESP_OK;
}
esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (!text) {
return ESP_ERR_INVALID_ARG;
}
int cursor_x = x;
int cursor_y = y;
for (size_t i = 0; text[i] != '\0'; ++i) {
char c = text[i];
if (c == '\n') {
cursor_x = x;
cursor_y += CHAR_H;
continue;
}
if (cursor_x + CHAR_W > DISPLAY_WIDTH) {
cursor_x = x;
cursor_y += CHAR_H;
}
if (cursor_y + CHAR_H > DISPLAY_HEIGHT) {
break;
}
ESP_RETURN_ON_ERROR(draw_char(cursor_x, cursor_y, c, fg, bg), TAG, "draw_char failed");
cursor_x += CHAR_W;
}
return ESP_OK;
}
esp_err_t display_draw_button_status(int btn_id, bool pressed)
{
if (btn_id < 0 || btn_id > 2) {
return ESP_ERR_INVALID_ARG;
}
const int x = 10;
const int y = 30 + btn_id * 32;
const int w = DISPLAY_WIDTH - 20;
const int h = 24;
uint16_t box_color = pressed ? RGB565_GREEN : RGB565_RED;
uint16_t text_color = RGB565_WHITE;
char line[32];
snprintf(line, sizeof(line), "D%d: %s", btn_id, pressed ? "PRESSED" : "RELEASED");
ESP_RETURN_ON_ERROR(display_fill_rect(x, y, w, h, box_color), TAG, "status box failed");
ESP_RETURN_ON_ERROR(display_draw_text(x + 6, y + 8, line, text_color, box_color), TAG, "status text failed");
return ESP_OK;
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
#define DISPLAY_WIDTH 240
#define DISPLAY_HEIGHT 135
#define RGB565_BLACK 0x0000
#define RGB565_WHITE 0xFFFF
#define RGB565_RED 0xF800
#define RGB565_GREEN 0x07E0
#define RGB565_BLUE 0x001F
#define RGB565_YELLOW 0xFFE0
#define RGB565_CYAN 0x07FF
#define RGB565_MAGENTA 0xF81F
esp_err_t display_init(void);
esp_err_t display_clear(uint16_t color);
esp_err_t display_fill_rect(int x, int y, int w, int h, uint16_t color);
esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg);
esp_err_t display_draw_button_status(int btn_id, bool pressed);
#ifdef __cplusplus
}
#endif

View 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
};

View File

@@ -0,0 +1,5 @@
dependencies:
idf:
version: ">=5.0.0"
espressif/esp_tinyusb:
version: "^1.4.4"

View File

@@ -0,0 +1,245 @@
#include "key_derivation.h"
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "esp_random.h"
#include "mbedtls/md.h"
#include "secp256k1.h"
#include "secp256k1_extrakeys.h"
#include "secp256k1_schnorrsig.h"
#define BIP32_HARDENED_FLAG 0x80000000u
typedef struct {
uint8_t priv[32];
uint8_t chain[32];
uint8_t pub[33];
} hd_key_t;
static int hmac_sha512(const uint8_t *key,
size_t key_len,
const uint8_t *data,
size_t data_len,
uint8_t out[64])
{
const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA512);
if (md_info == NULL) {
return -1;
}
if (mbedtls_md_hmac(md_info, key, key_len, data, data_len, out) != 0) {
return -1;
}
return 0;
}
static secp256k1_context *create_context(void)
{
uint8_t rand32[32] = {0};
secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
if (ctx == NULL) {
return NULL;
}
esp_fill_random(rand32, sizeof(rand32));
if (!secp256k1_context_randomize(ctx, rand32)) {
memset(rand32, 0, sizeof(rand32));
secp256k1_context_destroy(ctx);
return NULL;
}
memset(rand32, 0, sizeof(rand32));
return ctx;
}
static int compute_compressed_pubkey(const secp256k1_context *ctx, const uint8_t priv[32], uint8_t pub33[33])
{
secp256k1_pubkey pubkey;
size_t out_len = 33;
if (!secp256k1_ec_pubkey_create(ctx, &pubkey, priv)) {
return -1;
}
if (!secp256k1_ec_pubkey_serialize(ctx, pub33, &out_len, &pubkey, SECP256K1_EC_COMPRESSED)) {
return -1;
}
if (out_len != 33) {
return -1;
}
return 0;
}
static int bip32_master_from_seed(const secp256k1_context *ctx, const uint8_t seed[64], hd_key_t *master)
{
static const uint8_t kBitcoinSeed[] = "Bitcoin seed";
uint8_t i64[64] = {0};
if (hmac_sha512(kBitcoinSeed, sizeof(kBitcoinSeed) - 1, seed, 64, i64) != 0) {
return -1;
}
memcpy(master->priv, i64, 32);
memcpy(master->chain, i64 + 32, 32);
memset(i64, 0, sizeof(i64));
if (!secp256k1_ec_seckey_verify(ctx, master->priv)) {
return -1;
}
return compute_compressed_pubkey(ctx, master->priv, master->pub);
}
static int bip32_ckd_priv(const secp256k1_context *ctx,
const hd_key_t *parent,
uint32_t index,
hd_key_t *child)
{
uint8_t data[37] = {0};
uint8_t i64[64] = {0};
if (index & BIP32_HARDENED_FLAG) {
data[0] = 0x00;
memcpy(data + 1, parent->priv, 32);
} else {
memcpy(data, parent->pub, 33);
}
data[33] = (uint8_t)((index >> 24) & 0xFF);
data[34] = (uint8_t)((index >> 16) & 0xFF);
data[35] = (uint8_t)((index >> 8) & 0xFF);
data[36] = (uint8_t)(index & 0xFF);
if (hmac_sha512(parent->chain, 32, data, sizeof(data), i64) != 0) {
return -1;
}
memcpy(child->priv, parent->priv, 32);
memcpy(child->chain, i64 + 32, 32);
if (!secp256k1_ec_seckey_tweak_add(ctx, child->priv, i64)) {
memset(i64, 0, sizeof(i64));
return -1;
}
memset(i64, 0, sizeof(i64));
if (!secp256k1_ec_seckey_verify(ctx, child->priv)) {
return -1;
}
return compute_compressed_pubkey(ctx, child->priv, child->pub);
}
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32])
{
static const uint32_t path[5] = {
44u | BIP32_HARDENED_FLAG,
1237u | BIP32_HARDENED_FLAG,
0u | BIP32_HARDENED_FLAG,
0u,
0u,
};
secp256k1_context *ctx = NULL;
hd_key_t node;
if (seed == NULL || privkey == NULL || pubkey == NULL) {
return -1;
}
memset(&node, 0, sizeof(node));
ctx = create_context();
if (ctx == NULL) {
return -1;
}
if (bip32_master_from_seed(ctx, seed, &node) != 0) {
secp256k1_context_destroy(ctx);
return -1;
}
for (size_t i = 0; i < sizeof(path) / sizeof(path[0]); ++i) {
hd_key_t next;
memset(&next, 0, sizeof(next));
if (bip32_ckd_priv(ctx, &node, path[i], &next) != 0) {
memset(&node, 0, sizeof(node));
secp256k1_context_destroy(ctx);
return -1;
}
memset(&node, 0, sizeof(node));
node = next;
}
memcpy(privkey, node.priv, 32);
{
secp256k1_keypair kp;
secp256k1_xonly_pubkey xonly;
if (!secp256k1_keypair_create(ctx, &kp, node.priv)) {
memset(&node, 0, sizeof(node));
secp256k1_context_destroy(ctx);
return -1;
}
if (!secp256k1_keypair_xonly_pub(ctx, &xonly, NULL, &kp)) {
memset(&node, 0, sizeof(node));
secp256k1_context_destroy(ctx);
return -1;
}
if (!secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xonly)) {
memset(&node, 0, sizeof(node));
secp256k1_context_destroy(ctx);
return -1;
}
}
memset(&node, 0, sizeof(node));
secp256k1_context_destroy(ctx);
return 0;
}
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64])
{
secp256k1_context *ctx = NULL;
secp256k1_keypair kp;
uint8_t aux[32] = {0};
if (privkey == NULL || msg32 == NULL || sig64 == NULL) {
return -1;
}
ctx = create_context();
if (ctx == NULL) {
return -1;
}
if (!secp256k1_keypair_create(ctx, &kp, privkey)) {
secp256k1_context_destroy(ctx);
return -1;
}
esp_fill_random(aux, sizeof(aux));
if (!secp256k1_schnorrsig_sign32(ctx, sig64, msg32, &kp, aux)) {
memset(aux, 0, sizeof(aux));
secp256k1_context_destroy(ctx);
return -1;
}
memset(aux, 0, sizeof(aux));
secp256k1_context_destroy(ctx);
return 0;
}

View File

@@ -0,0 +1,6 @@
#pragma once
#include <stdint.h>
int derive_nostr_key(const uint8_t seed[64], uint8_t privkey[32], uint8_t pubkey[32]);
int schnorr_sign32(const uint8_t privkey[32], const uint8_t msg32[32], uint8_t sig64[64]);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
#include "mnemonic.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "esp_random.h"
#include "mbedtls/md.h"
#include "mbedtls/pkcs5.h"
#include "mbedtls/sha256.h"
#include "mnemonic_wordlist.h"
#define MNEMONIC_12_WORDS 12
#define ENTROPY_12_BYTES 16
#define CHECKSUM_12_BITS 4
#define TOTAL_12_BITS (ENTROPY_12_BYTES * 8 + CHECKSUM_12_BITS)
static bool append_word(char *out, size_t out_len, size_t *cursor, const char *word)
{
const size_t word_len = strlen(word);
if (*cursor >= out_len) {
return false;
}
if (*cursor != 0) {
if (*cursor + 1 >= out_len) {
return false;
}
out[*cursor] = ' ';
(*cursor)++;
}
if (*cursor + word_len >= out_len) {
return false;
}
memcpy(out + *cursor, word, word_len);
*cursor += word_len;
out[*cursor] = '\0';
return true;
}
static uint8_t get_bit_from_entropy_checksum(const uint8_t entropy[ENTROPY_12_BYTES],
const uint8_t checksum[32],
size_t bit_index)
{
if (bit_index < ENTROPY_12_BYTES * 8) {
const size_t byte_index = bit_index / 8;
const size_t bit_in_byte = 7 - (bit_index % 8);
return (uint8_t)((entropy[byte_index] >> bit_in_byte) & 0x01);
}
const size_t checksum_bit_index = bit_index - (ENTROPY_12_BYTES * 8);
const size_t bit_in_byte = 7 - checksum_bit_index;
return (uint8_t)((checksum[0] >> bit_in_byte) & 0x01);
}
int generate_mnemonic_12(char *out, size_t out_len)
{
uint8_t entropy[ENTROPY_12_BYTES] = {0};
uint8_t hash[32] = {0};
size_t cursor = 0;
if (out == NULL || out_len == 0) {
return -1;
}
out[0] = '\0';
esp_fill_random(entropy, sizeof(entropy));
mbedtls_sha256(entropy, sizeof(entropy), hash, 0);
for (size_t w = 0; w < MNEMONIC_12_WORDS; ++w) {
const size_t start_bit = w * 11;
uint16_t index = 0;
for (size_t b = 0; b < 11; ++b) {
index = (uint16_t)((index << 1) |
get_bit_from_entropy_checksum(entropy, hash, start_bit + b));
}
if (index >= 2048) {
memset(entropy, 0, sizeof(entropy));
memset(hash, 0, sizeof(hash));
return -1;
}
if (!append_word(out, out_len, &cursor, BIP39_WORDLIST[index])) {
memset(entropy, 0, sizeof(entropy));
memset(hash, 0, sizeof(hash));
out[0] = '\0';
return -1;
}
}
if (TOTAL_12_BITS != 132) {
memset(entropy, 0, sizeof(entropy));
memset(hash, 0, sizeof(hash));
out[0] = '\0';
return -1;
}
memset(entropy, 0, sizeof(entropy));
memset(hash, 0, sizeof(hash));
return 0;
}
int mnemonic_to_seed(const char *mnemonic, uint8_t seed[64])
{
const mbedtls_md_info_t *md_info;
mbedtls_md_context_t ctx;
uint8_t salt[8] = {'m', 'n', 'e', 'm', 'o', 'n', 'i', 'c'};
if (mnemonic == NULL || seed == NULL) {
return -1;
}
md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA512);
if (md_info == NULL) {
return -1;
}
mbedtls_md_init(&ctx);
if (mbedtls_md_setup(&ctx, md_info, 1) != 0) {
mbedtls_md_free(&ctx);
return -1;
}
(void)ctx;
if (mbedtls_pkcs5_pbkdf2_hmac_ext(MBEDTLS_MD_SHA512,
(const uint8_t *)mnemonic,
strlen(mnemonic),
salt,
sizeof(salt),
2048,
64,
seed) != 0) {
mbedtls_md_free(&ctx);
return -1;
}
mbedtls_md_free(&ctx);
return 0;
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
int generate_mnemonic_12(char *out, size_t out_len);
int mnemonic_to_seed(const char *mnemonic, uint8_t seed[64]);

View 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"
};

View 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;
}

View 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);

View 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;
}

View 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);

View File

@@ -0,0 +1 @@
6a50305bc61c7a361da8c0833642be824e92dacb0a6001719a832a4e96e471bf

View File

@@ -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

View File

@@ -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")

View File

@@ -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"

View File

@@ -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.

View File

@@ -0,0 +1,166 @@
# Espressif's additions to TinyUSB
[![Component Registry](https://components.espressif.com/components/espressif/esp_tinyusb/badge.svg)](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).

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,2 @@
supplier: 'Organization: Espressif Systems (Shanghai) CO LTD'
originator: 'Organization: Espressif Systems (Shanghai) CO LTD'

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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"]
```

View File

@@ -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)

View File

@@ -0,0 +1,4 @@
idf_component_register(SRC_DIRS .
INCLUDE_DIRS .
REQUIRES unity
WHOLE_ARCHIVE)

View File

@@ -0,0 +1,5 @@
## IDF Component Manager Manifest File
dependencies:
espressif/esp_tinyusb:
version: "*"
override_path: "../../../"

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,4 @@
idf_component_register(SRC_DIRS .
INCLUDE_DIRS .
REQUIRES unity
WHOLE_ARCHIVE)

View File

@@ -0,0 +1,5 @@
## IDF Component Manager Manifest File
dependencies:
espressif/esp_tinyusb:
version: "*"
override_path: "../../../"

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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')

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,4 @@
idf_component_register(SRC_DIRS .
INCLUDE_DIRS .
REQUIRES unity
WHOLE_ARCHIVE)

View File

@@ -0,0 +1,5 @@
## IDF Component Manager Manifest File
dependencies:
espressif/esp_tinyusb:
version: "*"
override_path: "../../../"

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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')

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,4 @@
idf_component_register(SRC_DIRS .
INCLUDE_DIRS .
REQUIRES unity
WHOLE_ARCHIVE)

View File

@@ -0,0 +1,5 @@
## IDF Component Manager Manifest File
dependencies:
espressif/esp_tinyusb:
version: "*"
override_path: "../../../"

View File

@@ -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();
}

View File

@@ -0,0 +1,117 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "soc/soc_caps.h"
#if SOC_USB_OTG_SUPPORTED
//
#include <stdio.h>
#include <string.h>
//
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
//
#include "esp_system.h"
#include "esp_log.h"
#include "esp_err.h"
//
#include "unity.h"
#include "tinyusb.h"
static const char *TAG = "default_task";
SemaphoreHandle_t wait_mount = NULL;
#define TEARDOWN_DEVICE_DELAY_MS 1000
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN)
static uint8_t const test_configuration_descriptor[] = {
// Config number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1, 0, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_SELF_POWERED | TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
};
static const tusb_desc_device_t test_device_descriptor = {
.bLength = sizeof(test_device_descriptor),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0x303A, // This is Espressif VID. This needs to be changed according to Users / Customers
.idProduct = 0x4002,
.bcdDevice = 0x100,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01
};
#if (TUD_OPT_HIGH_SPEED)
static const tusb_desc_device_qualifier_t device_qualifier = {
.bLength = sizeof(tusb_desc_device_qualifier_t),
.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.bNumConfigurations = 0x01,
.bReserved = 0
};
#endif // TUD_OPT_HIGH_SPEED
// Invoked when device is mounted
void tud_mount_cb(void)
{
xSemaphoreGive(wait_mount);
}
/**
* @brief TinyUSB Task specific testcase
*
* Scenario:
* 1. Install TinyUSB driver
* 2. Wait tud_mount_cb() until TUSB_DEVICE_DELAY_MS
* 3. Wait TUSB_DEVICE_DELAY_MS
* 4. Teardown TinyUSB
* 5. Release resources
*/
TEST_CASE("tinyusb_default_task_with_init", "[esp_tinyusb][tusb_task]")
{
wait_mount = xSemaphoreCreateBinary();
TEST_ASSERT_NOT_EQUAL(NULL, wait_mount);
// TinyUSB driver configuration
const tinyusb_config_t tusb_cfg = {
.device_descriptor = &test_device_descriptor,
.string_descriptor = NULL,
.string_descriptor_count = 0,
.external_phy = false,
#if (TUD_OPT_HIGH_SPEED)
.fs_configuration_descriptor = test_configuration_descriptor,
.hs_configuration_descriptor = test_configuration_descriptor,
.qualifier_descriptor = &device_qualifier,
#else
.configuration_descriptor = test_configuration_descriptor,
#endif // TUD_OPT_HIGH_SPEED
};
TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_install(&tusb_cfg));
// Wait for the usb event
ESP_LOGD(TAG, "wait mount...");
TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(wait_mount, pdMS_TO_TICKS(TEARDOWN_DEVICE_DELAY_MS)));
ESP_LOGD(TAG, "mounted");
// Teardown
vTaskDelay(pdMS_TO_TICKS(TEARDOWN_DEVICE_DELAY_MS));
TEST_ASSERT_EQUAL(ESP_OK, tinyusb_driver_uninstall());
// Remove primitives
vSemaphoreDelete(wait_mount);
}
#endif

View File

@@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import pytest
from pytest_embedded_idf.dut import IdfDut
@pytest.mark.esp32s2
@pytest.mark.esp32s3
@pytest.mark.esp32p4
@pytest.mark.usb_device
def test_usb_default_task_with_init(dut: IdfDut) -> None:
dut.run_all_single_board_cases(group='tusb_task')

Some files were not shown because too many files have changed in this diff Show More