diff --git a/.gitignore b/.gitignore index 378eac25d..36e948fc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,25 @@ -build +# Build work directories (not the final artifacts) +build/ +_build/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# macOS +.DS_Store +._* + +# Unity +**/Library/ +**/Logs/ +**/Temp/ +**/obj/ +**/UserSettings/ +*.csproj +*.sln +*.slnx +mono_crash.*.json diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 000000000..dc00bd922 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,147 @@ +# Building libpeer + +This document describes how to build libpeer, including the FAT library that combines all dependencies into a single static library. + +## Prerequisites + +- CMake 3.16 or higher +- C compiler (GCC or Clang) +- Git +- Bash shell + +### Installing dependencies on Ubuntu/Debian + +```bash +sudo apt -y install git cmake build-essential +``` + +### Installing dependencies on macOS + +```bash +brew install cmake +``` + +## Clone the Repository + +Clone the repository with all submodules: + +```bash +git clone --recursive git@github.com:dsugisawa-mixi/libpeer.git +cd libpeer +``` + +If you already cloned without `--recursive`, initialize submodules: + +```bash +git submodule update --init --recursive +``` + +## Build Steps + +### 1. Configure CMake + +```bash +cmake -S . -B build +``` + +#### Build Options + +| Option | Default | Description | +|--------|---------|-------------| +| `ENABLE_TESTS` | OFF | Enable building tests | +| `BUILD_SHARED_LIBS` | OFF | Build shared libraries | +| `ADDRESS_SANITIZER` | OFF | Build with AddressSanitizer | +| `MEMORY_SANITIZER` | OFF | Build with MemorySanitizer | +| `THREAD_SANITIZER` | OFF | Build with ThreadSanitizer | +| `UNDEFINED_BEHAVIOR_SANITIZER` | OFF | Build with UndefinedBehaviorSanitizer | + + +### 2. Build the Project + +```bash +cmake --build build +``` + +This will: +- Build all third-party dependencies (mbedtls, libsrtp, usrsctp, cJSON) +- Build the main `libpeer` library +- Build the example applications + +### 3. Build the FAT Library + +The FAT library combines all static libraries into a single archive file, making it easier to link your application. + +```bash +cmake --build build --target fat_library +``` + +The FAT library will be created at: + +``` +build/dist/lib/libpeer_fat.a +``` + +### Libraries Included in FAT Library + +The FAT library (`libpeer_fat.a`) includes: + +- `libpeer.a` - Main WebRTC library +- `libusrsctp.a` - SCTP implementation for DataChannel +- `libsrtp2.a` - Secure RTP +- `libmbedtls.a` - TLS library +- `libmbedx509.a` - X.509 certificate handling +- `libmbedcrypto.a` - Cryptographic functions +- `libcjson.a` - JSON parsing + +## Using the FAT Library + +Link against the FAT library in your project: + +```bash +gcc -o myapp myapp.c -Ibuild/dist/include -Lbuild/dist/lib -lpeer_fat -lpthread +``` + +Or in CMake: + +```cmake +add_executable(myapp myapp.c) +target_include_directories(myapp PRIVATE ${LIBPEER_BUILD_DIR}/dist/include) +target_link_libraries(myapp ${LIBPEER_BUILD_DIR}/dist/lib/libpeer_fat.a pthread) +``` + +## Cross-Compilation + +To cross-compile for another platform, specify a CMake toolchain file: + +```bash +cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake +cmake --build build +cmake --build build --target fat_library +``` + +## Build Output Structure + +After building, the output directory structure is: + +``` +build/ +├── dist/ +│ ├── include/ # Header files +│ │ ├── cjson/ +│ │ ├── mbedtls/ +│ │ ├── srtp2/ +│ │ └── usrsctp/ +│ └── lib/ +│ ├── libcjson.a +│ ├── libmbedcrypto.a +│ ├── libmbedtls.a +│ ├── libmbedx509.a +│ ├── libpeer_fat.a # Combined FAT library +│ ├── libsrtp2.a +│ └── libusrsctp.a +├── src/ +│ └── libpeer.a # Main library +└── examples/ + └── generic/ + └── sample # Example application +``` diff --git a/CMakeLists.txt b/CMakeLists.txt index 83789d065..8872429ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ endif() project(peer) option(ENABLE_TESTS "Enable tests" OFF) -option(BUILD_SHARED_LIBS "Build shared libraries" ON) +option(BUILD_SHARED_LIBS "Build shared libraries" OFF) option(ADDRESS_SANITIZER "Build with AddressSanitizer." OFF) option(MEMORY_SANITIZER "Build with MemorySanitizer." OFF) option(THREAD_SANITIZER "Build with ThreadSanitizer." OFF) @@ -72,6 +72,7 @@ ExternalProject_Add(cjson SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/cJSON CMAKE_ARGS -DCMAKE_C_FLAGS="-fPIC" + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -DBUILD_SHARED_LIBS=off -DENABLE_CJSON_TEST=off -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/dist @@ -81,7 +82,8 @@ ExternalProject_Add(cjson ExternalProject_Add(mbedtls SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/mbedtls CMAKE_ARGS - -DCMAKE_C_FLAGS="-fPIC" + -DCMAKE_C_FLAGS=-fPIC + -DMBEDTLS_FATAL_WARNINGS=off -DENABLE_TESTING=off -DENABLE_PROGRAMS=off -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/dist @@ -104,7 +106,20 @@ ExternalProject_Add(usrsctp SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/usrsctp CMAKE_ARGS -DCMAKE_C_FLAGS="-fPIC" + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -Dsctp_build_programs=off -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/dist -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} ) + +# FAT library target (combines all static libraries into one) +set(FAT_LIB_NAME "libpeer_fat.a") +set(FAT_LIB_PATH "${CMAKE_BINARY_DIR}/dist/lib/${FAT_LIB_NAME}") + +add_custom_target(fat_library + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/scripts/create_fat_library.sh + ${CMAKE_BINARY_DIR} + ${FAT_LIB_PATH} + DEPENDS peer + COMMENT "Creating FAT library: ${FAT_LIB_NAME}" +) diff --git a/cmake/unity/CMakeLists.txt b/cmake/unity/CMakeLists.txt new file mode 100644 index 000000000..f17e0a7d5 --- /dev/null +++ b/cmake/unity/CMakeLists.txt @@ -0,0 +1,257 @@ +# Unity Plugin Build Configuration for libpeer +# Usage: +# cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE= -DUNITY_PLATFORM= +# cmake --build build --target unity_plugin + +cmake_minimum_required(VERSION 3.16) +project(libpeer_unity C) + +# Unity platform detection +set(UNITY_PLATFORM "" CACHE STRING "Target Unity platform: iOS, Android, macOS, Windows, Linux") + +# Validate platform +if(NOT UNITY_PLATFORM) + if(IOS) + set(UNITY_PLATFORM "iOS") + elseif(ANDROID) + set(UNITY_PLATFORM "Android") + elseif(APPLE) + set(UNITY_PLATFORM "macOS") + elseif(WIN32) + set(UNITY_PLATFORM "Windows") + elseif(UNIX) + set(UNITY_PLATFORM "Linux") + else() + message(FATAL_ERROR "UNITY_PLATFORM not specified and cannot be auto-detected") + endif() +endif() + +message(STATUS "Building libpeer Unity plugin for: ${UNITY_PLATFORM}") + +# Common settings +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# Optimization for release +set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3 -DNDEBUG") +set(CMAKE_C_FLAGS_MINSIZEREL "${CMAKE_C_FLAGS_MINSIZEREL} -Os -DNDEBUG") + +# Source directory (relative to repository root) +set(LIBPEER_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../..") +set(LIBPEER_SRC "${LIBPEER_ROOT}/src") +set(LIBPEER_INCLUDE "${LIBPEER_ROOT}/include") +set(LIBPEER_THIRD_PARTY "${LIBPEER_ROOT}/third_party") + +# Include third-party build helpers +include(${LIBPEER_THIRD_PARTY}/coreHTTP/httpFilePaths.cmake) +include(${LIBPEER_THIRD_PARTY}/coreMQTT/mqttFilePaths.cmake) + +# Collect source files +file(GLOB LIBPEER_SOURCES "${LIBPEER_SRC}/*.c") + +# Platform-specific configuration +if(UNITY_PLATFORM STREQUAL "iOS") + # iOS: Static library only + set(UNITY_LIB_TYPE STATIC) + set(UNITY_OUTPUT_NAME "peer") + set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0") + add_definitions(-DUNITY_IOS=1) + +elseif(UNITY_PLATFORM STREQUAL "Android") + # Android: Shared library + set(UNITY_LIB_TYPE SHARED) + set(UNITY_OUTPUT_NAME "peer") + add_definitions(-DUNITY_ANDROID=1) + add_definitions(-DANDROID=1) + # Enable crypto extension for mbedtls AESCE on arm64 + if(CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=armv8-a+crypto") + endif() + +elseif(UNITY_PLATFORM STREQUAL "macOS") + # macOS: Bundle (MODULE library) + set(UNITY_LIB_TYPE MODULE) + set(UNITY_OUTPUT_NAME "peer") + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15") + add_definitions(-DUNITY_STANDALONE_OSX=1) + +elseif(UNITY_PLATFORM STREQUAL "Windows") + # Windows: DLL + set(UNITY_LIB_TYPE SHARED) + set(UNITY_OUTPUT_NAME "peer") + add_definitions(-DUNITY_STANDALONE_WIN=1) + add_definitions(-DWIN32_LEAN_AND_MEAN) + +elseif(UNITY_PLATFORM STREQUAL "Linux") + # Linux: Shared library + set(UNITY_LIB_TYPE SHARED) + set(UNITY_OUTPUT_NAME "peer") + add_definitions(-DUNITY_STANDALONE_LINUX=1) + +else() + message(FATAL_ERROR "Unsupported UNITY_PLATFORM: ${UNITY_PLATFORM}") +endif() + +# Common definitions +add_definitions( + -DHTTP_DO_NOT_USE_CUSTOM_CONFIG + -DMQTT_DO_NOT_USE_CUSTOM_CONFIG +) + +# Dependencies output directory +set(DEPS_INSTALL_DIR "${CMAKE_BINARY_DIR}/deps") +include_directories(${DEPS_INSTALL_DIR}/include ${DEPS_INSTALL_DIR}/include/cjson) +link_directories(${DEPS_INSTALL_DIR}/lib) + +# Build third-party dependencies +include(ExternalProject) + +# Common ExternalProject arguments for cross-compilation +set(EXTERNAL_PROJECT_C_FLAGS "-fPIC") +if(CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") + set(EXTERNAL_PROJECT_C_FLAGS "${EXTERNAL_PROJECT_C_FLAGS} -march=armv8-a+crypto") +endif() + +set(EXTERNAL_PROJECT_CMAKE_ARGS + -DCMAKE_C_FLAGS=${EXTERNAL_PROJECT_C_FLAGS} + -DCMAKE_INSTALL_PREFIX=${DEPS_INSTALL_DIR} + -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} + -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} + -DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} + -DCMAKE_OSX_SYSROOT=${CMAKE_OSX_SYSROOT} + -DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME} + -DCMAKE_ANDROID_ARCH_ABI=${CMAKE_ANDROID_ARCH_ABI} + -DANDROID_ABI=${ANDROID_ABI} + -DANDROID_PLATFORM=${ANDROID_PLATFORM} + -DANDROID_STL=${ANDROID_STL} +) + +ExternalProject_Add(cjson_ext + SOURCE_DIR ${LIBPEER_THIRD_PARTY}/cJSON + CMAKE_ARGS + ${EXTERNAL_PROJECT_CMAKE_ARGS} + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + -DBUILD_SHARED_LIBS=OFF + -DENABLE_CJSON_TEST=OFF +) + +ExternalProject_Add(mbedtls_ext + SOURCE_DIR ${LIBPEER_THIRD_PARTY}/mbedtls + CMAKE_ARGS + ${EXTERNAL_PROJECT_CMAKE_ARGS} + -DMBEDTLS_FATAL_WARNINGS=OFF + -DENABLE_TESTING=OFF + -DENABLE_PROGRAMS=OFF +) + +# Enable DTLS-SRTP in mbedtls +file(READ ${LIBPEER_THIRD_PARTY}/mbedtls/include/mbedtls/mbedtls_config.h MBEDTLS_CONFIG) +string(REPLACE "//#define MBEDTLS_SSL_DTLS_SRTP" "#define MBEDTLS_SSL_DTLS_SRTP" MBEDTLS_CONFIG "${MBEDTLS_CONFIG}") +file(WRITE ${LIBPEER_THIRD_PARTY}/mbedtls/include/mbedtls/mbedtls_config.h "${MBEDTLS_CONFIG}") + +ExternalProject_Add(srtp2_ext + SOURCE_DIR ${LIBPEER_THIRD_PARTY}/libsrtp + CMAKE_ARGS + ${EXTERNAL_PROJECT_CMAKE_ARGS} + -DTEST_APPS=OFF +) + +ExternalProject_Add(usrsctp_ext + SOURCE_DIR ${LIBPEER_THIRD_PARTY}/usrsctp + CMAKE_ARGS + ${EXTERNAL_PROJECT_CMAKE_ARGS} + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + -Dsctp_build_programs=OFF +) + +# Create the Unity plugin library +add_library(unity_plugin ${UNITY_LIB_TYPE} + ${LIBPEER_SOURCES} + ${HTTP_SOURCES} + ${MQTT_SOURCES} + ${MQTT_SERIALIZER_SOURCES} +) + +set_target_properties(unity_plugin PROPERTIES + OUTPUT_NAME ${UNITY_OUTPUT_NAME} + PREFIX "lib" +) + +target_include_directories(unity_plugin PRIVATE + ${LIBPEER_INCLUDE} + ${LIBPEER_SRC} + ${HTTP_INCLUDE_PUBLIC_DIRS} + ${MQTT_INCLUDE_PUBLIC_DIRS} +) + +add_dependencies(unity_plugin cjson_ext mbedtls_ext srtp2_ext usrsctp_ext) + +# Link dependencies +set(DEP_LIBS srtp2 usrsctp mbedtls mbedcrypto mbedx509 cjson) + +if(UNITY_PLATFORM STREQUAL "iOS") + # iOS: Static library, dependencies linked later by Unity + # Create fat static library target + add_custom_target(unity_fat_library + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/output + COMMAND bash ${LIBPEER_ROOT}/scripts/create_fat_library.sh + ${CMAKE_BINARY_DIR} + ${CMAKE_BINARY_DIR}/output/libpeer.a + DEPENDS unity_plugin + COMMENT "Creating iOS fat static library" + ) + +elseif(UNITY_PLATFORM STREQUAL "Android") + target_link_libraries(unity_plugin PRIVATE + ${DEPS_INSTALL_DIR}/lib/libsrtp2.a + ${DEPS_INSTALL_DIR}/lib/libusrsctp.a + ${DEPS_INSTALL_DIR}/lib/libmbedtls.a + ${DEPS_INSTALL_DIR}/lib/libmbedcrypto.a + ${DEPS_INSTALL_DIR}/lib/libmbedx509.a + ${DEPS_INSTALL_DIR}/lib/libcjson.a + log + ) + +elseif(UNITY_PLATFORM STREQUAL "macOS") + # macOS bundle settings + set_target_properties(unity_plugin PROPERTIES + BUNDLE TRUE + BUNDLE_EXTENSION "bundle" + MACOSX_BUNDLE_GUI_IDENTIFIER "jp.co.mixi.libpeer" + ) + target_link_libraries(unity_plugin PRIVATE + ${DEPS_INSTALL_DIR}/lib/libsrtp2.a + ${DEPS_INSTALL_DIR}/lib/libusrsctp.a + ${DEPS_INSTALL_DIR}/lib/libmbedtls.a + ${DEPS_INSTALL_DIR}/lib/libmbedcrypto.a + ${DEPS_INSTALL_DIR}/lib/libmbedx509.a + ${DEPS_INSTALL_DIR}/lib/libcjson.a + "-framework Foundation" + "-framework Security" + ) + +else() + # Linux/Windows + target_link_libraries(unity_plugin PRIVATE + ${DEPS_INSTALL_DIR}/lib/libsrtp2.a + ${DEPS_INSTALL_DIR}/lib/libusrsctp.a + ${DEPS_INSTALL_DIR}/lib/libmbedtls.a + ${DEPS_INSTALL_DIR}/lib/libmbedcrypto.a + ${DEPS_INSTALL_DIR}/lib/libmbedx509.a + ${DEPS_INSTALL_DIR}/lib/libcjson.a + ) + if(UNIX) + target_link_libraries(unity_plugin PRIVATE pthread) + endif() +endif() + +# Installation target for UPM package +set(UPM_OUTPUT_DIR "${LIBPEER_ROOT}/upm/jp.co.mixi.libpeer/Plugins") + +install(TARGETS unity_plugin + LIBRARY DESTINATION ${UPM_OUTPUT_DIR}/${UNITY_PLATFORM} + ARCHIVE DESTINATION ${UPM_OUTPUT_DIR}/${UNITY_PLATFORM} + RUNTIME DESTINATION ${UPM_OUTPUT_DIR}/${UNITY_PLATFORM} + BUNDLE DESTINATION ${UPM_OUTPUT_DIR}/${UNITY_PLATFORM} +) diff --git a/examples/generic/main.c b/examples/generic/main.c index 8fc13ffdf..01f7da987 100644 --- a/examples/generic/main.c +++ b/examples/generic/main.c @@ -4,9 +4,12 @@ #include #include #include +#include +#include +#include +#include #include "peer.h" -#include "reader.h" int g_interrupted = 0; PeerConnection* g_pc = NULL; @@ -18,18 +21,21 @@ static void onconnectionstatechange(PeerConnectionState state, void* data) { } static void onopen(void* user_data) { + printf("on_open..\n"); } static void onclose(void* user_data) { + printf("on_close..\n"); } static void onmessage(char* msg, size_t len, void* user_data, uint16_t sid) { printf("on message: %d %.*s", sid, (int)len, msg); if (strncmp(msg, "ping", 4) == 0) { - printf(", send pong\n"); + printf(", send pong"); peer_connection_datachannel_send(g_pc, "pong", 4); } + printf("\n"); } static void signal_handler(int signal) { @@ -86,11 +92,10 @@ void parse_arguments(int argc, char* argv[], const char** url, const char** toke } int main(int argc, char* argv[]) { - uint64_t curr_time, video_time, audio_time; - uint8_t* buf = NULL; + uint64_t curr_time, dcmsg_time = 0; const char* url = NULL; const char* token = NULL; - int size; + int count = 0; pthread_t peer_singaling_thread; pthread_t peer_connection_thread; @@ -104,8 +109,8 @@ int main(int argc, char* argv[]) { {.urls = "stun:stun.l.google.com:19302"}, }, .datachannel = DATA_CHANNEL_STRING, - .video_codec = CODEC_H264, - .audio_codec = CODEC_PCMA}; + .video_codec = CODEC_NONE, + .audio_codec = CODEC_NONE}; printf("=========== Parsed Arguments ===========\n"); printf(" %-5s : %s\n", "URL", url); @@ -122,29 +127,16 @@ int main(int argc, char* argv[]) { pthread_create(&peer_connection_thread, NULL, peer_connection_task, NULL); pthread_create(&peer_singaling_thread, NULL, peer_singaling_task, NULL); - reader_init(); - while (!g_interrupted) { if (g_state == PEER_CONNECTION_COMPLETED) { curr_time = get_timestamp(); - // FPS 25 - if (curr_time - video_time > 40) { - video_time = curr_time; - if ((buf = reader_get_video_frame(&size)) != NULL) { - peer_connection_send_video(g_pc, buf, size); - // need to free the buffer - free(buf); - buf = NULL; - } - } - - if (curr_time - audio_time > 20) { - if ((buf = reader_get_audio_frame(&size)) != NULL) { - peer_connection_send_audio(g_pc, buf, size); - buf = NULL; - } - audio_time = curr_time; + // Message on Datachannel + if ((curr_time - dcmsg_time) > 1000) { + dcmsg_time = curr_time; + char msg[256] = { 0x00, }; + snprintf(msg, sizeof(msg) - 1, "datachannel message : %05d", count++); + peer_connection_datachannel_send(g_pc, msg, strlen(msg)); } } usleep(1000); @@ -153,8 +145,6 @@ int main(int argc, char* argv[]) { pthread_join(peer_singaling_thread, NULL); pthread_join(peer_connection_thread, NULL); - reader_deinit(); - peer_signaling_disconnect(); peer_connection_destroy(g_pc); peer_deinit(); diff --git a/examples/generic/reader.c b/examples/generic/reader.c deleted file mode 100644 index 85ef8a57e..000000000 --- a/examples/generic/reader.c +++ /dev/null @@ -1,176 +0,0 @@ -#include -#include -#include -#include -#include - -static int g_video_size = 0; -static int g_audio_size = 0; -static uint8_t* g_video_buf = NULL; -static uint8_t* g_audio_buf = NULL; -static uint8_t* g_pps_buf = NULL; -static uint8_t* g_sps_buf = NULL; -static const uint32_t nalu_start_4bytecode = 0x01000000; -static const uint32_t nalu_start_3bytecode = 0x010000; - -typedef enum H264_NALU_TYPE { - NALU_TYPE_SPS = 7, - NALU_TYPE_PPS = 8, - NALU_TYPE_IDR = 5, - NALU_TYPE_NON_IDR = 1, -} H264_NALU_TYPE; - -int reader_init() { - FILE* video_fp = NULL; - FILE* audio_fp = NULL; - char videofile[] = "test.264"; - char audiofile[] = "alaw08m.wav"; - - video_fp = fopen(videofile, "rb"); - - if (video_fp == NULL) { - printf("open file %s failed\n", videofile); - return -1; - } - - fseek(video_fp, 0, SEEK_END); - g_video_size = ftell(video_fp); - fseek(video_fp, 0, SEEK_SET); - g_video_buf = (uint8_t*)calloc(1, g_video_size); - fread(g_video_buf, g_video_size, 1, video_fp); - fclose(video_fp); - - audio_fp = fopen(audiofile, "rb"); - - if (audio_fp == NULL) { - printf("open file %s failed\n", audiofile); - return -1; - } - - fseek(audio_fp, 0, SEEK_END); - g_audio_size = ftell(audio_fp); - fseek(audio_fp, 0, SEEK_SET); - g_audio_buf = (uint8_t*)calloc(1, g_audio_size); - fread(g_audio_buf, 1, g_audio_size, audio_fp); - fclose(audio_fp); - - return 0; -} - -uint8_t* reader_h264_find_nalu(uint8_t* buf_start, uint8_t* buf_end) { - uint8_t* p = buf_start; - - while ((p + 3) < buf_end) { - if (memcmp(p, &nalu_start_4bytecode, 4) == 0) { - return p; - } else if (memcmp(p, &nalu_start_3bytecode, 3) == 0) { - return p; - } - p++; - } - - return buf_end; -} - -uint8_t* reader_get_video_frame(int* size) { - uint8_t* buf = NULL; - static int pps_size = 0; - static int sps_size = 0; - uint8_t* buf_end = g_video_buf + g_video_size; - - static uint8_t* pstart = NULL; - static uint8_t* pend = NULL; - size_t nalu_size; - - if (!pstart) - pstart = g_video_buf; - - pend = reader_h264_find_nalu(pstart + 2, buf_end); - - if (pend == buf_end) { - pstart = NULL; - return NULL; - } - - nalu_size = pend - pstart; - int start_code_offset = memcmp(pstart, &nalu_start_3bytecode, 3) == 0 ? 3 : 4; - H264_NALU_TYPE nalu_type = (H264_NALU_TYPE)(pstart[start_code_offset] & 0x1f); - - switch (nalu_type) { - case NALU_TYPE_SPS: - sps_size = nalu_size; - if (g_sps_buf != NULL) { - free(g_sps_buf); - g_sps_buf = NULL; - } - g_sps_buf = (uint8_t*)calloc(1, sps_size); - memcpy(g_sps_buf, pstart, sps_size); - break; - case NALU_TYPE_PPS: - pps_size = nalu_size; - if (g_pps_buf != NULL) { - free(g_pps_buf); - g_pps_buf = NULL; - } - g_pps_buf = (uint8_t*)calloc(1, pps_size); - memcpy(g_pps_buf, pstart, pps_size); - - break; - case NALU_TYPE_IDR: - *size = sps_size + pps_size + nalu_size; - buf = (uint8_t*)calloc(1, *size); - memcpy(buf, g_sps_buf, sps_size); - memcpy(buf + sps_size, g_pps_buf, pps_size); - memcpy(buf + sps_size + pps_size, pstart, nalu_size); - - break; - case NALU_TYPE_NON_IDR: - default: - *size = nalu_size; - buf = (uint8_t*)calloc(1, *size); - memcpy(buf, pstart, nalu_size); - - break; - } - - pstart = pend; - - return buf; -} - -uint8_t* reader_get_audio_frame(int* size) { - // sample-rate=8000 channels=1 format=S16LE duration=20ms alaw-size=160 - uint8_t* buf = NULL; - static int pos = 0; - *size = 160; - if ((pos + *size) > g_audio_size) { - pos = 0; - } - - buf = g_audio_buf + pos; - pos += *size; - - return buf; -} - -void reader_deinit() { - if (g_sps_buf != NULL) { - free(g_sps_buf); - g_sps_buf = NULL; - } - - if (g_pps_buf != NULL) { - free(g_pps_buf); - g_pps_buf = NULL; - } - - if (g_video_buf != NULL) { - free(g_video_buf); - g_video_buf = NULL; - } - - if (g_audio_buf != NULL) { - free(g_audio_buf); - g_audio_buf = NULL; - } -} diff --git a/examples/generic/reader.h b/examples/generic/reader.h deleted file mode 100644 index 629e49d03..000000000 --- a/examples/generic/reader.h +++ /dev/null @@ -1,12 +0,0 @@ -#include -#include -#include -#include - -int reader_init(); - -uint8_t* reader_get_video_frame(int* size); - -uint8_t* reader_get_audio_frame(int* size); - -void reader_deinit(); diff --git a/examples/hid-usb/.envrc b/examples/hid-usb/.envrc new file mode 100644 index 000000000..ce641a86c --- /dev/null +++ b/examples/hid-usb/.envrc @@ -0,0 +1,13 @@ +export PICO_TOOLCHAIN_PATH="$HOME/git/gcc-arm-none-eabi-10.3-2021.10/bin" +export PICO_SDK_PATH="$HOME/git/pico-sdk" +export PICO_EXAMPLES_PATH="$HOME/git/pico-examples" +export PICO_BOARD=pico2_w +export WIFI_SSID="mx-free24" +export WIFI_PASSWORD="ckuv7482" +export SIGNALING_URL="https://plant-tap-katrina-mall.trycloudflare.com/whip/00/00/00" +export SIGNALING_TOKEN="" + +export HID_W=430 +export HID_H=900 + + diff --git a/examples/hid-usb/.envrc.rp2040 b/examples/hid-usb/.envrc.rp2040 new file mode 100644 index 000000000..493567220 --- /dev/null +++ b/examples/hid-usb/.envrc.rp2040 @@ -0,0 +1,7 @@ +export PICO_SDK_PATH="$HOME/git/pico-sdk" +export PICO_EXAMPLES_PATH="$HOME/git/pico-examples" +export PICO_BOARD=pico_w +export WIFI_SSID="mx-free24" +export WIFI_PASSWORD="ckuv7482" +export SIGNALING_URL="https://targets-findarticles-trek-cooler.trycloudflare.com/whip/00/00/00" +export SIGNALING_TOKEN="" diff --git a/examples/hid-usb/.gitignore b/examples/hid-usb/.gitignore new file mode 100644 index 000000000..e2b085bc9 --- /dev/null +++ b/examples/hid-usb/.gitignore @@ -0,0 +1,2 @@ +_build +.patches_applied diff --git a/examples/hid-usb/README.md b/examples/hid-usb/README.md new file mode 100644 index 000000000..f0a91749b --- /dev/null +++ b/examples/hid-usb/README.md @@ -0,0 +1,152 @@ +# libpeer + RP2350 WebRTC HID-USB Mouse + +Firmware for Pico 2W (RP2350) that receives mouse commands over a WebRTC DataChannel and relays them to a host PC as a USB HID Mouse device, leveraging the dual-core architecture. + +## Architecture: Dual-Core Split + +``` +┌─────────────────────────────────────────────────────────┐ +│ Core 1 — CYW43 / WiFi / WebRTC │ +│ │ +│ cyw43_arch_init() │ +│ → WiFi (scan → auth → DHCP) │ +│ → DNS → Signaling Server (HTTP POST) │ +│ → ICE → DTLS handshake → SCTP → DataChannel open │ +│ → peer_connection_loop() + keepalive (15s ± jitter) │ +│ │ +│ onmessage() → queue_try_add(&g_queue, &entry) │ +└───────────────────────┬─────────────────────────────────┘ + │ multicore queue (lock-free) +┌───────────────────────▼─────────────────────────────────┐ +│ Core 0 — USB HID Mouse │ +│ │ +│ board_init() → tusb_init() │ +│ → tud_task() + hid_task() tight loop │ +│ │ +│ hid_task() → queue_try_remove(&g_queue, &itm) │ +│ → sscanf "op dx dy" → tud_hid_mouse_report() │ +└─────────────────────────────────────────────────────────┘ +``` + +### Why Dual-Core Separation + +- **CYW43 IRQ isolation**: Calling `cyw43_arch_init()` on Core1 binds the WiFi background worker IRQs to Core1, preventing interference with Core0's `USBCTRL_IRQ`. +- **USB requires a tight loop**: `tud_task()` will timeout the USB protocol if blocked. It cannot share a core with heavy WiFi/DTLS processing. + +## Critical: Deferred USB Initialization + +**This is the single most important design decision.** + +USB device initialization (`board_init()` → `tusb_init()`) on Core0 must be **deferred** until the WebRTC session on Core1 has fully completed (ICE → DTLS → USRSCTP → DataChannel open). + +```c +// main() — Core 0 +multicore_launch_core1(core1_entry); + +// Wait until DataChannel is ready (first queue entry arrives) +while (1) { + queue_entry_t itm; + if (queue_try_remove(&g_queue, &itm)) { break; } + sleep_ms(100); +} + +// Only NOW initialize USB +board_init(); +irq_set_priority(USBCTRL_IRQ, 0); +tusb_init(); +``` + +### Why This Matters + +Initializing CYW43 (WiFi) and TinyUSB simultaneously on the Pico 2W **exceeds the power budget**. During the DTLS handshake and USRSCTP negotiation, the CYW43 chip draws peak current. Bringing up the USB PHY at the same time causes: + +``` +TinyUSB PANIC: EP0 is already available +``` + +This panic indicates that endpoint 0 has entered an invalid hardware state. The root cause is the USB controller registers becoming unstable due to insufficient power. + +**Solution**: Defer Core0's USB initialization until Core1's DTLS + USRSCTP completes and the DataChannel opens — i.e., after the power peak has passed. The trigger is successfully dequeuing the first DataChannel message from the multicore queue. + +## DataChannel Protocol + +### Single command +```json +{"type": "mouse", "command": "0 5 -3"} +``` + +### Batch commands +```json +{"type": "mouse", "commands": ["0 5 -3", "0 2 1", "0 0 0"]} +``` + +Command format: `"op dx dy"` (space-separated) +- `op`: button state (0=none, 1=left, 2=right, 4=middle) +- `dx`: X-axis delta (-127 to 127) +- `dy`: Y-axis delta (-127 to 127) + +## Build + +```bash +export WIFI_SSID="your-ssid" +export WIFI_PASSWORD="your-password" +export SIGNALING_URL="http://your-server:8080/webrtc" +export SIGNALING_TOKEN="optional-token" + +cd cmake.rp2350 +mkdir -p build && cd build +cmake .. +make -j$(nproc) +``` + +Target board: `pico2_w` (RP2350) + +Output: `rp2350bm-hidusb.uf2` + +## USB Device Identity + +| Field | Value | +|-------------|----------------| +| VID | `0x413C` | +| PID | `0x301A` | +| Device | mx-mouse | +| Protocol | HID Boot Mouse | + +## Timing Sequence (typical) + +``` +[TIMING] Boot complete: ~2000 ms +[TIMING] WiFi scan start +[TIMING] WiFi auth/assoc done +[TIMING] DHCP bound +[TIMING] DNS resolved +[TIMING] Signaling server connect +[TIMING] ICE checking → connected → completed +[TIMING] DataChannel opened (SCTP ready) + ← Core0 USB initialization starts here → +[TIMING] First message TX / RX +``` + +## LED Feedback + +| Event | Pattern | +|-------------------|----------------------| +| WiFi connected | 100ms x 5 blinks | +| DataChannel TX | 100ms x 3 blinks | +| DataChannel RX | 20ms x 15 blinks | + +## Keepalive + +DataChannel keepalive messages are sent at 15-second intervals with ±20% jitter to prevent NAT table expiry from dropping the session. + +## Disconnect Recovery: Watchdog Reboot + +When the PeerConnection enters `FAILED` or `DISCONNECTED` state (e.g. WiFi drops, remote peer closes, DTLS timeout), the firmware triggers a full hardware reset via `watchdog_reboot()`. + +```c +case PEER_CONNECTION_FAILED: +case PEER_CONNECTION_DISCONNECTED: + watchdog_reboot(0, 0, 0); +``` + +A clean cold boot is chosen over in-place teardown/reconnect because CYW43, DTLS (mbedTLS), and USRSCTP all hold deeply nested global state that cannot be safely re-initialized without a full reset. The entire startup sequence — WiFi, WebRTC, deferred USB init — replays from scratch, guaranteeing a known-good state. diff --git a/examples/hid-usb/auto-test/common.py b/examples/hid-usb/auto-test/common.py new file mode 100644 index 000000000..ae7fea98e --- /dev/null +++ b/examples/hid-usb/auto-test/common.py @@ -0,0 +1,553 @@ +"""Monster Strike auto-test: shared utilities. + +USB HID mouse control via HTTP API. +Command format: "{op} {x} {y}" + op=0: move / mouse-up + op=1: mouse-down / drag + x, y: relative delta (HID units, NOT pixels) + +Note: HID delta ~100 moves the cursor roughly 1/4 of the screen. + Full screen ≈ 400 HID units (varies by OS mouse acceleration). +""" + +import os +import requests +import time +import math +import random + +# --------------- config --------------- +API_BASE = os.environ.get("SFU_API_BASE", "http://192.168.124.45:8888/api/message") +_device_id = "" # set by init(device_id) +MOVE_DELAY = 0.13 # inter-report delay (s) +CLICK_HOLD = 0.15 # mouse-down duration (s) +UI_WAIT = 1.0 # wait for UI transition (s) +SCAN_STEP = 10 # calibration scan step (px) +MAX_DELTA = 10 # max delta per-report (both dx, dy) + +# --------------- state --------------- +screen_w = 0 # screen width (HID units) +screen_h = 0 # screen height (HID units) +_cx = 0 # tracked cursor x (HID units) +_cy = 0 # tracked cursor y (HID units) + +RESET_SWEEP = 500 # HID units to guarantee reaching any corner + +# --------------- batch config (IPS/IDS evasion) --------------- +BATCH_MIN = 6 # min commands per batch +BATCH_MAX = 14 # max commands per batch +PAD_MIN = 0 # min random padding length +PAD_MAX = 32 # max random padding length + + +# ============================================================ +# Low-level API +# ============================================================ + +def init(device_id: str): + """Set device ID (must be called before any mouse operation).""" + global _device_id + _device_id = device_id + print(f"[common] device_id = {device_id}") + + +def _api_url() -> str: + return f"{API_BASE}/{_device_id}/00/00" + + +def send(op: int, x: int, y: int, delay: float = MOVE_DELAY): + """Send a single mouse report.""" + payload = {"type": "mouse", "command": f"{op} {x} {y}"} + requests.post(_api_url(), json=payload) + if delay > 0: + time.sleep(delay) + + +def _send_batch(commands: list): + """Send a batch of commands in one packet with random padding.""" + pad = "x" * random.randint(PAD_MIN, PAD_MAX) + payload = {"type": "mouse", "commands": commands, "p": pad} + requests.post(_api_url(), json=payload) + + +def _chunked_move(op: int, dx: int, dy: int): + """Move in MAX_DELTA chunks, sent as random-sized batches.""" + global _cx, _cy + print(f"_chunked_move: {dx}, {dy}\n") + + # Collect all chunks first + chunks = [] + while dx != 0 or dy != 0: + sx = max(-MAX_DELTA, min(MAX_DELTA, dx)) + sy = max(-MAX_DELTA, min(MAX_DELTA, dy)) + chunks.append(f"{op} {sx} {sy}") + dx -= sx + dy -= sy + _cx += sx + _cy += sy + + # Send in random-sized batches + i = 0 + while i < len(chunks): + n = random.randint(BATCH_MIN, BATCH_MAX) + batch = chunks[i:i + n] + _send_batch(batch) + i += len(batch) + time.sleep(MOVE_DELAY * len(batch)) + + + + +# ============================================================ +# High-level operations +# ============================================================ + +def reset_origin(): + """Cursor to top-left (0,0) via large negative deltas.""" + global _cx, _cy + steps = RESET_SWEEP // 100 + 1 # e.g. 500/100+1 = 6 + cmds = [f"0 -100 -100" for _ in range(steps)] + _send_batch(cmds) + time.sleep(0.01 * steps) + _cx = 0 + _cy = 0 + + +def reset_origin_visual(): + """Cursor to top-left with visual feedback. + + First sweep to bottom-right so the user can see the cursor moving, + then sweep far left/up to guarantee reaching (0,0). + """ + global _cx, _cy + step = 20 + n_left = RESET_SWEEP // step + 1 # → top-left + print(f"[reset_origin_visual] → top-left origin ({n_left} steps) ...") + + # Collect all commands, send in random batches + cmds = [f"0 {-step} {-step}" for _ in range(n_left)] + i = 0 + while i < len(cmds): + n = random.randint(BATCH_MIN, BATCH_MAX) + batch = cmds[i:i + n] + _send_batch(batch) + i += len(batch) + time.sleep(MOVE_DELAY * len(batch)) + + _cx = 0 + _cy = 0 + print("[reset_origin_visual] done") + + +def move_to(x: int, y: int): + """Move cursor to absolute position (from origin).""" + print(f"move_to : {_cx} , {x}, {_cy} , {y}") + _chunked_move(0, x - _cx, y - _cy) + + +def click(x: int, y: int, repeat: int = 1, interval: float = 0.3): + """Tap at absolute position. + + repeat: number of clicks + interval: delay between clicks (s) + """ + print(f"click {x} {y} (x{repeat})") + move_to(x, y) + for i in range(repeat): + send(1, 0, 0, delay=CLICK_HOLD) # mouse-down + send(0, 0, 0, delay=0.1) # mouse-up + if i < repeat - 1: + time.sleep(interval) + + +def click_pct(rx: float, ry: float, repeat: int = 1, interval: float = 0.3): + """Tap at relative position (0.0-1.0 of screen).""" + click(int(screen_w * rx), int(screen_h * ry), repeat=repeat, interval=interval) + + +def long_press(x: int, y: int, duration: float = 1.0): + """Long press at absolute position.""" + move_to(x, y) + send(1, 0, 0, delay=duration) + send(0, 0, 0, delay=0.1) + + +def drag(x1, y1, x2, y2, steps: int = 20): + """Drag from (x1,y1) to (x2,y2) with smooth interpolation.""" + move_to(x1, y1) + send(1, 0, 0, delay=0.1) # mouse-down + + # Collect intermediate drag commands + dx = x2 - x1 + dy = y2 - y1 + cmds = [] + for i in range(1, steps + 1): + sx = (dx * i // steps) - (dx * (i - 1) // steps) + sy = (dy * i // steps) - (dy * (i - 1) // steps) + cmds.append(f"1 {sx} {sy}") + + # Send in random batches + i = 0 + while i < len(cmds): + n = random.randint(BATCH_MIN, BATCH_MAX) + batch = cmds[i:i + n] + _send_batch(batch) + i += len(batch) + time.sleep(0.02 * len(batch)) + + global _cx, _cy + _cx = x2 + _cy = y2 + time.sleep(0.1) + send(0, 0, 0, delay=0.1) # mouse-up + + +def wait(sec: float): + """Sleep with log.""" + print(f" wait {sec:.1f}s") + time.sleep(sec) + + +# ============================================================ +# Screen capture / CV (stub -- implement per environment) +# ============================================================ + +def capture_screen(): + """Capture current screen. + Returns: numpy ndarray (BGR) or None. + TODO: implement via WebRTC video frame, adb screencap, etc. + """ + return None + + +def detect_hamburger_menu(img) -> bool: + """Return True if hamburger-menu overlay is visible. + TODO: implement with cv2.matchTemplate or similar. + """ + if img is None: + return False + # Example: + # import cv2, numpy as np + # tmpl = cv2.imread("templates/hamburger_menu.png") + # res = cv2.matchTemplate(img, tmpl, cv2.TM_CCOEFF_NORMED) + # return res.max() > 0.8 + return False + + +def detect_home_screen(img) -> bool: + """Return True if Monster Strike home screen is visible.""" + if img is None: + return False + return False + + +# ============================================================ +# Calibration +# ============================================================ + +def calibrate(manual_size: tuple = None): + """Detect screen size in HID units. + + If manual_size is given as (w, h) in HID units, skip auto-detection. + Otherwise: scan from origin with step+click until hamburger + menu is detected by CV. + """ + global screen_w, screen_h, _cx, _cy + + if manual_size: + screen_w, screen_h = manual_size + print(f"[calibrate] manual: {screen_w}x{screen_h}") + reset_origin_visual() + return True + + print("[calibrate] reset to origin ...") + reset_origin_visual() + + print(f"[calibrate] scanning (step={SCAN_STEP}) ...") + max_steps = 500 # safety limit + for step in range(1, max_steps + 1): + send(0, SCAN_STEP, SCAN_STEP) + _cx += SCAN_STEP + _cy += SCAN_STEP + + # click at current position + send(1, 0, 0, delay=CLICK_HOLD) + send(0, 0, 0, delay=0.3) + + img = capture_screen() + if detect_hamburger_menu(img): + # hamburger icon is near bottom-right corner + screen_w = int(_cx / 0.95) + screen_h = int(_cy / 0.97) + print(f"[calibrate] detected: {screen_w}x{screen_h}") + + # close menu + reset_origin() + click(screen_w // 2, screen_h // 4) + wait(1.0) + return True + + if step % 50 == 0: + print(f" step {step} pos=({_cx},{_cy})") + + print("[calibrate] FAILED") + return False + + +# ============================================================ +# Actions (callable from CLI) +# ============================================================ + +ACTIONS = {} + + +def action(name): + """Decorator to register a CLI action.""" + def _reg(fn): + ACTIONS[name] = fn + return fn + return _reg + + +@action("calibrate") +def act_calibrate(args): + """Run calibration. [hid_w hid_h]""" + manual = None + if len(args) == 2: + manual = (int(args[0]), int(args[1])) + calibrate(manual_size=manual) + + +@action("quest_bt_click") +def act_quest_bt_click(args): + """Click Quest button. """ + if len(args) < 2: + print("quest_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap クエスト (0.50, 0.85) x5") + click_pct(0.50, 0.85, repeat=1) + + +@action("normal_bt_click") +def act_normal_bt_click(args): + """Click ノーマル button. """ + if len(args) < 2: + print("normal_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap ノーマル (0.50, 0.50)") + click_pct(0.50, 0.50, repeat=1) + + +@action("normal_ikusei_bt_click") +def act_normal_ikusei_bt_click(args): + """Click ノーマル育成 button. """ + if len(args) < 2: + print("normal_ikusei_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap ノーマル育成 (0.27, 0.72)") + click_pct(0.27, 0.72, repeat=1) + + +@action("shojin_bt_click") +def act_shojin_bt_click(args): + """Scroll down to bottom & click 初陣. """ + if len(args) < 2: + print("shojin_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + + # scroll down to bottom + for s in range(1, 10): + reset_origin() + print(f"[action] scroll down.. {s}") + x1 = int(screen_w * 0.50) + y1 = int(screen_h * 0.90) + x2 = int(screen_w * 0.50) + y2 = int(screen_h * 0.30) + drag(x1, y1, x2, y2) + wait(1.0) + + # tap 初陣 + reset_origin() + print("[action] tap 初陣 (0.50, 0.90)") + click_pct(0.50, 0.90, repeat=1) + + +@action("karyu_bt_click") +def act_karyu_bt_click(args): + """Click 火竜の試練 button. """ + if len(args) < 2: + print("karyu_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap 火竜の試練 (0.50, 0.60)") + click_pct(0.50, 0.60, repeat=1) + + +@action("solo_bt_click") +def act_solo_bt_click(args): + """Click ソロ button. """ + if len(args) < 2: + print("solo_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap ソロ (0.25, 0.60)") + click_pct(0.25, 0.60, repeat=1) + + +@action("helper_select") +def act_helper_select(args): + """Scroll to みんなのクリアモンスター & select. """ + if len(args) < 2: + print("helper_select requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + + for step in range(1, 10): + reset_origin() + print(f"[action] scroll up, reset.. {step}") + x1 = int(screen_w * 0.50) + y1 = int(screen_h * 0.30) + x2 = int(screen_w * 0.50) + y2 = int(screen_h * 0.90) + drag(x1, y1, x2, y2) + wait(1.0) + + reset_origin() + + # scroll down: drag (0.5, 0.5) → (0.5, 0.2) + x1 = int(screen_w * 0.50) + y1 = int(screen_h * 0.60) + x2 = int(screen_w * 0.50) + y2 = int(screen_h * 0.20) + print(f"[action] scroll down: drag ({x1},{y1}) → ({x2},{y2})") + drag(x1, y1, x2, y2) + wait(1.0) + # select helper + reset_origin() + print("[action] tap helper (0.50, 0.60)") + click_pct(0.50, 0.60, repeat=1) + + +@action("shutsugeki_bt_click") +def act_shutsugeki_bt_click(args): + """Click 出撃 button (deck screen). """ + if len(args) < 2: + print("shutsugeki_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap 出撃 (0.50, 0.70)") + click_pct(0.50, 0.70, repeat=1) + + +@action("play_turn") +def act_play_turn(args): + """Play 1 turn (random flick from center). """ + if len(args) < 2: + print("play_turn requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + + # random angle: 30deg increments (0, 30, 60, ... 330) + angle_deg = random.choice(range(0, 360, 30)) + angle_rad = math.radians(angle_deg) + # random strength: 100-200 HID units + strength = random.randint(100, 200) + # random hold time: 2-4 seconds + hold_sec = random.uniform(2.0, 4.0) + + dx = int(strength * math.cos(angle_rad)) + dy = int(strength * math.sin(angle_rad)) + + cx = int(screen_w * 0.50) + cy = int(screen_h * 0.50) + + print(f"[action] play_turn: angle={angle_deg}deg strength={strength} " + f"hold={hold_sec:.1f}s drag ({cx},{cy})→({cx+dx},{cy+dy})") + + # move to center + move_to(cx, cy) + # mouse down + send(1, 0, 0, delay=0.1) + # drag to target + _chunked_move(1, dx, dy) + # hold (aiming) + print(f" holding {hold_sec:.1f}s ...") + time.sleep(hold_sec) + # release + send(0, 0, 0, delay=0.1) + print("[action] released") + + +@action("clear_ok") +def act_clear_ok(args): + """Click OK on quest clear dialog. """ + if len(args) < 2: + print("clear_ok requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap OK (0.50, 0.65)") + click_pct(0.50, 0.65, repeat=1) + + +@action("special_reward") +def act_special_reward(args): + """Click special reward (center). """ + if len(args) < 2: + print("special_reward requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap center (0.50, 0.50)") + click_pct(0.50, 0.50, repeat=1) + + +@action("reward_next") +def act_reward_next(args): + """Click reward page 2. """ + if len(args) < 2: + print("reward_next requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap reward next (0.50, 1.1)") + click_pct(0.50, 1.1, repeat=1) + + +if __name__ == "__main__": + import sys + + usage = ( + "Usage: python common.py [args...]\n" + "\n" + "Actions:\n" + ) + for name, fn in ACTIONS.items(): + usage += f" {name:20s} {fn.__doc__ or ''}\n" + + if len(sys.argv) < 3: + print(usage) + sys.exit(1) + + dev = sys.argv[1] + cmd = sys.argv[2] + rest = sys.argv[3:] + + if cmd not in ACTIONS: + print(f"Unknown action: {cmd}\n") + print(usage) + sys.exit(1) + + init(dev) + ACTIONS[cmd](rest) diff --git a/examples/hid-usb/cmake.rp2040/CMakeLists.txt b/examples/hid-usb/cmake.rp2040/CMakeLists.txt new file mode 100644 index 000000000..8e52ef1f4 --- /dev/null +++ b/examples/hid-usb/cmake.rp2040/CMakeLists.txt @@ -0,0 +1,241 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 3.12) + +SET(PRJ rp2040bm-hidusb) +SET(PICO_BOARD pico_w) +SET(HOME $ENV{HOME}) +SET(PICO_SDK_FETCH_FROM_GIT on) +INCLUDE(pico_sdk_import.cmake) + +PROJECT(${PRJ} C CXX ASM) +SET(CMAKE_C_STANDARD 11) +SET(CMAKE_CXX_STANDARD 17) + +# pico_sdk_init must be called early, right after project() +pico_sdk_init() +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g") + +# Include stdint.h/stddef.h for C/CXX only (not for assembler) +add_compile_options( + "$<$:-includestdint.h>" + "$<$:-includestddef.h>" +) + +# Fix for pico-sdk runtime.c missing CLOCKS_PER_SEC +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCLOCKS_PER_SEC=1000000") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCLOCKS_PER_SEC=1000000") + +# Configuration from environment variables +message(STATUS "=== RP2040 Build Configuration ===") +if(DEFINED ENV{SIGNALING_URL}) + message(STATUS "SIGNALING_URL: $ENV{SIGNALING_URL}") + add_definitions(-DSIGNALING_URL="$ENV{SIGNALING_URL}") +else() + message(STATUS "SIGNALING_URL: (not set, using default)") +endif() +if(DEFINED ENV{SIGNALING_TOKEN}) + message(STATUS "SIGNALING_TOKEN: (set)") + add_definitions(-DSIGNALING_TOKEN="$ENV{SIGNALING_TOKEN}") +else() + message(STATUS "SIGNALING_TOKEN: (not set)") +endif() +if(DEFINED ENV{WIFI_SSID}) + message(STATUS "WIFI_SSID: $ENV{WIFI_SSID}") + add_definitions(-DWIFI_SSID="$ENV{WIFI_SSID}") +else() + message(STATUS "WIFI_SSID: (not set, using default)") +endif() +if(DEFINED ENV{WIFI_PASSWORD}) + message(STATUS "WIFI_PASSWORD: (set)") + add_definitions(-DWIFI_PASSWORD="$ENV{WIFI_PASSWORD}") +else() + message(STATUS "WIFI_PASSWORD: (not set)") +endif() +message(STATUS "==================================") + +# libsrtp: use standard integer types +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_STDINT_H -DHAVE_UINT8_T -DHAVE_UINT16_T -DHAVE_UINT32_T -DHAVE_INT32_T -DHAVE_UINT64_T") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_STDINT_H -DHAVE_UINT8_T -DHAVE_UINT16_T -DHAVE_UINT32_T -DHAVE_INT32_T -DHAVE_UINT64_T") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_NETINET_IN_H") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_NETINET_IN_H") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Dbe32_to_cpu=lwip_ntohl") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Dbe32_to_cpu=lwip_ntohl") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Dbe64_to_cpu=__builtin_bswap64") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Dbe64_to_cpu=__builtin_bswap64") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_STDLIB_H") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_STDLIB_H") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DPACKAGE_STRING=\\\"libsrtp2\\\" -DPACKAGE_VERSION=\\\"2.0.0\\\"") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPACKAGE_STRING=\\\"libsrtp2\\\" -DPACKAGE_VERSION=\\\"2.0.0\\\"") + +IF (PICO_SDK_VERSION_STRING VERSION_LESS "1.3.0") + message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.3.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}") +ENDIF() +SET(CMAKE_INCLUDE_CURRENT_DIR ON) + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__RP2040_BM__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__RP2040_BM__") + +# Note: CONFIG_USE_LWIP=1 causes header conflicts with our socket stubs +# Instead, ports.c checks __RP2040_BM__ to use lwIP netif_list directly +# SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCONFIG_USE_LWIP=1") +# SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCONFIG_USE_LWIP=1") + +# Use mbedtls 2.x API (Pico SDK uses mbedtls 2.x) +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCONFIG_MBEDTLS_2_X=1") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCONFIG_MBEDTLS_2_X=1") + +# libsrtp: use mbedtls for AES and define CPU type +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMBEDTLS") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMBEDTLS") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCPU_RISC") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCPU_RISC") + +# RP2040 is little-endian ARM Cortex-M0+ +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__LITTLE_ENDIAN=1234 -D__BIG_ENDIAN=4321 -D__BYTE_ORDER=__LITTLE_ENDIAN") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__LITTLE_ENDIAN=1234 -D__BIG_ENDIAN=4321 -D__BYTE_ORDER=__LITTLE_ENDIAN") + +# usrsctp defines +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSCTP_SIMPLE_ALLOCATOR -DSCTP_PROCESS_LEVEL_LOCKS -D__Userspace__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSCTP_SIMPLE_ALLOCATOR -DSCTP_PROCESS_LEVEL_LOCKS -D__Userspace__") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DERESTART=85") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DERESTART=85") +# lwIP sockaddr has sa_len field +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_SCONN_LEN") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_SCONN_LEN") + + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHTTP_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHTTP_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDMQTT_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMQTT_DO_NOT_USE_CUSTOM_CONFIG") + + +IF (OLD_SRM) + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__OLD_SRM__") + MESSAGE(">> OLD SRM mode") +ELSE() + MESSAGE(">> Stages Mode") +ENDIF() + + +# SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__DRIVER_INFO__ -D__DRIVER_DEBUG__") +# SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__DRIVER_INFO__ -D__DRIVER_DEBUG__") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__DRIVER_INFO__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__DRIVER_INFO__") + +INCLUDE_DIRECTORIES(".") + + +INCLUDE(${CMAKE_CURRENT_LIST_DIR}/../../../third_party/coreHTTP/httpFilePaths.cmake) +INCLUDE(${CMAKE_CURRENT_LIST_DIR}/../../../third_party/coreMQTT/mqttFilePaths.cmake) +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_LIST_DIR}/ + ${CMAKE_CURRENT_LIST_DIR}/inc/ + ${CMAKE_CURRENT_LIST_DIR}/../inc/ + ${CMAKE_BINARY_DIR}/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/libsrtp/include/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/libsrtp/crypto/include/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/cJSON/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/usrsctp/usrsctplib/ + ${CMAKE_CURRENT_LIST_DIR}/../../../src/ + ${HTTP_INCLUDE_PUBLIC_DIRS} + ${MQTT_INCLUDE_PUBLIC_DIRS} +) + +FILE(GLOB C_SOURCE_FILES ../../../src/*.c) +# Build libsrtp +file(GLOB LIBSRC + ../../../third_party/libsrtp/srtp/srtp.c + ../../../third_party/libsrtp/crypto/cipher/cipher.c + ../../../third_party/libsrtp/crypto/cipher/null_cipher.c + ../../../third_party/libsrtp/crypto/cipher/aes.c + ../../../third_party/libsrtp/crypto/cipher/aes_icm.c + ../../../third_party/libsrtp/crypto/cipher/cipher_test_cases.c + ../../../third_party/libsrtp/crypto/hash/auth.c + ../../../third_party/libsrtp/crypto/hash/null_auth.c + ../../../third_party/libsrtp/crypto/hash/hmac.c + ../../../third_party/libsrtp/crypto/hash/sha1.c + ../../../third_party/libsrtp/crypto/hash/auth_test_cases.c + ../../../third_party/libsrtp/crypto/kernel/alloc.c + ../../../third_party/libsrtp/crypto/kernel/crypto_kernel.c + ../../../third_party/libsrtp/crypto/kernel/err.c + ../../../third_party/libsrtp/crypto/kernel/key.c + ../../../third_party/libsrtp/crypto/math/datatypes.c + ../../../third_party/libsrtp/crypto/math/stat.c + ../../../third_party/libsrtp/crypto/replay/rdb.c + ../../../third_party/libsrtp/crypto/replay/rdbx.c + ../../../third_party/libsrtp/crypto/replay/ut_sim.c + # cJSON + ../../../third_party/cJSON/cJSON.c + # usrsctp (nothreads mode - no user_recv_thread.c) + ../../../third_party/usrsctp/usrsctplib/user_environment.c + ../../../third_party/usrsctp/usrsctplib/user_mbuf.c + ../../../third_party/usrsctp/usrsctplib/user_socket.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_asconf.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_auth.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_bsd_addr.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_callout.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_cc_functions.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_crc32.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_indata.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_input.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_output.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_pcb.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_peeloff.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_sha1.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_ss_functions.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_sysctl.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_timer.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_userspace.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_usrreq.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctputil.c +) + +ADD_EXECUTABLE( + ${PRJ} + ${C_SOURCE_FILES} + ${LIBSRC} + ${HTTP_SOURCES} + ${CMAKE_CURRENT_LIST_DIR}/../src/atomic_compat.c + ${CMAKE_CURRENT_LIST_DIR}/../src/inet_compat.c + ${CMAKE_CURRENT_LIST_DIR}/../main.c +) +FILE(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/srtp2/) +FILE(COPY ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/libsrtp/include/srtp.h + DESTINATION ${CMAKE_BINARY_DIR}/srtp2/) + +PROJECT(${PRJ} VERSION 1.0.0) + +TARGET_LINK_LIBRARIES( + ${PRJ} + pico_stdlib + pico_multicore + hardware_spi + hardware_i2c + tinyusb_host + tinyusb_device + tinyusb_board + pico_cyw43_arch_lwip_threadsafe_background + # pico_cyw43_arch_lwip_poll + pico_mbedtls +) + +pico_enable_stdio_usb(${PRJ} 0) +pico_enable_stdio_uart(${PRJ} 1) + +# Explicit UART configuration for RP2350 +target_compile_definitions(${PRJ} PRIVATE + PICO_DEFAULT_UART=0 + PICO_DEFAULT_UART_TX_PIN=0 + PICO_DEFAULT_UART_RX_PIN=1 + PICO_DEFAULT_UART_BAUD_RATE=115200 +) + +pico_add_extra_outputs(${PRJ}) + +ADD_COMPILE_OPTIONS( + -Wall + -Wno-format # int != int32_t as far as the compiler is concerned because gcc has int32_t as long int + -Wno-unused-function # we have some for the docs that aren't called + -Wno-maybe-uninitialized +) diff --git a/examples/hid-usb/cmake.rp2040/pico_sdk_import.cmake b/examples/hid-usb/cmake.rp2040/pico_sdk_import.cmake new file mode 100644 index 000000000..65f8a6f7d --- /dev/null +++ b/examples/hid-usb/cmake.rp2040/pico_sdk_import.cmake @@ -0,0 +1,73 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + GIT_SUBMODULES_RECURSE FALSE + ) + else () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + ) + endif () + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/examples/hid-usb/cmake.rp2350/CMakeLists.txt b/examples/hid-usb/cmake.rp2350/CMakeLists.txt new file mode 100644 index 000000000..b3ee11320 --- /dev/null +++ b/examples/hid-usb/cmake.rp2350/CMakeLists.txt @@ -0,0 +1,271 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 3.12) + +# Apply patches to third-party libraries (only once) +set(PATCH_MARKER "${CMAKE_CURRENT_LIST_DIR}/.patches_applied") +if(NOT EXISTS ${PATCH_MARKER}) + message(STATUS "Applying patches to third-party libraries...") + execute_process( + COMMAND patch -p1 --forward -i ${CMAKE_CURRENT_LIST_DIR}/../patch/libsrtp.patch + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/libsrtp + RESULT_VARIABLE LIBSRTP_PATCH_RESULT + ) + execute_process( + COMMAND patch -p1 --forward -i ${CMAKE_CURRENT_LIST_DIR}/../patch/mbedtls.patch + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/mbedtls + RESULT_VARIABLE MBEDTLS_PATCH_RESULT + ) + execute_process( + COMMAND patch -p1 --forward -i ${CMAKE_CURRENT_LIST_DIR}/../patch/usrsctp.patch + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/usrsctp + RESULT_VARIABLE USRSCTP_PATCH_RESULT + ) + file(WRITE ${PATCH_MARKER} "Patches applied at ${CMAKE_CURRENT_LIST_DIR}") + message(STATUS "Patches applied successfully") +endif() + +SET(PRJ rp2350bm-hidusb) +SET(PICO_BOARD pico2_w) +SET(HOME $ENV{HOME}) +SET(PICO_SDK_FETCH_FROM_GIT on) +INCLUDE(pico_sdk_import.cmake) + +PROJECT(${PRJ} C CXX ASM) +SET(CMAKE_C_STANDARD 11) +SET(CMAKE_CXX_STANDARD 17) + +# pico_sdk_init must be called early, right after project() +pico_sdk_init() +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g") + +# Include stdint.h/stddef.h for C/CXX only (not for assembler) +add_compile_options( + "$<$:-includestdint.h>" + "$<$:-includestddef.h>" +) + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DLOG_LEVEL=LEVEL_ERROR") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DLOG_LEVEL=LEVEL_ERROR") + + +# Fix for pico-sdk runtime.c missing CLOCKS_PER_SEC +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCLOCKS_PER_SEC=1000000") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCLOCKS_PER_SEC=1000000") + +# Configuration from environment variables +message(STATUS "=== RP2040 Build Configuration ===") +if(DEFINED ENV{SIGNALING_URL}) + message(STATUS "SIGNALING_URL: $ENV{SIGNALING_URL}") + add_definitions(-DSIGNALING_URL="$ENV{SIGNALING_URL}") +else() + message(STATUS "SIGNALING_URL: (not set, using default)") +endif() +if(DEFINED ENV{SIGNALING_TOKEN}) + message(STATUS "SIGNALING_TOKEN: (set)") + add_definitions(-DSIGNALING_TOKEN="$ENV{SIGNALING_TOKEN}") +else() + message(STATUS "SIGNALING_TOKEN: (not set)") +endif() +if(DEFINED ENV{WIFI_SSID}) + message(STATUS "WIFI_SSID: $ENV{WIFI_SSID}") + add_definitions(-DWIFI_SSID="$ENV{WIFI_SSID}") +else() + message(STATUS "WIFI_SSID: (not set, using default)") +endif() +if(DEFINED ENV{WIFI_PASSWORD}) + message(STATUS "WIFI_PASSWORD: (set)") + add_definitions(-DWIFI_PASSWORD="$ENV{WIFI_PASSWORD}") +else() + message(STATUS "WIFI_PASSWORD: (not set)") +endif() +message(STATUS "==================================") + +# libsrtp: use standard integer types +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_STDINT_H -DHAVE_UINT8_T -DHAVE_UINT16_T -DHAVE_UINT32_T -DHAVE_INT32_T -DHAVE_UINT64_T") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_STDINT_H -DHAVE_UINT8_T -DHAVE_UINT16_T -DHAVE_UINT32_T -DHAVE_INT32_T -DHAVE_UINT64_T") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_NETINET_IN_H") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_NETINET_IN_H") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Dbe32_to_cpu=lwip_ntohl") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Dbe32_to_cpu=lwip_ntohl") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Dbe64_to_cpu=__builtin_bswap64") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Dbe64_to_cpu=__builtin_bswap64") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_STDLIB_H") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_STDLIB_H") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DPACKAGE_STRING=\\\"libsrtp2\\\" -DPACKAGE_VERSION=\\\"2.0.0\\\"") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPACKAGE_STRING=\\\"libsrtp2\\\" -DPACKAGE_VERSION=\\\"2.0.0\\\"") + +IF (PICO_SDK_VERSION_STRING VERSION_LESS "1.3.0") + message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.3.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}") +ENDIF() +SET(CMAKE_INCLUDE_CURRENT_DIR ON) + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__RP2040_BM__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__RP2040_BM__") + +# Note: CONFIG_USE_LWIP=1 causes header conflicts with our socket stubs +# Instead, ports.c checks __RP2040_BM__ to use lwIP netif_list directly +# SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCONFIG_USE_LWIP=1") +# SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCONFIG_USE_LWIP=1") + +# libsrtp: use mbedtls for AES and define CPU type +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMBEDTLS") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMBEDTLS") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCPU_RISC") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCPU_RISC") + +# RP2040 is little-endian ARM Cortex-M0+ +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__LITTLE_ENDIAN=1234 -D__BIG_ENDIAN=4321 -D__BYTE_ORDER=__LITTLE_ENDIAN") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__LITTLE_ENDIAN=1234 -D__BIG_ENDIAN=4321 -D__BYTE_ORDER=__LITTLE_ENDIAN") + +# usrsctp defines +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSCTP_SIMPLE_ALLOCATOR -DSCTP_PROCESS_LEVEL_LOCKS -D__Userspace__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSCTP_SIMPLE_ALLOCATOR -DSCTP_PROCESS_LEVEL_LOCKS -D__Userspace__") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DERESTART=85") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DERESTART=85") +# lwIP sockaddr has sa_len field +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_SCONN_LEN") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_SCONN_LEN") + + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHTTP_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHTTP_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDMQTT_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMQTT_DO_NOT_USE_CUSTOM_CONFIG") + + +IF (OLD_SRM) + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__OLD_SRM__") + MESSAGE(">> OLD SRM mode") +ELSE() + MESSAGE(">> Stages Mode") +ENDIF() + + +# SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__DRIVER_INFO__ -D__DRIVER_DEBUG__") +# SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__DRIVER_INFO__ -D__DRIVER_DEBUG__") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__DRIVER_INFO__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__DRIVER_INFO__") + +INCLUDE_DIRECTORIES(".") + + +INCLUDE(${CMAKE_CURRENT_LIST_DIR}/../../../third_party/coreHTTP/httpFilePaths.cmake) +INCLUDE(${CMAKE_CURRENT_LIST_DIR}/../../../third_party/coreMQTT/mqttFilePaths.cmake) +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_LIST_DIR}/ + ${CMAKE_CURRENT_LIST_DIR}/inc/ + ${CMAKE_CURRENT_LIST_DIR}/../inc/ + ${CMAKE_BINARY_DIR}/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/libsrtp/include/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/libsrtp/crypto/include/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/cJSON/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/usrsctp/usrsctplib/ + ${CMAKE_CURRENT_LIST_DIR}/../../../src/ + ${HTTP_INCLUDE_PUBLIC_DIRS} + ${MQTT_INCLUDE_PUBLIC_DIRS} +) + +FILE(GLOB C_SOURCE_FILES ../../../src/*.c) +# Build libsrtp +file(GLOB LIBSRC + ../../../third_party/libsrtp/srtp/srtp.c + ../../../third_party/libsrtp/crypto/cipher/cipher.c + ../../../third_party/libsrtp/crypto/cipher/null_cipher.c + ../../../third_party/libsrtp/crypto/cipher/aes.c + ../../../third_party/libsrtp/crypto/cipher/aes_icm.c + ../../../third_party/libsrtp/crypto/cipher/cipher_test_cases.c + ../../../third_party/libsrtp/crypto/hash/auth.c + ../../../third_party/libsrtp/crypto/hash/null_auth.c + ../../../third_party/libsrtp/crypto/hash/hmac.c + ../../../third_party/libsrtp/crypto/hash/sha1.c + ../../../third_party/libsrtp/crypto/hash/auth_test_cases.c + ../../../third_party/libsrtp/crypto/kernel/alloc.c + ../../../third_party/libsrtp/crypto/kernel/crypto_kernel.c + ../../../third_party/libsrtp/crypto/kernel/err.c + ../../../third_party/libsrtp/crypto/kernel/key.c + ../../../third_party/libsrtp/crypto/math/datatypes.c + ../../../third_party/libsrtp/crypto/math/stat.c + ../../../third_party/libsrtp/crypto/replay/rdb.c + ../../../third_party/libsrtp/crypto/replay/rdbx.c + ../../../third_party/libsrtp/crypto/replay/ut_sim.c + # cJSON + ../../../third_party/cJSON/cJSON.c + # usrsctp (nothreads mode - no user_recv_thread.c) + ../../../third_party/usrsctp/usrsctplib/user_environment.c + ../../../third_party/usrsctp/usrsctplib/user_mbuf.c + ../../../third_party/usrsctp/usrsctplib/user_socket.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_asconf.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_auth.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_bsd_addr.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_callout.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_cc_functions.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_crc32.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_indata.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_input.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_output.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_pcb.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_peeloff.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_sha1.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_ss_functions.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_sysctl.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_timer.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_userspace.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_usrreq.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctputil.c +) + +ADD_EXECUTABLE( + ${PRJ} + ${C_SOURCE_FILES} + ${LIBSRC} + ${HTTP_SOURCES} + ${CMAKE_CURRENT_LIST_DIR}/../src/atomic_compat.c + ${CMAKE_CURRENT_LIST_DIR}/../src/inet_compat.c + ${CMAKE_CURRENT_LIST_DIR}/../main.c +) +FILE(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/srtp2/) +FILE(COPY ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/libsrtp/include/srtp.h + DESTINATION ${CMAKE_BINARY_DIR}/srtp2/) + +PROJECT(${PRJ} VERSION 1.0.0) + +TARGET_LINK_LIBRARIES( + ${PRJ} + pico_stdlib + pico_multicore + pico_time + hardware_spi + hardware_timer + hardware_i2c + tinyusb_host + tinyusb_device + tinyusb_board + pico_cyw43_arch_lwip_threadsafe_background + # pico_cyw43_arch_lwip_poll + pico_mbedtls +) + +pico_enable_stdio_usb(${PRJ} 0) +pico_enable_stdio_uart(${PRJ} 1) + +# Explicit UART configuration for RP2350 +target_compile_definitions(${PRJ} PRIVATE + PICO_DEFAULT_UART=0 + PICO_DEFAULT_UART_TX_PIN=0 + PICO_DEFAULT_UART_RX_PIN=1 + PICO_DEFAULT_UART_BAUD_RATE=115200 + MBEDTLS_PLATFORM_TIME_ALT +) + +pico_add_extra_outputs(${PRJ}) + +ADD_COMPILE_OPTIONS( + -Wall + -Wno-format # int != int32_t as far as the compiler is concerned because gcc has int32_t as long int + -Wno-unused-function # we have some for the docs that aren't called + -Wno-maybe-uninitialized + -Wno-incompatible-pointer-types + -Wno-discarded-qualifier + -Wno-error=incompatible-pointer-types +) + diff --git a/examples/hid-usb/cmake.rp2350/pico_sdk_import.cmake b/examples/hid-usb/cmake.rp2350/pico_sdk_import.cmake new file mode 100644 index 000000000..65f8a6f7d --- /dev/null +++ b/examples/hid-usb/cmake.rp2350/pico_sdk_import.cmake @@ -0,0 +1,73 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + GIT_SUBMODULES_RECURSE FALSE + ) + else () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + ) + endif () + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/examples/hid-usb/inc/arpa/inet.h b/examples/hid-usb/inc/arpa/inet.h new file mode 100644 index 000000000..61d89edf5 --- /dev/null +++ b/examples/hid-usb/inc/arpa/inet.h @@ -0,0 +1,9 @@ +#ifndef _ARPA_INET_H_STUB +#define _ARPA_INET_H_STUB + +// Redirect to sys/socket.h which provides inet_pton/inet_ntop declarations +// and includes lwip/inet.h for address types. +// Required by usrsctp and libpeermx for IP address handling. +#include + +#endif diff --git a/examples/hid-usb/inc/ifaddrs.h b/examples/hid-usb/inc/ifaddrs.h new file mode 100644 index 000000000..f09edae78 --- /dev/null +++ b/examples/hid-usb/inc/ifaddrs.h @@ -0,0 +1,48 @@ +#ifndef _IFADDRS_H_STUB +#define _IFADDRS_H_STUB + +// Stub for network interface enumeration on RP2040 bare metal. +// +// usrsctp calls getifaddrs() in sctp_init_ifns_for_vrf() to enumerate +// network interfaces. Our stub returns -1 (failure), causing usrsctp +// to skip interface enumeration. This is acceptable because: +// - RP2040 has a single WiFi interface managed by lwIP +// - We use AF_CONN mode with UDP encapsulation, not raw sockets +// - Interface binding is not needed for our use case + +#include +#include + +// Interface flags - referenced by usrsctp but not actually used +// when getifaddrs() returns failure +#ifndef IFF_UP +#define IFF_UP 0x1 +#endif +#ifndef IFF_RUNNING +#define IFF_RUNNING 0x40 +#endif +#ifndef IFF_LOOPBACK +#define IFF_LOOPBACK 0x8 +#endif + +struct ifaddrs { + struct ifaddrs *ifa_next; + char *ifa_name; + unsigned int ifa_flags; + struct sockaddr *ifa_addr; + struct sockaddr *ifa_netmask; + struct sockaddr *ifa_broadaddr; + void *ifa_data; +}; + +// getifaddrs stub - returns failure to skip interface enumeration +static inline int getifaddrs(struct ifaddrs **ifap) { + *ifap = NULL; + return -1; +} + +static inline void freeifaddrs(struct ifaddrs *ifa) { + (void)ifa; +} + +#endif diff --git a/examples/hid-usb/inc/lwipopts.h b/examples/hid-usb/inc/lwipopts.h new file mode 100644 index 000000000..07cce9ccd --- /dev/null +++ b/examples/hid-usb/inc/lwipopts.h @@ -0,0 +1,97 @@ +#ifndef _LWIPOPTS_EXAMPLE_COMMONH_H +#define _LWIPOPTS_EXAMPLE_COMMONH_H + + +// Common settings used in most of the pico_w examples +// (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html for details) + +// allow override in some examples +#ifndef NO_SYS +#define NO_SYS 1 +#endif +// Socket API requires NO_SYS=0, not available in bare metal mode +// usrsctp uses UDP encapsulation, mbedtls uses altcp_tls +#ifndef LWIP_SOCKET +#define LWIP_SOCKET 0 +#endif +// Prevent redefinition of struct timeval (already defined in ARM toolchain) +#define LWIP_TIMEVAL_PRIVATE 0 +// Enable IGMP for multicast support +#define LWIP_IGMP 1 + +#if PICO_CYW43_ARCH_POLL +#define MEM_LIBC_MALLOC 1 +#else +// MEM_LIBC_MALLOC is incompatible with non polling versions +#define MEM_LIBC_MALLOC 0 +#endif +#define MEM_ALIGNMENT 4 +#define MEM_SIZE 4000 +#define MEMP_NUM_TCP_SEG 32 +#define MEMP_NUM_ARP_QUEUE 10 +#define PBUF_POOL_SIZE 16 +#define LWIP_ARP 1 +#define LWIP_ETHERNET 1 +#define LWIP_ICMP 1 +#define LWIP_RAW 1 +#define TCP_WND (4 * TCP_MSS) +#define TCP_MSS 1460 +#define TCP_SND_BUF (4 * TCP_MSS) +#define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS)) +#define LWIP_NETIF_STATUS_CALLBACK 1 +#define LWIP_NETIF_LINK_CALLBACK 1 +#define LWIP_NETIF_HOSTNAME 1 +#define LWIP_NETCONN 0 +#define MEM_STATS 0 +#define SYS_STATS 0 +#define MEMP_STATS 0 +#define LINK_STATS 0 +// #define ETH_PAD_SIZE 2 +#define LWIP_CHKSUM_ALGORITHM 3 +#define LWIP_DHCP 1 +#define LWIP_IPV4 1 +#define LWIP_IPV6 1 +#define LWIP_TCP 1 +#define LWIP_UDP 1 +#define LWIP_DNS 1 +#define LWIP_TCP_KEEPALIVE 1 +#define LWIP_NETIF_TX_SINGLE_PBUF 1 +#define DHCP_DOES_ARP_CHECK 0 +#define LWIP_DHCP_DOES_ACD_CHECK 0 + +#ifndef NDEBUG +#define LWIP_DEBUG 1 +#define LWIP_STATS 1 +#define LWIP_STATS_DISPLAY 1 +#endif + +#define ETHARP_DEBUG LWIP_DBG_OFF +#define NETIF_DEBUG LWIP_DBG_OFF +#define PBUF_DEBUG LWIP_DBG_OFF +#define API_LIB_DEBUG LWIP_DBG_OFF +#define API_MSG_DEBUG LWIP_DBG_OFF +#define SOCKETS_DEBUG LWIP_DBG_OFF +#define ICMP_DEBUG LWIP_DBG_OFF +#define INET_DEBUG LWIP_DBG_OFF +#define IP_DEBUG LWIP_DBG_OFF +#define IP_REASS_DEBUG LWIP_DBG_OFF +#define RAW_DEBUG LWIP_DBG_OFF +#define MEM_DEBUG LWIP_DBG_OFF +#define MEMP_DEBUG LWIP_DBG_OFF +#define SYS_DEBUG LWIP_DBG_OFF +#define TCP_DEBUG LWIP_DBG_ON +#define TCP_INPUT_DEBUG LWIP_DBG_OFF +#define TCP_OUTPUT_DEBUG LWIP_DBG_OFF +#define TCP_RTO_DEBUG LWIP_DBG_OFF +#define TCP_CWND_DEBUG LWIP_DBG_OFF +#define TCP_WND_DEBUG LWIP_DBG_OFF +#define TCP_FR_DEBUG LWIP_DBG_OFF +#define TCP_QLEN_DEBUG LWIP_DBG_OFF +#define TCP_RST_DEBUG LWIP_DBG_OFF +#define UDP_DEBUG LWIP_DBG_OFF +#define TCPIP_DEBUG LWIP_DBG_ON +#define PPP_DEBUG LWIP_DBG_OFF +#define SLIP_DEBUG LWIP_DBG_OFF +#define DHCP_DEBUG LWIP_DBG_ON + +#endif /* __LWIPOPTS_H__ */ diff --git a/examples/hid-usb/inc/mbedtls_config.h b/examples/hid-usb/inc/mbedtls_config.h new file mode 100644 index 000000000..0a5fc160c --- /dev/null +++ b/examples/hid-usb/inc/mbedtls_config.h @@ -0,0 +1,128 @@ +#ifndef MBEDTLS_CONFIG_H +#define MBEDTLS_CONFIG_H + +/* Workaround for some mbedtls source files using INT_MAX without including limits.h */ +#include + +/*============================================================================ + * Memory optimizations for RP2350 (520KB RAM) + *============================================================================*/ +#define MBEDTLS_NO_PLATFORM_ENTROPY +#define MBEDTLS_ENTROPY_HARDWARE_ALT +#define MBEDTLS_ALLOW_PRIVATE_ACCESS +#define MBEDTLS_HAVE_TIME + +/* Smaller implementations */ +#define MBEDTLS_SHA256_SMALLER +#define MBEDTLS_AES_FEWER_TABLES +#define MBEDTLS_AES_ROM_TABLES +#define MBEDTLS_ECP_NIST_OPTIM + +/* SSL buffer sizes - RP2350 has enough RAM for full 16KB buffers */ +#define MBEDTLS_SSL_MAX_CONTENT_LEN 16384 +#define MBEDTLS_SSL_IN_CONTENT_LEN 16384 +#define MBEDTLS_SSL_OUT_CONTENT_LEN 4096 + +/* Reduce MPI (bignum) window size to save RAM */ +#define MBEDTLS_MPI_WINDOW_SIZE 2 +#define MBEDTLS_MPI_MAX_SIZE 384 + +/* SSL client and server (server needed for DTLS in WebRTC) */ +#define MBEDTLS_SSL_CLI_C +#define MBEDTLS_SSL_SRV_C + +/*============================================================================ + * ECC curves - only what Cloudflare/modern servers use + *============================================================================*/ +#define MBEDTLS_ECP_DP_SECP256R1_ENABLED /* Required for most servers */ +#define MBEDTLS_ECP_DP_SECP384R1_ENABLED /* Fallback */ +#define MBEDTLS_ECP_DP_CURVE25519_ENABLED /* For DTLS/WebRTC */ + +/*============================================================================ + * Key exchange - ECDHE only (no RSA key exchange, save ~15KB) + *============================================================================*/ +#define MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED +#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED + +/*============================================================================ + * Ciphers - minimal set for TLS 1.2 + *============================================================================*/ +#define MBEDTLS_CIPHER_MODE_CBC +#define MBEDTLS_GCM_C +#define MBEDTLS_AES_C +#define MBEDTLS_CIPHER_C + +/*============================================================================ + * Core crypto + *============================================================================*/ +#define MBEDTLS_BIGNUM_C +#define MBEDTLS_CTR_DRBG_C +#define MBEDTLS_ENTROPY_C +#define MBEDTLS_ENTROPY_SHA256_ACCUMULATOR +#define MBEDTLS_MD_C +#define MBEDTLS_SHA1_C +#define MBEDTLS_SHA224_C +#define MBEDTLS_SHA256_C + +/* RSA for certificate verification (servers use RSA certs) */ +#define MBEDTLS_RSA_C +#define MBEDTLS_PKCS1_V15 +#define MBEDTLS_PKCS1_V21 + +/* ECDSA/ECDH */ +#define MBEDTLS_ECDH_C +#define MBEDTLS_ECP_C +#define MBEDTLS_ECDSA_C + +/*============================================================================ + * X.509 / Certificates + *============================================================================*/ +#define MBEDTLS_ASN1_PARSE_C +#define MBEDTLS_ASN1_WRITE_C +#define MBEDTLS_OID_C +#define MBEDTLS_PK_C +#define MBEDTLS_PK_PARSE_C +#define MBEDTLS_X509_CRT_PARSE_C +#define MBEDTLS_X509_USE_C +#define MBEDTLS_PEM_PARSE_C +#define MBEDTLS_BASE64_C +#define MBEDTLS_SSL_SERVER_NAME_INDICATION + +/*============================================================================ + * TLS 1.2 (for HTTPS signaling) + *============================================================================*/ +#define MBEDTLS_SSL_TLS_C +#define MBEDTLS_SSL_PROTO_TLS1_2 +#define MBEDTLS_PLATFORM_C + +/*============================================================================ + * DTLS (for WebRTC) + *============================================================================*/ +#define MBEDTLS_SSL_PROTO_DTLS +#define MBEDTLS_SSL_DTLS_SRTP +#define MBEDTLS_SSL_EXPORT_KEYS +#define MBEDTLS_SSL_DTLS_HELLO_VERIFY +#define MBEDTLS_SSL_COOKIE_C +#define MBEDTLS_TIMING_C + +/*============================================================================ + * Certificate generation (for DTLS self-signed cert) + *============================================================================*/ +#define MBEDTLS_PEM_WRITE_C +#define MBEDTLS_X509_CRT_WRITE_C +#define MBEDTLS_X509_CREATE_C +#define MBEDTLS_GENPRIME +#define MBEDTLS_PK_WRITE_C + +/*============================================================================ + * Misc + *============================================================================*/ +#define MBEDTLS_ERROR_C +/* #define MBEDTLS_DEBUG_C */ /* Disabled: save RAM, enable for debugging */ +#define MBEDTLS_SSL_KEEP_PEER_CERTIFICATE /* Required for DTLS fingerprint verification */ +#define __unix__ + +#define MBEDTLS_PLATFORM_MS_TIME_ALT + + +#endif // MBEDTLS_CONFIG_H diff --git a/examples/hid-usb/inc/net/if.h b/examples/hid-usb/inc/net/if.h new file mode 100644 index 000000000..c6fbffeae --- /dev/null +++ b/examples/hid-usb/inc/net/if.h @@ -0,0 +1,23 @@ +#ifndef _NET_IF_H_STUB +#define _NET_IF_H_STUB + +// Minimal net/if.h stub for RP2040 bare metal + +#ifndef IFNAMSIZ +#define IFNAMSIZ 16 +#endif + +// Forward declaration - used as opaque pointer in usrsctp +struct ifnet; + +// Stub for if_nametoindex - only needed for linking. +// Never actually called because getifaddrs() returns -1, +// causing sctp_init_ifns_for_vrf() to early-return before +// reaching if_nametoindex(). usrsctp uses AF_CONN sockets +// with UDP encapsulation, so interface info is not required. +static inline unsigned int if_nametoindex(const char *ifname) { + (void)ifname; + return 0; +} + +#endif diff --git a/examples/hid-usb/inc/netdb.h b/examples/hid-usb/inc/netdb.h new file mode 100644 index 000000000..4b62d998a --- /dev/null +++ b/examples/hid-usb/inc/netdb.h @@ -0,0 +1,48 @@ +#ifndef _NETDB_H_STUB +#define _NETDB_H_STUB + +// Stub for DNS/name resolution on RP2040 bare metal. +// +// usrsctp's sctp_pcb.c includes but does not actually call +// getaddrinfo() or related functions. This header exists only for +// compilation compatibility. +// +// If DNS resolution is needed in the future, lwIP's dns.h can be used +// with dns_gethostbyname() callback API. + +#include +#include + +#ifndef AF_UNSPEC +#define AF_UNSPEC 0 +#endif + +// addrinfo flags - not used, but defined for compilation +#define AI_PASSIVE 0x01 +#define AI_CANONNAME 0x02 +#define AI_NUMERICHOST 0x04 + +#define EAI_NONAME -2 +#define EAI_FAIL -4 + +struct addrinfo { + int ai_flags; + int ai_family; + int ai_socktype; + int ai_protocol; + size_t ai_addrlen; + struct sockaddr *ai_addr; + char *ai_canonname; + struct addrinfo *ai_next; +}; + +// DNS resolution implemented in inet_compat.c using lwIP +int getaddrinfo_impl(const char *node, const char *service, + const struct addrinfo *hints, + struct addrinfo **res); +void freeaddrinfo_impl(struct addrinfo *res); + +#define getaddrinfo getaddrinfo_impl +#define freeaddrinfo freeaddrinfo_impl + +#endif diff --git a/examples/hid-usb/inc/netinet/in.h b/examples/hid-usb/inc/netinet/in.h new file mode 100644 index 000000000..15b11ec7a --- /dev/null +++ b/examples/hid-usb/inc/netinet/in.h @@ -0,0 +1,9 @@ +#ifndef NETINET_INET_H +#define NETINET_INET_H + +// Redirect to sys/socket.h which provides all necessary types +// (sockaddr_in, sockaddr_in6, in_addr, etc.) via lwip/inet.h. +// Required by sctp_os_userspace.h. +#include + +#endif // NETINET_INET_H diff --git a/examples/hid-usb/inc/netinet/in_systm.h b/examples/hid-usb/inc/netinet/in_systm.h new file mode 100644 index 000000000..69dea3c04 --- /dev/null +++ b/examples/hid-usb/inc/netinet/in_systm.h @@ -0,0 +1,9 @@ +#ifndef _NETINET_IN_SYSTM_H_STUB +#define _NETINET_IN_SYSTM_H_STUB + +// Empty stub - only needed for compilation. +// sctp_os_userspace.h includes , but +// the types (n_short, n_long, n_time) are not actually used +// by usrsctp in our configuration. + +#endif diff --git a/examples/hid-usb/inc/netinet/ip.h b/examples/hid-usb/inc/netinet/ip.h new file mode 100644 index 000000000..edda30a1a --- /dev/null +++ b/examples/hid-usb/inc/netinet/ip.h @@ -0,0 +1,34 @@ +#ifndef _NETINET_IP_H_STUB +#define _NETINET_IP_H_STUB + +// Minimal netinet/ip.h for RP2040 bare metal. +// Required because sctp_os_userspace.h only defines struct ip +// for _WIN32 or __native_client__. For other platforms (including +// RP2040), it expects to provide the definition. + +#include +#include // for struct in_addr + +#define IPVERSION 4 + +#define IPTOS_LOWDELAY 0x10 +#define IPTOS_THROUGHPUT 0x08 +#define IPTOS_RELIABILITY 0x04 + +struct ip { + uint8_t ip_hl:4; + uint8_t ip_v:4; + uint8_t ip_tos; + uint16_t ip_len; + uint16_t ip_id; + uint16_t ip_off; + uint8_t ip_ttl; + uint8_t ip_p; + uint16_t ip_sum; + struct in_addr ip_src; + struct in_addr ip_dst; +}; + +#define IP_MAXPACKET 65535 + +#endif diff --git a/examples/hid-usb/inc/pthread.h b/examples/hid-usb/inc/pthread.h new file mode 100644 index 000000000..76c3215f1 --- /dev/null +++ b/examples/hid-usb/inc/pthread.h @@ -0,0 +1,65 @@ +#ifndef _PTHREAD_H_STUB +#define _PTHREAD_H_STUB + +// Stub for POSIX threads on RP2040 bare metal (NO_SYS=1, single-threaded). +// +// usrsctp uses pthread types and functions for locking throughout its code. +// In bare metal single-threaded mode, all lock operations are no-ops since +// there's no concurrent access. +// +// The ARM toolchain provides pthread types in sys/_pthreadtypes.h. + +#include +#include // for clock_t needed by sys/_pthreadtypes.h +#include + +// rwlock types not provided by ARM toolchain +#ifndef _PTHREAD_RWLOCK_T_DEFINED +#define _PTHREAD_RWLOCK_T_DEFINED +typedef int pthread_rwlock_t; +typedef int pthread_rwlockattr_t; +#endif + +#ifndef PTHREAD_MUTEX_INITIALIZER +#define PTHREAD_MUTEX_INITIALIZER 0 +#endif +#ifndef PTHREAD_MUTEX_ERRORCHECK +#define PTHREAD_MUTEX_ERRORCHECK 0 +#endif + +// Mutex stubs - no-op in single-threaded mode +static inline int pthread_mutexattr_init(pthread_mutexattr_t *a) { (void)a; return 0; } +static inline int pthread_mutexattr_destroy(pthread_mutexattr_t *a) { (void)a; return 0; } +static inline int pthread_mutexattr_settype(pthread_mutexattr_t *a, int t) { (void)a; (void)t; return 0; } +static inline int pthread_mutex_init(pthread_mutex_t *m, const pthread_mutexattr_t *a) { (void)m; (void)a; return 0; } +static inline int pthread_mutex_destroy(pthread_mutex_t *m) { (void)m; return 0; } +static inline int pthread_mutex_lock(pthread_mutex_t *m) { (void)m; return 0; } +static inline int pthread_mutex_unlock(pthread_mutex_t *m) { (void)m; return 0; } +static inline int pthread_mutex_trylock(pthread_mutex_t *m) { (void)m; return 0; } + +// RW lock stubs - no-op in single-threaded mode +static inline int pthread_rwlockattr_init(pthread_rwlockattr_t *a) { (void)a; return 0; } +static inline int pthread_rwlockattr_destroy(pthread_rwlockattr_t *a) { (void)a; return 0; } +static inline int pthread_rwlock_init(pthread_rwlock_t *l, const pthread_rwlockattr_t *a) { (void)l; (void)a; return 0; } +static inline int pthread_rwlock_destroy(pthread_rwlock_t *l) { (void)l; return 0; } +static inline int pthread_rwlock_rdlock(pthread_rwlock_t *l) { (void)l; return 0; } +static inline int pthread_rwlock_wrlock(pthread_rwlock_t *l) { (void)l; return 0; } +static inline int pthread_rwlock_unlock(pthread_rwlock_t *l) { (void)l; return 0; } +static inline int pthread_rwlock_tryrdlock(pthread_rwlock_t *l) { (void)l; return 0; } +static inline int pthread_rwlock_trywrlock(pthread_rwlock_t *l) { (void)l; return 0; } + +// Condition variable stubs - no-op in single-threaded mode +static inline int pthread_cond_init(pthread_cond_t *c, const pthread_condattr_t *a) { (void)c; (void)a; return 0; } +static inline int pthread_cond_destroy(pthread_cond_t *c) { (void)c; return 0; } +static inline int pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m) { (void)c; (void)m; return 0; } +static inline int pthread_cond_signal(pthread_cond_t *c) { (void)c; return 0; } +static inline int pthread_cond_broadcast(pthread_cond_t *c) { (void)c; return 0; } + +// Thread stubs - no-op in single-threaded mode +static inline pthread_t pthread_self(void) { return 0; } +static inline int pthread_join(pthread_t t, void **v) { (void)t; (void)v; return 0; } +static inline int pthread_create(pthread_t *t, const pthread_attr_t *a, void *(*f)(void *), void *arg) { + (void)t; (void)a; (void)f; (void)arg; return 0; +} + +#endif diff --git a/examples/hid-usb/inc/sys/ioctl.h b/examples/hid-usb/inc/sys/ioctl.h new file mode 100644 index 000000000..f57f12212 --- /dev/null +++ b/examples/hid-usb/inc/sys/ioctl.h @@ -0,0 +1,10 @@ +#ifndef _SYS_IOCTL_H_STUB +#define _SYS_IOCTL_H_STUB + +// Empty stub - only needed for compilation. +// sctp_os_userspace.h includes , but ioctl() is only +// called after socket() succeeds in sctp_userspace.c. Since we use +// lwIP raw API (not BSD sockets), socket() is not available and +// the ioctl code path is never executed. + +#endif diff --git a/examples/hid-usb/inc/sys/select.h b/examples/hid-usb/inc/sys/select.h new file mode 100644 index 000000000..fc473d8b6 --- /dev/null +++ b/examples/hid-usb/inc/sys/select.h @@ -0,0 +1,80 @@ +#ifndef _SYS_SELECT_H_STUB +#define _SYS_SELECT_H_STUB + +// Stub for select() on RP2040 bare metal. +// +// libpeermx uses select() to poll sockets for incoming data. In RP2040 +// bare metal mode, lwIP callbacks directly fill receive buffers. +// +// This stub polls cyw43_arch_poll() repeatedly during the timeout period, +// giving lwIP time to process incoming packets and fire callbacks. + +#include +#include + +// Forward declarations to avoid header inclusion order issues +extern void cyw43_arch_poll(void); +extern uint64_t time_us_64(void); +extern void sleep_us(uint64_t us); + +// fd_set for select() - only define if not already defined +#ifndef FD_SETSIZE +#define FD_SETSIZE 16 +#endif + +#ifndef _FD_SET_DEFINED +#define _FD_SET_DEFINED +typedef struct { + unsigned long fds_bits[1]; +} fd_set; +#endif + +#ifndef FD_ZERO +#define FD_ZERO(set) ((set)->fds_bits[0] = 0) +#endif +#ifndef FD_SET +#define FD_SET(fd, set) ((set)->fds_bits[0] |= (1UL << (fd))) +#endif +#ifndef FD_CLR +#define FD_CLR(fd, set) ((set)->fds_bits[0] &= ~(1UL << (fd))) +#endif +#ifndef FD_ISSET +#define FD_ISSET(fd, set) ((set)->fds_bits[0] & (1UL << (fd))) +#endif + +// select stub - poll with timeout +static inline int select(int nfds, fd_set *readfds, fd_set *writefds, + fd_set *exceptfds, struct timeval *timeout) { + (void)nfds; + (void)writefds; + (void)exceptfds; + + if (!readfds || !readfds->fds_bits[0]) { + return 0; + } + + // Calculate timeout in microseconds + uint64_t timeout_us = 0; + if (timeout) { + timeout_us = (uint64_t)timeout->tv_sec * 1000000 + timeout->tv_usec; + } + + // Poll with timeout - give lwIP time to process incoming packets + uint64_t start = time_us_64(); + do { + cyw43_arch_poll(); + if (timeout_us > 0) { + uint64_t elapsed = time_us_64() - start; + if (elapsed >= timeout_us) { + break; + } + // Small sleep to avoid busy loop + sleep_us(100); + } + } while (timeout_us > 0 && (time_us_64() - start) < timeout_us); + + // Return 1 to indicate caller should try recv + return 1; +} + +#endif diff --git a/examples/hid-usb/inc/sys/socket.h b/examples/hid-usb/inc/sys/socket.h new file mode 100644 index 000000000..9d92c006d --- /dev/null +++ b/examples/hid-usb/inc/sys/socket.h @@ -0,0 +1,276 @@ +#ifndef _SYS_SOCKET_H_STUB +#define _SYS_SOCKET_H_STUB + +//============================================================================= +// Minimal socket types stub for RP2040 bare metal (NO_SYS=1, LWIP_SOCKET=0) +// +// This header provides type definitions and constants required by usrsctp +// to compile. We use lwIP raw API instead of BSD sockets, so no actual +// socket functions are implemented here. +// +// usrsctp uses AF_CONN mode with UDP encapsulation - it doesn't use the +// BSD socket API internally, but its headers reference these types. +//============================================================================= + +#include +#include + +// lwIP provides in_addr, in6_addr, and related types +#include + +//----------------------------------------------------------------------------- +// Basic socket types - used throughout usrsctp headers +//----------------------------------------------------------------------------- +#ifndef __SOCKLEN_T_DEFINED +#define __SOCKLEN_T_DEFINED +typedef uint32_t socklen_t; +#endif + +#ifndef __SA_FAMILY_T_DEFINED +#define __SA_FAMILY_T_DEFINED +typedef uint8_t sa_family_t; +#endif + +// ssize_t is already defined in lwip/arch.h + +//----------------------------------------------------------------------------- +// Address families - used by usrsctp for socket address handling +//----------------------------------------------------------------------------- +#ifndef AF_INET +#define AF_INET 2 +#endif +#ifndef AF_INET6 +#define AF_INET6 10 +#endif + +//----------------------------------------------------------------------------- +// Socket types - SOCK_SEQPACKET is used by SCTP +//----------------------------------------------------------------------------- +#ifndef SOCK_STREAM +#define SOCK_STREAM 1 +#endif +#ifndef SOCK_DGRAM +#define SOCK_DGRAM 2 +#endif +#ifndef SOCK_SEQPACKET +#define SOCK_SEQPACKET 5 +#endif + +//----------------------------------------------------------------------------- +// Socket constants - used by usrsctp internally +//----------------------------------------------------------------------------- +#ifndef IPPORT_RESERVED +#define IPPORT_RESERVED 1024 +#endif +#ifndef INET_ADDRSTRLEN +#define INET_ADDRSTRLEN 16 +#endif +#ifndef INET6_ADDRSTRLEN +#define INET6_ADDRSTRLEN 46 +#endif +#ifndef SOMAXCONN +#define SOMAXCONN 128 +#endif + +//----------------------------------------------------------------------------- +// Message flags - used by usrsctp's send/recv operations +// MSG_EOR and MSG_NOTIFICATION are especially important for SCTP +//----------------------------------------------------------------------------- +#ifndef MSG_OOB +#define MSG_OOB 0x01 +#endif +#ifndef MSG_PEEK +#define MSG_PEEK 0x02 +#endif +#ifndef MSG_DONTWAIT +#define MSG_DONTWAIT 0x40 +#endif +#ifndef MSG_EOR +#define MSG_EOR 0x80 +#endif +#ifndef MSG_TRUNC +#define MSG_TRUNC 0x20 +#endif +#ifndef MSG_NOTIFICATION +#define MSG_NOTIFICATION 0x1000 +#endif + +//----------------------------------------------------------------------------- +// Protocol levels - referenced by usrsctp socket option handling +//----------------------------------------------------------------------------- +#ifndef IPPROTO_IP +#define IPPROTO_IP 0 +#endif +#ifndef IPPROTO_TCP +#define IPPROTO_TCP 6 +#endif +#ifndef IPPROTO_UDP +#define IPPROTO_UDP 17 +#endif + +//----------------------------------------------------------------------------- +// Socket options - used by usrsctp's setsockopt/getsockopt +//----------------------------------------------------------------------------- +#ifndef SOL_SOCKET +#define SOL_SOCKET 0xfff +#endif +#ifndef SO_LINGER +#define SO_LINGER 0x0080 +#endif +#ifndef SO_REUSEADDR +#define SO_REUSEADDR 0x0004 +#endif +#ifndef SO_RCVBUF +#define SO_RCVBUF 0x1002 +#endif +#ifndef SO_SNDBUF +#define SO_SNDBUF 0x1001 +#endif +#ifndef SO_ERROR +#define SO_ERROR 0x1007 +#endif + +//----------------------------------------------------------------------------- +// Shutdown flags - used by usrsctp's shutdown handling +//----------------------------------------------------------------------------- +#ifndef SHUT_RD +#define SHUT_RD 0 +#endif +#ifndef SHUT_WR +#define SHUT_WR 1 +#endif +#ifndef SHUT_RDWR +#define SHUT_RDWR 2 +#endif + +//----------------------------------------------------------------------------- +// Linger structure - used with SO_LINGER option +//----------------------------------------------------------------------------- +#ifndef __LINGER_DEFINED +#define __LINGER_DEFINED +struct linger { + int l_onoff; + int l_linger; +}; +#endif + +//----------------------------------------------------------------------------- +// Socket address structures - fundamental types used throughout usrsctp +//----------------------------------------------------------------------------- +#ifndef __SOCKADDR_DEFINED +#define __SOCKADDR_DEFINED +struct sockaddr { + uint8_t sa_len; + uint8_t sa_family; + char sa_data[14]; +}; +#endif + +#ifndef __SOCKADDR_IN_DEFINED +#define __SOCKADDR_IN_DEFINED +struct sockaddr_in { + uint8_t sin_len; + uint8_t sin_family; + uint16_t sin_port; + struct in_addr sin_addr; + char sin_zero[8]; +}; +#endif + +#ifndef __SOCKADDR_IN6_DEFINED +#define __SOCKADDR_IN6_DEFINED +struct sockaddr_in6 { + uint8_t sin6_len; + uint8_t sin6_family; + uint16_t sin6_port; + uint32_t sin6_flowinfo; + struct in6_addr sin6_addr; + uint32_t sin6_scope_id; +}; +#endif + +#ifndef __SOCKADDR_STORAGE_DEFINED +#define __SOCKADDR_STORAGE_DEFINED +struct sockaddr_storage { + uint8_t ss_len; + uint8_t ss_family; + char __ss_padding[126]; +}; +#endif + +//----------------------------------------------------------------------------- +// I/O vector for scatter/gather - used by usrsctp's message handling +//----------------------------------------------------------------------------- +#ifndef __IOVEC_DEFINED +#define __IOVEC_DEFINED +struct iovec { + void *iov_base; + size_t iov_len; +}; +#endif + +#ifndef UIO_MAXIOV +#define UIO_MAXIOV 1024 +#endif + +//----------------------------------------------------------------------------- +// Control message header and macros - used for SCTP ancillary data +// (sndrcvinfo, notifications, etc.) +//----------------------------------------------------------------------------- +#ifndef __CMSGHDR_DEFINED +#define __CMSGHDR_DEFINED +struct cmsghdr { + socklen_t cmsg_len; + int cmsg_level; + int cmsg_type; +}; +#endif + +#ifndef CMSG_ALIGN +#define CMSG_ALIGN(len) (((len) + sizeof(long) - 1) & ~(sizeof(long) - 1)) +#endif +#ifndef CMSG_SPACE +#define CMSG_SPACE(len) (CMSG_ALIGN(sizeof(struct cmsghdr)) + CMSG_ALIGN(len)) +#endif +#ifndef CMSG_LEN +#define CMSG_LEN(len) (CMSG_ALIGN(sizeof(struct cmsghdr)) + (len)) +#endif +#ifndef CMSG_DATA +#define CMSG_DATA(cmsg) ((unsigned char *)((struct cmsghdr *)(cmsg) + 1)) +#endif +#ifndef CMSG_FIRSTHDR +#define CMSG_FIRSTHDR(mhdr) \ + ((size_t)(mhdr)->msg_controllen >= sizeof(struct cmsghdr) ? \ + (struct cmsghdr *)(mhdr)->msg_control : (struct cmsghdr *)0) +#endif +#ifndef CMSG_NXTHDR +#define CMSG_NXTHDR(mhdr, cmsg) \ + (((char *)(cmsg) + CMSG_ALIGN((cmsg)->cmsg_len) + sizeof(struct cmsghdr) > \ + (char *)(mhdr)->msg_control + (mhdr)->msg_controllen) ? \ + (struct cmsghdr *)0 : \ + (struct cmsghdr *)((char *)(cmsg) + CMSG_ALIGN((cmsg)->cmsg_len))) +#endif + +//----------------------------------------------------------------------------- +// Message header structure - used by usrsctp's recvmsg/sendmsg +//----------------------------------------------------------------------------- +#ifndef __MSGHDR_DEFINED +#define __MSGHDR_DEFINED +struct msghdr { + void *msg_name; + socklen_t msg_namelen; + struct iovec *msg_iov; + int msg_iovlen; + void *msg_control; + socklen_t msg_controllen; + int msg_flags; +}; +#endif + +//----------------------------------------------------------------------------- +// IP address conversion functions - implemented in lwIP or our stubs +//----------------------------------------------------------------------------- +int inet_pton(int af, const char *src, void *dst); +const char *inet_ntop(int af, const void *src, char *dst, uint32_t size); + +#endif // _SYS_SOCKET_H_STUB diff --git a/examples/hid-usb/inc/sys/time.h b/examples/hid-usb/inc/sys/time.h new file mode 100644 index 000000000..cc9d71d40 --- /dev/null +++ b/examples/hid-usb/inc/sys/time.h @@ -0,0 +1,60 @@ +#ifndef _SYS_TIME_H_STUB +#define _SYS_TIME_H_STUB + +// Stub for sys/time.h on RP2040 bare metal. +// Provides struct timeval and timer macros for usrsctp. + +#include + +#ifndef _TIMEVAL_DEFINED +#define _TIMEVAL_DEFINED +struct timeval { + long tv_sec; + long tv_usec; +}; +#endif + +// Timer comparison and arithmetic macros used by usrsctp +#ifndef timercmp +#define timercmp(a, b, CMP) \ + (((a)->tv_sec == (b)->tv_sec) ? \ + ((a)->tv_usec CMP (b)->tv_usec) : \ + ((a)->tv_sec CMP (b)->tv_sec)) +#endif + +#ifndef timerclear +#define timerclear(tvp) ((tvp)->tv_sec = (tvp)->tv_usec = 0) +#endif + +#ifndef timerisset +#define timerisset(tvp) ((tvp)->tv_sec || (tvp)->tv_usec) +#endif + +#ifndef timeradd +#define timeradd(a, b, result) \ + do { \ + (result)->tv_sec = (a)->tv_sec + (b)->tv_sec; \ + (result)->tv_usec = (a)->tv_usec + (b)->tv_usec; \ + if ((result)->tv_usec >= 1000000) { \ + ++(result)->tv_sec; \ + (result)->tv_usec -= 1000000; \ + } \ + } while (0) +#endif + +#ifndef timersub +#define timersub(a, b, result) \ + do { \ + (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \ + (result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \ + if ((result)->tv_usec < 0) { \ + --(result)->tv_sec; \ + (result)->tv_usec += 1000000; \ + } \ + } while (0) +#endif + +// gettimeofday - implemented in inet_compat.c +int gettimeofday(struct timeval *tv, void *tz); + +#endif diff --git a/examples/hid-usb/inc/sys/uio.h b/examples/hid-usb/inc/sys/uio.h new file mode 100644 index 000000000..154aa3c1d --- /dev/null +++ b/examples/hid-usb/inc/sys/uio.h @@ -0,0 +1,9 @@ +#ifndef _SYS_UIO_H_STUB +#define _SYS_UIO_H_STUB + +// Redirect to our sys/socket.h stub which defines struct iovec. +// UIO_MAXIOV is also defined in usrsctp's user_socketvar.h. +// Required by sctp_os_userspace.h. +#include + +#endif diff --git a/examples/hid-usb/inc/tusb_config.h b/examples/hid-usb/inc/tusb_config.h new file mode 100644 index 000000000..6ab59e67d --- /dev/null +++ b/examples/hid-usb/inc/tusb_config.h @@ -0,0 +1,10 @@ +#pragma once + +/* CFG_TUSB_MCU is auto-detected by pico-sdk based on PICO_BOARD */ +#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED) + +#define CFG_TUD_ENDPOINT0_SIZE 64 +#define CFG_TUD_HID 1 +#define CFG_TUD_HID_EP_BUFSIZE 16 +#define CFG_TUD_VBUS_MONITORING 0 + diff --git a/examples/hid-usb/main.c b/examples/hid-usb/main.c new file mode 100644 index 000000000..3c3f8dc2c --- /dev/null +++ b/examples/hid-usb/main.c @@ -0,0 +1,782 @@ +#include +#include +#include +#include + +#include "pico/stdlib.h" +#include "hardware/uart.h" +#include "hardware/gpio.h" +#include "pico/cyw43_arch.h" +#include "bsp/board.h" +#include "lwip/dns.h" +#include "lwip/ip_addr.h" +#include "tusb.h" +#include "pico/util/queue.h" +#include "pico/multicore.h" +#include "hardware/resets.h" +#include "hardware/structs/usb.h" +#include "hardware/watchdog.h" + + +#include "peer.h" +#include "cJSON.h" + +//============================================================================= +// Configuration - set via environment variables or defaults below +// Build with: SIGNALING_URL=... WIFI_SSID=... cmake .. +//============================================================================= +#ifndef WIFI_SSID +#define WIFI_SSID "your-wifi-ssid" +#endif +#ifndef WIFI_PASSWORD +#define WIFI_PASSWORD "your-wifi-password" +#endif +#ifndef SIGNALING_URL +#define SIGNALING_URL "http://localhost:8080/webrtc" +#endif +#ifndef SIGNALING_TOKEN +#define SIGNALING_TOKEN "" +#endif + + +//============================================================================= +// HID-USB +//============================================================================= +#define USB_VID 0x413C +#define USB_PID 0x301A +#define USB_BCD 0x0100 +#define POLL_TIME_S 1 +enum +{ + ITF_NUM_HID, + ITF_NUM_TOTAL +}; +enum +{ + EPNUM_HID = 0x81, + EPNUM_HID_B = 0x01 +}; +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN) +#define CONFIG_TOTAL_LEN_B (9 + 9 + 9 + 7) +#define BUF_SIZE 2048 + +enum { + PARSE_IDLE = 0, + PARSE_VALUE, + PARSE_DATA, + PARSE_END +}; + +//============================================================================= +// Queue (core1 -> core0, multicore-safe) +//============================================================================= +#define QUEUE_SIZE 128 +#define QUEUE_ITEM_LEN 128 + +typedef struct { + uint8_t data[QUEUE_ITEM_LEN]; + uint16_t len; +} queue_entry_t; + +static queue_t g_queue; +static void hid_task(void); + +//============================================================================= +// HID USB +//============================================================================= +tusb_desc_device_t const desc_device = +{ + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = USB_VID, + .idProduct = USB_PID, + .bcdDevice = USB_BCD, + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + .bNumConfigurations = 0x01 +}; +uint8_t const * tud_descriptor_device_cb(void) { + return((uint8_t const *) &desc_device); +} + +uint8_t const desc_hid_report[] = { + TUD_HID_REPORT_DESC_MOUSE(HID_REPORT_ID(1)) +}; +uint8_t const desc_hid_report_b[] = { + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x02, // Usage (Mouse) + 0xA1, 0x01, // Collection (Application) + 0x09, 0x01, // Usage (Pointer) + 0xA1, 0x00, // Collection (Physical) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Minimum (Button 1) + 0x29, 0x03, // Usage Maximum (Button 3) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x95, 0x03, // Report Count (3 buttons) + 0x75, 0x01, // Report Size (1) + 0x81, 0x02, // Input (Data,Var,Abs) + 0x95, 0x01, // Report Count (1) + 0x75, 0x05, // Report Size (5) padding + 0x81, 0x03, // Input (Const,Var,Abs) + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x15, 0x81, // Logical Minimum (-127) + 0x25, 0x7F, // Logical Maximum (127) + 0x75, 0x08, // Report Size (8) + 0x95, 0x02, // Report Count (2 bytes: X,Y) + 0x81, 0x06, // Input (Data,Var,Rel) + 0xC0, // End Collection + 0xC0 // End Collection +}; + +uint8_t const * tud_hid_descriptor_report_cb(uint8_t instance) { + return(desc_hid_report_b); +} + +uint8_t const desc_configuration[] = { + TUD_CONFIG_DESCRIPTOR( + 1, + ITF_NUM_TOTAL, + 0, + CONFIG_TOTAL_LEN, + 0x00, + 100 + ), TUD_HID_DESCRIPTOR( + ITF_NUM_HID, + 0, + HID_ITF_PROTOCOL_MOUSE, + sizeof(desc_hid_report_b), + EPNUM_HID, + 8, + 10 + ) +}; + + +uint8_t const desc_configuration_b[] = { + + 0x09, // bLength + TUSB_DESC_CONFIGURATION, // bDescriptorType + (CONFIG_TOTAL_LEN_B & 0xFF), // wTotalLength LSB + (CONFIG_TOTAL_LEN_B >> 8), // wTotalLength MSB + 0x01, // bNumInterfaces + 0x01, // bConfigurationValue + 0x00, // iConfiguration + TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, // bmAttributes (Bus powered + Remote Wakeup Available) + 250, // bMaxPower (2mA units) -> 250 = 500mA + + // ----- Interface Descriptor (HID Boot Mouse) ----- + 0x09, // bLength + TUSB_DESC_INTERFACE, // bDescriptorType + ITF_NUM_HID, // bInterfaceNumber + 0x00, // bAlternateSetting + 0x01, // bNumEndpoints (1 IN endpoint) + TUSB_CLASS_HID, // bInterfaceClass (HID = 0x03) + HID_SUBCLASS_BOOT, // bInterfaceSubClass (Boot Interface = 0x01) + HID_ITF_PROTOCOL_MOUSE, // bInterfaceProtocol (Mouse = 0x02) + 0x00, // iInterface + + // ----- HID Descriptor ----- + 0x09, // bLength + HID_DESC_TYPE_HID, // bDescriptorType (HID) + 0x11, 0x01, // bcdHID = 1.11 + 0x00, // bCountryCode + 0x01, // bNumDescriptors + HID_DESC_TYPE_REPORT, // bDescriptorType (Report) + sizeof(desc_hid_report_b) & 0xFF, // wDescriptorLength LSB + (sizeof(desc_hid_report_b) >> 8), // wDescriptorLength MSB + + // ----- Endpoint Descriptor (INT IN) ----- + 0x07, // bLength + TUSB_DESC_ENDPOINT, // bDescriptorType + 0x80 | EPNUM_HID_B, // bEndpointAddress (IN endpoint 1) + TUSB_XFER_INTERRUPT, // bmAttributes (Interrupt) + 0x08, 0x00, // wMaxPacketSize (8 bytes) + 0x0A // bInterval (10ms) + +}; +uint8_t const * tud_descriptor_configuration_cb(uint8_t index) { + return(desc_configuration_b); +} + +char const *string_desc_arr[] = { + (const char[]) { 0x09, 0x04 }, // 0: lang ID = 0x0409 (US-EN) + "mx-mouse", // 1: Manufacturer + "mx-mouse", // 2: Product + "987654" // 3: Serial +}; + +static uint16_t _desc_str[32] = { 0x00,}; + +uint16_t const * tud_descriptor_string_cb(uint8_t index, uint16_t langid) { + uint8_t chr_count; + if (index == 0) { + _desc_str[1] = (uint16_t) string_desc_arr[0][0] | (string_desc_arr[0][1] << 8); + _desc_str[0] = (TUSB_DESC_STRING << 8) | (2 + 2); + return(_desc_str); + } + + const char *str = string_desc_arr[index]; + chr_count = (uint8_t) strlen(str); + if (chr_count > 31) { chr_count = 31; } + + for (uint8_t i = 0; i < chr_count; i++) { + _desc_str[1 + i] = str[i]; + } + _desc_str[0] = (TUSB_DESC_STRING << 8) | (2 * chr_count + 2); + return(_desc_str); +} + + +//============================================================================= +// Global state +//============================================================================= +static PeerConnection* g_pc = NULL; +static PeerConnectionState g_state = PEER_CONNECTION_NEW; +static uint32_t g_dcmsg_time = 0; +static uint32_t g_dcmsg_interval = 15000; // keepAlive interval with jitter (ms) +static int g_count = 0; + +#define KEEPALIVE_BASE_MS 15000 // 15 seconds +#define KEEPALIVE_JITTER_PCT 20 // ±20% → 12000–18000 ms + + +// Return randomized interval: base ± jitter% +static uint32_t keepalive_jittered_interval(void) { + uint32_t jitter_range = KEEPALIVE_BASE_MS * KEEPALIVE_JITTER_PCT / 100; // 3000 + uint32_t lo = KEEPALIVE_BASE_MS - jitter_range; // 12000 + uint32_t hi = KEEPALIVE_BASE_MS + jitter_range; // 18000 + return lo + (rand() % (hi - lo + 1)); +} + +//============================================================================= +// Timing measurement +//============================================================================= +static uint32_t g_time_boot = 0; // Boot time (reference) +static uint32_t g_time_wifi_start = 0; // WiFi connection start +static uint32_t g_time_wifi_done = 0; // WiFi connected +static uint32_t g_time_signaling_start = 0;// Signaling server connect start +static uint32_t g_time_ice_checking = 0; // ICE checking started +static uint32_t g_time_ice_connected = 0; // ICE connected (DTLS starts) +static uint32_t g_time_ice_completed = 0; // ICE completed +static uint32_t g_time_datachannel_open = 0; // DataChannel opened (SCTP ready) +static uint32_t g_time_first_rx = 0; // First message received +static uint32_t g_time_first_tx = 0; // First message sent +static bool g_first_rx_logged = false; +static bool g_first_tx_logged = false; + +#define LOG_TIMING(label) printf("[TIMING] %s: %lu ms\n", (label), (unsigned long)board_millis()) + +//============================================================================= +// LED blink state machine (non-blocking) +//============================================================================= +typedef struct { + uint32_t interval_ms; // Blink interval + int remaining; // Remaining blink count + uint32_t last_toggle; // Last toggle time + bool led_on; // Current LED state +} LedBlinkState; + +static LedBlinkState g_led_blink = {0, 0, 0, false}; + +// Start a blink pattern +static void led_blink_start(uint32_t interval_ms, int count) { + g_led_blink.interval_ms = interval_ms; + g_led_blink.remaining = count * 2; // Each blink = on + off + g_led_blink.last_toggle = board_millis(); + g_led_blink.led_on = true; + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1); +} + +// Process LED blink (call from main loop) +static void led_blink_loop(void) { + if (g_led_blink.remaining <= 0) return; + + uint32_t now = board_millis(); + if ((now - g_led_blink.last_toggle) >= g_led_blink.interval_ms) { + g_led_blink.last_toggle = now; + g_led_blink.remaining--; + + if (g_led_blink.remaining > 0) { + g_led_blink.led_on = !g_led_blink.led_on; + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, g_led_blink.led_on ? 1 : 0); + } else { + // Done blinking - turn off LED + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0); + g_led_blink.led_on = false; + } + } +} + +// TX: 100ms x 3 blinks (ピッ・ピッ・ピッ) +static void led_blink_tx(void) { + led_blink_start(100, 3); +} + +// RX: 20ms x 15 blinks (ピピピピピ...) +static void led_blink_rx(void) { + led_blink_start(20, 15); +} + +//============================================================================= +// Callbacks(PeerConnection) +//============================================================================= +static void onconnectionstatechange(PeerConnectionState state, void* data) { + uint32_t now = board_millis(); + printf("[TIMING] State -> %s: %lu ms\n", peer_connection_state_to_string(state), (unsigned long)now); + + switch (state) { + case PEER_CONNECTION_CHECKING: + g_time_ice_checking = now; + printf("[TIMING] ICE checking started: %lu ms (WiFi+%lu ms)\n", + (unsigned long)now, (unsigned long)(now - g_time_wifi_done)); + break; + case PEER_CONNECTION_CONNECTED: + g_time_ice_connected = now; + printf("[TIMING] ICE connected (DTLS starting): %lu ms (ICE took %lu ms)\n", + (unsigned long)now, (unsigned long)(now - g_time_ice_checking)); + break; + case PEER_CONNECTION_COMPLETED: + g_time_ice_completed = now; + printf("[TIMING] ICE completed: %lu ms\n", (unsigned long)now); + break; + case PEER_CONNECTION_FAILED: + case PEER_CONNECTION_DISCONNECTED: + printf("[REBOOT] PeerConnection %s at %lu ms — rebooting in 1s\n", + peer_connection_state_to_string(state), (unsigned long)now); + sleep_ms(1000); + watchdog_reboot(0, 0, 0); + while (1) tight_loop_contents(); // wait for watchdog reset + break; + default: + break; + } + g_state = state; +} + +static void onopen(void* user_data) { + g_time_datachannel_open = board_millis(); + printf("[TIMING] DataChannel opened (SCTP ready): %lu ms\n", (unsigned long)g_time_datachannel_open); + printf("[TIMING] DTLS+SCTP took: %lu ms (from ICE connected)\n", + (unsigned long)(g_time_datachannel_open - g_time_ice_connected)); + printf("[TIMING] Total connection time: %lu ms (from boot)\n", + (unsigned long)g_time_datachannel_open); +} + +static void onclose(void* user_data) { + printf("DataChannel closed\n"); +} + +static void onmessage(char* msg, size_t len, void* user_data, uint16_t sid) { + // Log first RX timing + if (!g_first_rx_logged) { + g_time_first_rx = board_millis(); + g_first_rx_logged = true; + printf("[TIMING] First message RX: %lu ms (from boot)\n", (unsigned long)g_time_first_rx); + printf("[TIMING] Time from DataChannel open to first RX: %lu ms\n", + (unsigned long)(g_time_first_rx - g_time_datachannel_open)); + } + // RX blink: 20ms x 15 + led_blink_rx(); + + // Parse JSON: {"command":"x,y,z","type":"mouse"} + cJSON* json = cJSON_ParseWithLength(msg, len); + if (json) { + const cJSON* type = cJSON_GetObjectItemCaseSensitive(json, "type"); + const cJSON* command = cJSON_GetObjectItemCaseSensitive(json, "command"); + + if (cJSON_IsString(type) && strcmp(type->valuestring, "mouse") == 0) { + // Single command (backward compatible) + if (cJSON_IsString(command) && command->valuestring[0] != '\0') { + queue_entry_t entry = {0}; + entry.len = strlen(command->valuestring); + if (entry.len > QUEUE_ITEM_LEN) entry.len = QUEUE_ITEM_LEN; + memcpy(entry.data, command->valuestring, entry.len); + queue_try_add(&g_queue, &entry); + } + // Batch commands (array) + const cJSON* commands = cJSON_GetObjectItemCaseSensitive(json, "commands"); + if (cJSON_IsArray(commands)) { + cJSON* cmd_item = NULL; + cJSON_ArrayForEach(cmd_item, commands) { + if (cJSON_IsString(cmd_item) && cmd_item->valuestring[0] != '\0') { + queue_entry_t entry = {0}; + entry.len = strlen(cmd_item->valuestring); + if (entry.len > QUEUE_ITEM_LEN) entry.len = QUEUE_ITEM_LEN; + memcpy(entry.data, cmd_item->valuestring, entry.len); + queue_try_add(&g_queue, &entry); + } + } + } + } else { + printf(" [JSON] unknown type=%s\n", cJSON_IsString(type) ? type->valuestring : "(null)"); + } + cJSON_Delete(json); + } else if (len >= 4 && strncmp(msg, "ping", 4) == 0) { + peer_connection_datachannel_send(g_pc, "pong", 4); + // TX blink: 100ms x 3 + led_blink_tx(); + } +} + +//============================================================================= +// WiFi timing variables +//============================================================================= +static uint32_t g_time_wifi_scan_start = 0; +static uint32_t g_time_wifi_auth_start = 0; +static uint32_t g_time_wifi_auth_done = 0; +static uint32_t g_time_dhcp_start = 0; +static uint32_t g_time_dhcp_done = 0; + +//============================================================================= +// WiFi initialization (detailed timing) +//============================================================================= +static int wifi_init(void) { + int last_status = CYW43_LINK_DOWN; + int status; + uint32_t now; + uint32_t timeout_start; + + cyw43_arch_enable_sta_mode(); + + // Start async WiFi connection + g_time_wifi_start = board_millis(); + g_time_wifi_scan_start = g_time_wifi_start; + printf("[TIMING] WiFi scan start: %lu ms\n", (unsigned long)g_time_wifi_scan_start); + printf("Connecting to WiFi '%s'...\n", WIFI_SSID); + + if (cyw43_arch_wifi_connect_async(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK) != 0) { + printf("WiFi connect_async failed\n"); + return -1; + } + + // Poll for connection status with detailed timing + timeout_start = board_millis(); + while (1) { + cyw43_arch_poll(); + sleep_ms(10); + + now = board_millis(); + if ((now - timeout_start) > 30000) { + printf("WiFi connection timeout\n"); + return -1; + } + + status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + + // Detect state transitions + if (status != last_status) { + switch (status) { + case CYW43_LINK_JOIN: + // Auth/Assoc completed (scan also done at this point) + g_time_wifi_auth_done = now; + printf("[TIMING] WiFi scan done: %lu ms (took %lu ms)\n", + (unsigned long)now, (unsigned long)(now - g_time_wifi_scan_start)); + printf("[TIMING] WiFi auth/assoc done: %lu ms\n", (unsigned long)now); + break; + + case CYW43_LINK_NOIP: + // Link up, waiting for DHCP + g_time_dhcp_start = now; + printf("[TIMING] DHCP start: %lu ms\n", (unsigned long)now); + break; + + case CYW43_LINK_UP: + // DHCP completed, got IP + g_time_dhcp_done = now; + g_time_wifi_done = now; + printf("[TIMING] DHCP bound: %lu ms (took %lu ms)\n", + (unsigned long)now, (unsigned long)(now - g_time_dhcp_start)); + printf("[TIMING] WiFi fully connected: %lu ms (total %lu ms)\n", + (unsigned long)now, (unsigned long)(now - g_time_wifi_start)); + goto wifi_connected; + + case CYW43_LINK_FAIL: + printf("WiFi connection failed\n"); + return -1; + + case CYW43_LINK_NONET: + printf("WiFi: No matching SSID found\n"); + return -1; + + case CYW43_LINK_BADAUTH: + printf("WiFi: Authentication failure\n"); + return -1; + + default: + break; + } + last_status = status; + } + } + +wifi_connected: + // Blink LED to confirm WiFi connected (disabled for timing measurement) + for (int i = 0; i < 5; i++) { + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1); + sleep_ms(100); + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0); + sleep_ms(100); + } + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1); + + return 0; +} + +//============================================================================= +// DNS resolution timing +//============================================================================= +static uint32_t g_time_dns_start = 0; +static uint32_t g_time_dns_done = 0; +static volatile bool g_dns_done = false; +static ip_addr_t g_dns_result; + +static void dns_callback(const char *name, const ip_addr_t *ipaddr, void *arg) { + g_time_dns_done = board_millis(); + if (ipaddr) { + g_dns_result = *ipaddr; + printf("[TIMING] DNS resolved: %lu ms (took %lu ms) -> %s\n", + (unsigned long)g_time_dns_done, + (unsigned long)(g_time_dns_done - g_time_dns_start), + ipaddr_ntoa(ipaddr)); + } else { + printf("[TIMING] DNS resolution failed: %lu ms\n", (unsigned long)g_time_dns_done); + } + g_dns_done = true; +} + +// Extract hostname from URL (e.g., "http://example.com:8080/path" -> "example.com") +static void extract_hostname(const char *url, char *hostname, size_t hostname_size) { + const char *start = url; + const char *end; + + // Skip protocol + if (strncmp(url, "http://", 7) == 0) start = url + 7; + else if (strncmp(url, "https://", 8) == 0) start = url + 8; + + // Find end of hostname (port or path) + end = start; + while (*end && *end != ':' && *end != '/') end++; + + size_t len = end - start; + if (len >= hostname_size) len = hostname_size - 1; + memcpy(hostname, start, len); + hostname[len] = '\0'; +} + +//============================================================================= +// WebRTC initialization +//============================================================================= +static int webrtc_init(void) { + PeerConfiguration config = { + .ice_servers = { + {.urls = "stun:stun.l.google.com:19302"}, + }, + .datachannel = DATA_CHANNEL_STRING, + .video_codec = CODEC_NONE, + .audio_codec = CODEC_NONE, + }; + + printf("Initializing WebRTC...\n"); + peer_init(); + + g_pc = peer_connection_create(&config); + if (!g_pc) { + printf("Failed to create PeerConnection\n"); + return -1; + } + + peer_connection_oniceconnectionstatechange(g_pc, onconnectionstatechange); + peer_connection_ondatachannel(g_pc, onmessage, onopen, onclose); + + // DNS resolution test for signaling server + char hostname[128]; + extract_hostname(SIGNALING_URL, hostname, sizeof(hostname)); + + g_time_dns_start = board_millis(); + printf("[TIMING] DNS lookup start: %lu ms (host: %s)\n", (unsigned long)g_time_dns_start, hostname); + + g_dns_done = false; + err_t err = dns_gethostbyname(hostname, &g_dns_result, dns_callback, NULL); + if (err == ERR_OK) { + // Already cached + g_time_dns_done = board_millis(); + printf("[TIMING] DNS resolved (cached): %lu ms -> %s\n", + (unsigned long)g_time_dns_done, ipaddr_ntoa(&g_dns_result)); + } else if (err == ERR_INPROGRESS) { + // Wait for DNS callback + uint32_t dns_timeout = board_millis(); + while (!g_dns_done && (board_millis() - dns_timeout) < 10000) { + cyw43_arch_poll(); + sleep_ms(10); + } + if (!g_dns_done) { + printf("[TIMING] DNS timeout\n"); + } + } else { + printf("[TIMING] DNS lookup failed: err=%d\n", err); + } + + g_time_signaling_start = board_millis(); + printf("[TIMING] Signaling server connect start: %lu ms\n", (unsigned long)g_time_signaling_start); + printf("Connecting to signaling server: %s\n", SIGNALING_URL); + peer_signaling_connect(SIGNALING_URL, + SIGNALING_TOKEN[0] ? SIGNALING_TOKEN : NULL, + g_pc); + + return 0; +} + +//============================================================================= +// Core 1: CYW43 + WiFi + WebRTC (all network on core1) +// cyw43_arch_init() called here so background worker IRQs fire on core1, +// never blocking core0's USB interrupts. +//============================================================================= +static void core1_entry(void) { + uint32_t now; + + // CYW43 init on core1: background worker binds to THIS core + if (cyw43_arch_init()) { + printf("core1: cyw43_arch_init failed\n"); + while (1) { sleep_ms(1000); } + } + + if (wifi_init() != 0) { + printf("core1: WiFi init failed\n"); + while (1) { sleep_ms(1000); } + } + + // Seed PRNG after WiFi init (board_millis provides entropy from boot timing) + // Must be before webrtc_init() so UDP socket gets a random ephemeral port + srand(board_millis()); + + if (webrtc_init() != 0) { + printf("core1: WebRTC init failed\n"); + while (1) { sleep_ms(1000); } + } + + g_dcmsg_interval = keepalive_jittered_interval(); + + printf("core1: entering WebRTC loop\n"); + + while (1) { + peer_connection_loop(g_pc); + + if (g_state == PEER_CONNECTION_COMPLETED) { + now = board_millis(); + + if ((now - g_dcmsg_time) > g_dcmsg_interval) { + g_dcmsg_time = now; + g_dcmsg_interval = keepalive_jittered_interval(); + char msg[64]; + snprintf(msg, sizeof(msg), "datachannel message: %05d", g_count++); + peer_connection_datachannel_send(g_pc, msg, strlen(msg)); + + if (!g_first_tx_logged) { + g_time_first_tx = board_millis(); + g_first_tx_logged = true; + printf("[TIMING] First message TX: %lu ms (from boot)\n", (unsigned long)g_time_first_tx); + printf("[TIMING] Time from DataChannel open to first TX: %lu ms\n", + (unsigned long)(g_time_first_tx - g_time_datachannel_open)); + } + + led_blink_tx(); + } + } + + led_blink_loop(); + sleep_us(500); + } +} + +//============================================================================= +// Main loop (Core 0: USB + HID + LED) +//============================================================================= +int main() { + // Explicit UART setup for RP2350 compatibility + uart_init(uart0, 115200); + gpio_set_function(0, GPIO_FUNC_UART); // TX + gpio_set_function(1, GPIO_FUNC_UART); // RX + + stdio_init_all(); + + // Wait for serial + sleep_ms(2000); + g_time_boot = board_millis(); + printf("\n\n=== RP2040 WebRTC+HID Demo (Dual Core) ===\n"); + printf("[TIMING] Boot complete: %lu ms\n", (unsigned long)g_time_boot); + + queue_init(&g_queue, sizeof(queue_entry_t), QUEUE_SIZE); + + // Launch core1: CYW43 + WiFi + WebRTC + // cyw43_arch_init() on core1 ensures background worker IRQs + // fire on core1, keeping core0's USB IRQs unblocked + printf("Launching core1 for CYW43/WiFi/WebRTC...\n"); + multicore_launch_core1(core1_entry); + // + while (1) { + queue_entry_t itm; + if (queue_try_remove(&g_queue, &itm)) { break; } + sleep_ms(100); + } + // Core 0: USB only (tud_task must never be blocked) + board_init(); + irq_set_priority(USBCTRL_IRQ, 0); + tusb_init(); + // + while (1) { + tud_task(); + hid_task(); + tight_loop_contents(); + } + return 0; +} + + +static void hid_task(void) { + queue_entry_t itm; + static uint32_t start_ms = 0; + uint32_t now = board_millis(); + if ((now - start_ms) < 10) { return; } + start_ms = now; + if (!tud_mounted()) { return; } + if (!tud_hid_ready()) { return; } + if (!queue_try_remove(&g_queue, &itm)) { return; } + if (!itm.len) { return; } + + int op = 0, dx = 0, dy = 0; + int ret = sscanf( + itm.data, + "%d %d %d", + &op, &dx, &dy + ); + if (ret == 3) { + tud_hid_mouse_report(0, (int8_t)op, (int8_t)dx, (int8_t)dy, 0, 0); + } +} + +uint16_t tud_hid_get_report_cb(uint8_t instance, + uint8_t report_id, + hid_report_type_t report_type, + uint8_t* buffer, + uint16_t reqlen) +{ + return(0); +} + +void tud_hid_set_report_cb( + uint8_t instance, + uint8_t report_id, + hid_report_type_t report_type, + uint8_t const* buffer, + uint16_t bufsize) +{ +} diff --git a/examples/hid-usb/patch/libsrtp.patch b/examples/hid-usb/patch/libsrtp.patch new file mode 100644 index 000000000..5bac2f57d --- /dev/null +++ b/examples/hid-usb/patch/libsrtp.patch @@ -0,0 +1,13 @@ +diff --git a/include/srtp.h b/include/srtp.h +index fa34daf..222ef69 100644 +--- a/include/srtp.h ++++ b/include/srtp.h +@@ -614,7 +614,7 @@ srtp_err_status_t srtp_add_stream(srtp_t session, const srtp_policy_t *policy); + * - [other] otherwise. + * + */ +-srtp_err_status_t srtp_remove_stream(srtp_t session, unsigned int ssrc); ++srtp_err_status_t srtp_remove_stream(srtp_t session, uint32_t ssrc); + + /** + * @brief srtp_update() updates all streams in the session. diff --git a/examples/hid-usb/patch/mbedtls.patch b/examples/hid-usb/patch/mbedtls.patch new file mode 100644 index 000000000..87f907ee3 --- /dev/null +++ b/examples/hid-usb/patch/mbedtls.patch @@ -0,0 +1,13 @@ +diff --git a/include/mbedtls/mbedtls_config.h b/include/mbedtls/mbedtls_config.h +index 4292b493bd..738b032c03 100644 +--- a/include/mbedtls/mbedtls_config.h ++++ b/include/mbedtls/mbedtls_config.h +@@ -1788,7 +1788,7 @@ + * + * Uncomment this to enable support for use_srtp extension. + */ +-//#define MBEDTLS_SSL_DTLS_SRTP ++#define MBEDTLS_SSL_DTLS_SRTP + + /** + * \def MBEDTLS_SSL_DTLS_CLIENT_PORT_REUSE diff --git a/examples/hid-usb/patch/usrsctp.patch b/examples/hid-usb/patch/usrsctp.patch new file mode 100644 index 000000000..a84c5f6f6 --- /dev/null +++ b/examples/hid-usb/patch/usrsctp.patch @@ -0,0 +1,148 @@ +diff --git a/usrsctplib/user_environment.c b/usrsctplib/user_environment.c +index 3deb3ef..3b339d0 100755 +--- a/usrsctplib/user_environment.c ++++ b/usrsctplib/user_environment.c +@@ -374,6 +374,38 @@ read_random(void *buf, size_t size) + return; + } + ++void ++finish_random(void) ++{ ++ return; ++} ++#elif defined(__RP2040_BM__) ++// RP2040 bare metal - use Pico SDK hardware RNG ++#include "pico/rand.h" ++ ++void ++init_random(void) ++{ ++ return; ++} ++ ++void ++read_random(void *buf, size_t size) ++{ ++ uint8_t *p = (uint8_t *)buf; ++ while (size >= 4) { ++ uint32_t r = get_rand_32(); ++ memcpy(p, &r, 4); ++ p += 4; ++ size -= 4; ++ } ++ if (size > 0) { ++ uint32_t r = get_rand_32(); ++ memcpy(p, &r, size); ++ } ++ return; ++} ++ + void + finish_random(void) + { +diff --git a/usrsctplib/user_socket.c b/usrsctplib/user_socket.c +index ce9daed..6053fed 100755 +--- a/usrsctplib/user_socket.c ++++ b/usrsctplib/user_socket.c +@@ -664,6 +664,17 @@ out: + int + getsockaddr(struct sockaddr **namp, caddr_t uaddr, size_t len) + { ++ printf("getsockaddr: namp=%p, uaddr=%p, len=%zu\n", ++ (void*)namp, (void*)uaddr, len); ++ if (len >= 8) { ++ printf("getsockaddr: input bytes[0..7]: %02x %02x %02x %02x %02x %02x %02x %02x\n", ++ ((unsigned char*)uaddr)[0], ((unsigned char*)uaddr)[1], ++ ((unsigned char*)uaddr)[2], ((unsigned char*)uaddr)[3], ++ ((unsigned char*)uaddr)[4], ((unsigned char*)uaddr)[5], ++ ((unsigned char*)uaddr)[6], ((unsigned char*)uaddr)[7]); ++ } ++ printf("getsockaddr: sizeof(struct sockaddr)=%zu, offsetof(sa_data)=%zu\n", ++ sizeof(struct sockaddr), offsetof(struct sockaddr, sa_data)); + struct sockaddr *sa; + int error; + +@@ -673,12 +684,24 @@ getsockaddr(struct sockaddr **namp, caddr_t uaddr, size_t len) + return (EINVAL); + MALLOC(sa, struct sockaddr *, len, M_SONAME, M_WAITOK); + error = copyin(uaddr, sa, len); ++ printf("getsockaddr: copyin returned %d, sa=%p\n", error, (void*)sa); ++ if (len >= 8) { ++ printf("getsockaddr: copied bytes[0..7]: %02x %02x %02x %02x %02x %02x %02x %02x\n", ++ ((unsigned char*)sa)[0], ((unsigned char*)sa)[1], ++ ((unsigned char*)sa)[2], ((unsigned char*)sa)[3], ++ ((unsigned char*)sa)[4], ((unsigned char*)sa)[5], ++ ((unsigned char*)sa)[6], ((unsigned char*)sa)[7]); ++ } + if (error) { + FREE(sa, M_SONAME); + } else { + #ifdef HAVE_SA_LEN ++ printf("getsockaddr: HAVE_SA_LEN defined, setting sa_len=%zu\n", len); + sa->sa_len = len; ++#else ++ printf("getsockaddr: HAVE_SA_LEN not defined\n"); + #endif ++ printf("getsockaddr: sa->sa_family=%d (at offset %zu)\n", sa->sa_family, offsetof(struct sockaddr, sa_family)); + *namp = sa; + } + return (error); +@@ -1476,17 +1499,30 @@ int + usrsctp_bind(struct socket *so, struct sockaddr *name, int namelen) + { + struct sockaddr *sa; ++ int gsa_err, bind_err; ++ ++ printf("usrsctp_bind: so=%p, name=%p, namelen=%d\n", ++ (void*)so, (void*)name, namelen); + + if (so == NULL) { + errno = EBADF; + return (-1); + } +- if ((errno = getsockaddr(&sa, (caddr_t)name, namelen)) != 0) ++ ++ gsa_err = getsockaddr(&sa, (caddr_t)name, namelen); ++ printf("usrsctp_bind: getsockaddr returned %d, sa=%p\n", gsa_err, (void*)sa); ++ if (gsa_err != 0) { ++ errno = gsa_err; + return (-1); ++ } + +- errno = sobind(so, sa); ++ printf("usrsctp_bind: calling sobind, sa->sa_family=%d\n", sa->sa_family); ++ bind_err = sobind(so, sa); ++ printf("usrsctp_bind: sobind returned %d\n", bind_err); + FREE(sa, M_SONAME); +- if (errno) { ++ errno = bind_err; ++ if (bind_err) { ++ printf("usrsctp_bind: failed with errno=%d\n", bind_err); + return (-1); + } else { + return (0); +diff --git a/usrsctplib/usrsctp.h b/usrsctplib/usrsctp.h +index e0c17c3..919a08e 100644 +--- a/usrsctplib/usrsctp.h ++++ b/usrsctplib/usrsctp.h +@@ -43,6 +43,9 @@ extern "C" { + #endif + #include + #include ++#elif defined(CONFIG_USE_LWIP) ++#include ++#include + #else + #include + #include +@@ -120,7 +123,8 @@ struct sctp_common_header { + * tune with other sockaddr_* structures. + */ + #if defined(__APPLE__) || defined(__Bitrig__) || defined(__DragonFly__) || \ +- defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) ++ defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || \ ++ defined(__RP2040_BM__) + struct sockaddr_conn { + uint8_t sconn_len; + uint8_t sconn_family; diff --git a/examples/hid-usb/scene-detect/README.md b/examples/hid-usb/scene-detect/README.md new file mode 100644 index 000000000..a35ae7f56 --- /dev/null +++ b/examples/hid-usb/scene-detect/README.md @@ -0,0 +1,138 @@ +# Scene Detect + +Real-time scene detection and automated gameplay for Monster Strike, using a Raspberry Pi camera and USB HID mouse control over a WebRTC SFU. + +## Overview + +`scene.py` captures live video from a Pi Camera, crops a configurable ROI (region of interest), and compares each frame against a library of snapshot templates using HSV histogram correlation. A **Moore FSM** (finite state machine) tracks game state transitions and dispatches HID mouse actions (clicks, drags, scrolls) to navigate menus and play quests autonomously. + +### Architecture + +``` +Pi Camera (Picamera2) + | + v + ROI crop + resize (400x800) + | + v + HSV histogram matching <-- snapshots/*.jpg templates + | (full-frame + sub-region) + v + Moore FSM (state machine) + | + v + HID action dispatch --> HTTP API --> SFU --> USB HID mouse + | + v + OpenCV display (live view + score bar chart) +``` + +## Requirements + +- **Hardware**: Raspberry Pi (tested on RPi 5) with Pi Camera module +- **Python 3.10+** +- **Dependencies**: + - `opencv-python` (`cv2`) + - `numpy` + - `picamera2` + - `requests` + +## Files + +| File | Description | +|------|-------------| +| `scene.py` | Main entry point: camera capture, scene detection, FSM, and display loop | +| `common.py` | USB HID mouse control via HTTP API: low-level send, click, drag, calibration | +| `snapshots/` | Template images for each scene (`_.jpg`) | + +## Usage + +```bash +# Display-only mode (no HID control) +python scene.py + +# With HID control enabled +python scene.py --device-id --hid-w --hid-h + +# Or via environment variables +DEVICE_ID=abc123 HID_W=400 HID_H=800 python scene.py +``` + +### Keyboard Shortcuts + +| Key | Action | +|-----|--------| +| `q` | Quit | +| `s` | Save current ROI frame as a snapshot template | +| `m` | Send a mouse click at the current cursor position | + +## Scene Detection + +Each template image is stored as `snapshots/_.jpg`. On startup, all templates are loaded and their HSV histograms are pre-computed. + +Detection runs at **1 Hz** and uses two layers of comparison: + +1. **Full-frame histogram** - `cv2.compareHist` with `HISTCMP_CORREL` against every template +2. **Sub-region histograms** - For scenes defined in `SCENE_REGIONS`, specific UI areas (buttons, bars) are cropped and compared independently. Region scores are blended with the base score to boost or penalise the match. + +A `SCENE_CUSTOMS` mechanism provides differential weighting for rival scenes (e.g., `quest` vs `event`) that share similar layouts but differ in which button is highlighted. + +## FSM (Finite State Machine) + +The Moore FSM enforces valid game-flow transitions: + +``` +UNKNOWN -> HOME -> EVENT/QUEST -> NORMAL-QUEST -> NORMAL-QUEST-UIJIN + -> NORMAL-QUEST-UIJIN-KARYU -> HELPER-SELECT -> DECK-SELECT + -> IN-PLAY -> CLEAR-OK -> SPECIAL-REWARD -> REWARD-NEXT -> HOME (loop) +``` + +Key properties: + +- **Transition guard**: Only transitions listed in `FSM_TRANSITIONS` are allowed +- **Confirmation count**: A candidate state must be detected **3 consecutive times** before the FSM transitions (debouncing) +- **Action dispatch**: On each transition, the corresponding HID action from `FSM_ACTIONS` is enqueued to a background worker thread + +## HID Control (`common.py`) + +Mouse commands are sent as HTTP POST requests to the SFU API: + +``` +POST {SFU_API_BASE}/{device_id}/00/00 +{"type": "mouse", "command": "{op} {dx} {dy}"} +``` + +- `op=0`: move / mouse-up +- `op=1`: mouse-down / drag +- `dx`, `dy`: relative delta in HID units + +Commands are chunked into small deltas (`MAX_DELTA=10`) and sent in random-sized batches for robustness. High-level operations include `click_pct`, `drag`, `long_press`, and cursor calibration. + +## Adding New Scenes + +1. Run `scene.py` and navigate to the target screen in the game +2. Press `s` to save a snapshot +3. Enter a filename in the format `_` (e.g., `my-scene_00`) +4. Add multiple snapshots per scene for robustness (3-5 recommended) +5. Optionally define sub-regions in `SCENE_REGIONS` and add FSM transitions in `FSM_TRANSITIONS` + +## Configuration + +Key constants in `scene.py`: + +| Constant | Default | Description | +|----------|---------|-------------| +| `ROI_X/Y/W/H` | 0.442/0.432/0.126/0.332 | Normalized ROI within camera frame | +| `OUTPUT_W/H` | 400x800 | Resized ROI dimensions for matching | +| `CAP_W/H` | 2028x1520 | Camera capture resolution | +| `MIN_SIMILARITY` | 0.4 | Minimum score threshold | +| `FSM_CONFIRM_COUNT` | 3 | Consecutive detections required for state transition | + +Environment variables: + +| Variable | Description | +|----------|-------------| +| `DEVICE_ID` | SFU device identifier | +| `HID_W` | HID screen width in HID units | +| `HID_H` | HID screen height in HID units | +| `SFU_API_BASE` | SFU API endpoint (default: `http://192.168.124.45:8888/api/message`) | diff --git a/examples/hid-usb/scene-detect/common.py b/examples/hid-usb/scene-detect/common.py new file mode 100644 index 000000000..ef2760057 --- /dev/null +++ b/examples/hid-usb/scene-detect/common.py @@ -0,0 +1,567 @@ +"""Monster Strike auto-test: shared utilities. + +USB HID mouse control via HTTP API. +Command format: "{op} {x} {y}" + op=0: move / mouse-up + op=1: mouse-down / drag + x, y: relative delta (HID units, NOT pixels) + +Note: HID delta ~100 moves the cursor roughly 1/4 of the screen. + Full screen ≈ 400 HID units (varies by OS mouse acceleration). +""" + +import os +import requests +import time +import math +import random + +# --------------- config --------------- +API_BASE = os.environ.get("SFU_API_BASE", "http://192.168.124.45:8888/api/message") +_device_id = "" # set by init(device_id) +MOVE_DELAY = 0.13 # inter-report delay (s) +CLICK_HOLD = 0.15 # mouse-down duration (s) +UI_WAIT = 1.0 # wait for UI transition (s) +SCAN_STEP = 10 # calibration scan step (px) +MAX_DELTA = 10 # max delta per-report (both dx, dy) + +# --------------- state --------------- +screen_w = 0 # screen width (HID units) +screen_h = 0 # screen height (HID units) +_cx = 0 # tracked cursor x (HID units) +_cy = 0 # tracked cursor y (HID units) +_seq = 0 # packet sequence number (IPS/IDS evasion) + +RESET_SWEEP = 500 # HID units to guarantee reaching any corner + +# --------------- batch config (IPS/IDS evasion) --------------- +BATCH_MIN = 6 # min commands per batch +BATCH_MAX = 14 # max commands per batch +PAD_MIN = 0 # min random padding length +PAD_MAX = 32 # max random padding length + + +# ============================================================ +# Low-level API +# ============================================================ + +def init(device_id: str): + """Set device ID (must be called before any mouse operation).""" + global _device_id + _device_id = device_id + print(f"[common] device_id = {device_id}") + + +def _api_url() -> str: + return f"{API_BASE}/{_device_id}/00/00" + + +def send(op: int, x: int, y: int, delay: float = MOVE_DELAY): + """Send a single mouse report.""" + global _seq + _seq += 1 + url = _api_url() + payload = {"type": "mouse", "command": f"{op} {x} {y}", "seq": f"{_seq}"} + print(f" [API] POST {url} {payload}") + resp = requests.post(url, json=payload) + print(f" [API] -> {resp.status_code}") + if delay > 0: + time.sleep(delay) + + +def _send_batch(commands: list): + """Send a batch of commands in one packet with random padding.""" + pad = "x" * random.randint(PAD_MIN, PAD_MAX) + payload = {"type": "mouse", "commands": commands, "p": pad} + url = _api_url() + print(f" [API] POST {url} batch={len(commands)} pad={len(pad)}") + resp = requests.post(url, json=payload) + print(f" [API] -> {resp.status_code}") + + +def _chunked_move(op: int, dx: int, dy: int): + """Move in MAX_DELTA chunks, sent as random-sized batches.""" + global _cx, _cy + print(f"_chunked_move: {dx}, {dy}\n") + + # Collect all chunks first + chunks = [] + while dx != 0 or dy != 0: + sx = max(-MAX_DELTA, min(MAX_DELTA, dx)) + sy = max(-MAX_DELTA, min(MAX_DELTA, dy)) + chunks.append(f"{op} {sx} {sy}") + dx -= sx + dy -= sy + _cx += sx + _cy += sy + + # Send in random-sized batches + i = 0 + while i < len(chunks): + n = random.randint(BATCH_MIN, BATCH_MAX) + batch = chunks[i:i + n] + _send_batch(batch) + i += len(batch) + time.sleep(MOVE_DELAY * len(batch)) + + + + +# ============================================================ +# High-level operations +# ============================================================ + +def reset_origin(): + """Cursor to top-left (0,0) via large negative deltas.""" + global _cx, _cy + steps = RESET_SWEEP // 100 + 1 # e.g. 500/100+1 = 6 + cmds = [f"0 -100 -100" for _ in range(steps)] + _send_batch(cmds) + time.sleep(0.01 * steps) + _cx = 0 + _cy = 0 + + +def reset_origin_visual(): + """Cursor to top-left with visual feedback. + + First sweep to bottom-right so the user can see the cursor moving, + then sweep far left/up to guarantee reaching (0,0). + """ + global _cx, _cy + step = 20 + n_left = RESET_SWEEP // step + 1 # → top-left + print(f"[reset_origin_visual] → top-left origin ({n_left} steps) ...") + + # Collect all commands, send in random batches + cmds = [f"0 {-step} {-step}" for _ in range(n_left)] + i = 0 + while i < len(cmds): + n = random.randint(BATCH_MIN, BATCH_MAX) + batch = cmds[i:i + n] + _send_batch(batch) + i += len(batch) + time.sleep(MOVE_DELAY * len(batch)) + + _cx = 0 + _cy = 0 + print("[reset_origin_visual] done") + + +def move_to(x: int, y: int): + """Move cursor to absolute position (from origin).""" + print(f"move_to : {_cx} , {x}, {_cy} , {y}") + _chunked_move(0, x - _cx, y - _cy) + + +def click(x: int, y: int, repeat: int = 1, interval: float = 0.3): + """Tap at absolute position. + + repeat: number of clicks + interval: delay between clicks (s) + """ + print(f"click {x} {y} (x{repeat})") + move_to(x, y) + for i in range(repeat): + send(1, 0, 0, delay=CLICK_HOLD) # mouse-down + send(0, 0, 0, delay=0.1) # mouse-up + if i < repeat - 1: + time.sleep(interval) + + +def click_pct(rx: float, ry: float, repeat: int = 1, interval: float = 0.3): + """Tap at relative position (0.0-1.0 of screen).""" + click(int(screen_w * rx), int(screen_h * ry), repeat=repeat, interval=interval) + + +def long_press(x: int, y: int, duration: float = 1.0): + """Long press at absolute position.""" + move_to(x, y) + send(1, 0, 0, delay=duration) + send(0, 0, 0, delay=0.1) + + +def drag(x1, y1, x2, y2, steps: int = 20): + """Drag from (x1,y1) to (x2,y2) with smooth interpolation.""" + move_to(x1, y1) + send(1, 0, 0, delay=0.1) # mouse-down + + # Collect intermediate drag commands + dx = x2 - x1 + dy = y2 - y1 + cmds = [] + for i in range(1, steps + 1): + sx = (dx * i // steps) - (dx * (i - 1) // steps) + sy = (dy * i // steps) - (dy * (i - 1) // steps) + cmds.append(f"1 {sx} {sy}") + + # Send in random batches + i = 0 + while i < len(cmds): + n = random.randint(BATCH_MIN, BATCH_MAX) + batch = cmds[i:i + n] + _send_batch(batch) + i += len(batch) + time.sleep(0.02 * len(batch)) + + global _cx, _cy + _cx = x2 + _cy = y2 + time.sleep(0.1) + send(0, 0, 0, delay=0.1) # mouse-up + + +def wait(sec: float): + """Sleep with log.""" + print(f" wait {sec:.1f}s") + time.sleep(sec) + + +# ============================================================ +# Screen capture / CV (stub -- implement per environment) +# ============================================================ + +def capture_screen(): + """Capture current screen. + Returns: numpy ndarray (BGR) or None. + TODO: implement via WebRTC video frame, adb screencap, etc. + """ + return None + + +def detect_hamburger_menu(img) -> bool: + """Return True if hamburger-menu overlay is visible. + TODO: implement with cv2.matchTemplate or similar. + """ + if img is None: + return False + # Example: + # import cv2, numpy as np + # tmpl = cv2.imread("templates/hamburger_menu.png") + # res = cv2.matchTemplate(img, tmpl, cv2.TM_CCOEFF_NORMED) + # return res.max() > 0.8 + return False + + +def detect_home_screen(img) -> bool: + """Return True if Monster Strike home screen is visible.""" + if img is None: + return False + return False + + +# ============================================================ +# Calibration +# ============================================================ + +def calibrate(manual_size: tuple = None): + """Detect screen size in HID units. + + If manual_size is given as (w, h) in HID units, skip auto-detection. + Otherwise: scan from origin with step+click until hamburger + menu is detected by CV. + """ + global screen_w, screen_h, _cx, _cy + + if manual_size: + screen_w, screen_h = manual_size + print(f"[calibrate] manual: {screen_w}x{screen_h}") + reset_origin_visual() + return True + + print("[calibrate] reset to origin ...") + reset_origin_visual() + + print(f"[calibrate] scanning (step={SCAN_STEP}) ...") + max_steps = 500 # safety limit + for step in range(1, max_steps + 1): + send(0, SCAN_STEP, SCAN_STEP) + _cx += SCAN_STEP + _cy += SCAN_STEP + + # click at current position + send(1, 0, 0, delay=CLICK_HOLD) + send(0, 0, 0, delay=0.3) + + img = capture_screen() + if detect_hamburger_menu(img): + # hamburger icon is near bottom-right corner + screen_w = int(_cx / 0.95) + screen_h = int(_cy / 0.97) + print(f"[calibrate] detected: {screen_w}x{screen_h}") + + # close menu + reset_origin() + click(screen_w // 2, screen_h // 4) + wait(1.0) + return True + + if step % 50 == 0: + print(f" step {step} pos=({_cx},{_cy})") + + print("[calibrate] FAILED") + return False + + +# ============================================================ +# Actions (callable from CLI) +# ============================================================ + +ACTIONS = {} + + +def action(name): + """Decorator to register a CLI action.""" + def _reg(fn): + ACTIONS[name] = fn + return fn + return _reg + + +@action("calibrate") +def act_calibrate(args): + """Run calibration. [hid_w hid_h]""" + manual = None + if len(args) == 2: + manual = (int(args[0]), int(args[1])) + calibrate(manual_size=manual) + + +@action("quest_bt_click") +def act_quest_bt_click(args): + """Click Quest button. """ + if len(args) < 2: + print("quest_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap クエスト (0.50, 0.85) x5") + click_pct(0.50, 0.85, repeat=1) + + +@action("normal_bt_click") +def act_normal_bt_click(args): + """Click ノーマル button. """ + if len(args) < 2: + print("normal_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap ノーマル (0.50, 0.50)") + click_pct(0.50, 0.50, repeat=1) + + +@action("normal_ikusei_bt_click") +def act_normal_ikusei_bt_click(args): + """Click ノーマル育成 button. """ + if len(args) < 2: + print("normal_ikusei_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap ノーマル育成 (0.27, 0.72)") + click_pct(0.27, 0.72, repeat=1) + + +@action("shojin_bt_click") +def act_shojin_bt_click(args): + """Scroll down to bottom & click 初陣. """ + if len(args) < 2: + print("shojin_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + + # scroll down to bottom + for s in range(1, 4): + reset_origin() + print(f"[action] scroll down.. {s}") + x1 = int(screen_w * 0.50) + y1 = int(screen_h * 0.90) + x2 = int(screen_w * 0.50) + y2 = int(screen_h * 0.30) + drag(x1, y1, x2, y2) + wait(1.0) + + # tap 初陣 + reset_origin() + print("[action] tap 初陣 (0.50, 0.90)") + click_pct(0.50, 0.90, repeat=1) + + +@action("karyu_bt_click") +def act_karyu_bt_click(args): + """Click 火竜の試練 button. """ + if len(args) < 2: + print("karyu_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap 火竜の試練 (0.50, 0.60)") + click_pct(0.50, 0.60, repeat=1) + + +@action("solo_bt_click") +def act_solo_bt_click(args): + """Click ソロ button. """ + if len(args) < 2: + print("solo_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap ソロ (0.25, 0.60)") + click_pct(0.25, 0.60, repeat=1) + + +@action("helper_select") +def act_helper_select(args): + """Scroll to みんなのクリアモンスター & select. """ + if len(args) < 2: + print("helper_select requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + + for step in range(1, 4): + reset_origin() + print(f"[action] scroll up, reset.. {step}") + x1 = int(screen_w * 0.50) + y1 = int(screen_h * 0.30) + x2 = int(screen_w * 0.50) + y2 = int(screen_h * 0.90) + drag(x1, y1, x2, y2) + wait(1.0) + + reset_origin() + + # scroll down: drag (0.5, 0.5) → (0.5, 0.2) + x1 = int(screen_w * 0.50) + y1 = int(screen_h * 0.60) + x2 = int(screen_w * 0.50) + y2 = int(screen_h * 0.20) + print(f"[action] scroll down: drag ({x1},{y1}) → ({x2},{y2})") + drag(x1, y1, x2, y2) + wait(1.0) + # select helper + reset_origin() + print("[action] tap helper (0.50, 0.46)") + click_pct(0.50, 0.46, repeat=1) + + +@action("shutsugeki_bt_click") +def act_shutsugeki_bt_click(args): + """Click 出撃 button (deck screen). """ + if len(args) < 2: + print("shutsugeki_bt_click requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap 出撃 (0.50, 0.70)") + click_pct(0.50, 0.70, repeat=1) + + +@action("play_turn") +def act_play_turn(args): + """Play 1 turn (random flick from center). """ + if len(args) < 2: + print("play_turn requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + + # random angle: 30deg increments (0, 30, 60, ... 330) + angle_deg = random.choice(range(0, 360, 30)) + angle_rad = math.radians(angle_deg) + # random strength: 100-200 HID units + strength = random.randint(100, 200) + # random hold time: 2-4 seconds + hold_sec = random.uniform(2.0, 4.0) + + dx = int(strength * math.cos(angle_rad)) + dy = int(strength * math.sin(angle_rad)) + + cx = int(screen_w * 0.50) + cy = int(screen_h * 0.50) + + print(f"[action] play_turn: angle={angle_deg}deg strength={strength} " + f"hold={hold_sec:.1f}s drag ({cx},{cy})→({cx+dx},{cy+dy})") + + # move to center + move_to(cx, cy) + # mouse down + send(1, 0, 0, delay=0.1) + # drag to target + _chunked_move(1, dx, dy) + # hold (aiming) + print(f" holding {hold_sec:.1f}s ...") + time.sleep(hold_sec) + # release + send(0, 0, 0, delay=0.1) + print("[action] released") + + +@action("clear_ok") +def act_clear_ok(args): + """Click OK on quest clear dialog. """ + if len(args) < 2: + print("clear_ok requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap OK (0.50, 0.65)") + click_pct(0.50, 0.65, repeat=1) + wait(1.0) + reset_origin() + print("[action] tap helper (0.50, 0.75)") + click_pct(0.50, 0.78, repeat=1) + + + +@action("special_reward") +def act_special_reward(args): + """Click special reward (center). """ + if len(args) < 2: + print("special_reward requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap center (0.50, 0.50)") + click_pct(0.50, 0.50, repeat=1) + + +@action("reward_next") +def act_reward_next(args): + """Click reward page 2. """ + if len(args) < 2: + print("reward_next requires: ") + return + calibrate(manual_size=(int(args[0]), int(args[1]))) + reset_origin() + print("[action] tap reward next (0.50, 1.1)") + click_pct(0.50, 0.999, repeat=1) + + +if __name__ == "__main__": + import sys + + usage = ( + "Usage: python common.py [args...]\n" + "\n" + "Actions:\n" + ) + for name, fn in ACTIONS.items(): + usage += f" {name:20s} {fn.__doc__ or ''}\n" + + if len(sys.argv) < 3: + print(usage) + sys.exit(1) + + dev = sys.argv[1] + cmd = sys.argv[2] + rest = sys.argv[3:] + + if cmd not in ACTIONS: + print(f"Unknown action: {cmd}\n") + print(usage) + sys.exit(1) + + init(dev) + ACTIONS[cmd](rest) diff --git a/examples/hid-usb/scene-detect/scene.py b/examples/hid-usb/scene-detect/scene.py new file mode 100644 index 000000000..d2d6aa56f --- /dev/null +++ b/examples/hid-usb/scene-detect/scene.py @@ -0,0 +1,783 @@ +#!/usr/bin/env python3 +"""Monster Strike scene detector - camera capture base.""" + +import argparse +import glob +import math +import os +import queue +import threading +import time + +import cv2 +import numpy as np +from picamera2 import Picamera2 + +import common as hid + +SAVE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "snapshots") + +# --- ROI config (matches: rpicam-still --roi 0.40,0.15,0.20,0.55) --- +ROI_X = 0.442 # left offset (normalized) +ROI_Y = 0.432 # top offset (normalized) +ROI_W = 0.126 # width (normalized) +ROI_H = 0.332 # height (normalized) + +OUTPUT_W = 400 +OUTPUT_H = 800 + +# Camera capture resolution (full sensor crop source) +CAP_W = 2028 +CAP_H = 1520 + +# Bar chart panel +CHART_W = 200 +BAR_H = 28 +BAR_GAP = 6 +BAR_MAX_W = 80 # max bar length in pixels + +# --- Scene-specific sub-region hints (normalized within OUTPUT frame) --- +# Each entry: scene_name -> [(x, y, w, h), ...] all normalised 0..1 +SCENE_REGIONS = { + + "home": [ + (0.01, 0.75, 0.32, 0.07), # quest button area.0 + (0.37, 0.73, 0.26, 0.10), # quest button area.1 + (0.66, 0.75, 0.32, 0.07), # quest button area.2 + (0.01, 0.91, 0.98, 0.07), # home bar + ], + "event": [ + (0.19, 0.59, 0.15, 0.08), # normal quest + (0.39, 0.58, 0.24, 0.12), # event quest + (0.68, 0.59, 0.15, 0.08), # charange quest + (0.01, 0.91, 0.98, 0.07), # home bar + ], + "quest": [ + (0.16, 0.57, 0.23, 0.12), # normal quest + (0.44, 0.60, 0.15, 0.08), # event quest + (0.68, 0.59, 0.15, 0.08), # charange quest + (0.01, 0.91, 0.98, 0.07), # home bar + ], + "normal-quest-uijin-karyu": [ + (0.1, 0.47, 0.35, 0.20), + (0.52, 0.47, 0.35, 0.20), + (0.01, 0.91, 0.98, 0.07), # home bar + ], + "normal-quest": [ + (0.02, 0.12, 0.59, 0.045), + (0.04, 0.22, 0.73, 0.07), + (0.04, 0.352, 0.73, 0.07), + (0.04, 0.482, 0.73, 0.07), + (0.04, 0.612, 0.73, 0.07), + (0.04, 0.742, 0.73, 0.07), + (0.01, 0.91, 0.98, 0.07), # home bar + ], + "normal-quest-uijin": [ + (0.02, 0.12, 0.59, 0.045), + (0.04, 0.204, 0.73, 0.07), + (0.08, 0.312, 0.71, 0.07), + (0.08, 0.408, 0.71, 0.07), + (0.08, 0.504, 0.71, 0.07), + (0.01, 0.91, 0.98, 0.07), # home bar + ], + "helper-select": [ + (0.02, 0.12, 0.46, 0.045), + (0.14, 0.17, 0.78, 0.065), + (0.01, 0.91, 0.98, 0.07), # home bar + ], + "deck-select": [ + (0.02, 0.12, 0.46, 0.045), + (0.06, 0.36, 0.82, 0.198), + (0.01, 0.91, 0.98, 0.07), # home bar + ], + "special-reward": [ + (0.18, 0.00, 0.70, 0.044), + ], + + "reward-next": [ + (0.01, 0.91, 0.98, 0.07), # ok button + ], + +} + +# --- Custom differential for disambiguating similar scenes --- +# key_region: the button index that should be dominant for this scene +# other_regions: the other button indices to compare against (avg) +# Rule: if r[key] > avg(r[others]) → boost, otherwise → penalise +SCENE_CUSTOMS = { + "quest": { + "rival": "event", + "key_region": 0, # r0 (left button) biggest → quest + "other_regions": [1, 2], + "weight": 0.5, + }, + "event": { + "rival": "quest", + "key_region": 1, # r1 (middle button) biggest → event + "other_regions": [0, 2], + "weight": 0.5, + }, +} + + +# --------------- Scene templates --------------- + +def calc_hist(img): + """Compute normalised HSV histogram for an image.""" + hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) + hist = cv2.calcHist([hsv], [0, 1], None, [32, 32], [0, 180, 0, 256]) + cv2.normalize(hist, hist) + return hist + + +def crop_region(img, region): + """Crop a normalised (x, y, w, h) region from img.""" + h, w = img.shape[:2] + rx, ry, rw, rh = region + x1 = int(w * rx) + y1 = int(h * ry) + x2 = int(w * (rx + rw)) + y2 = int(h * (ry + rh)) + return img[y1:y2, x1:x2] + + +def load_templates(snapshot_dir): + """Load template images grouped by scene name. + + Returns: + templates_hist: dict of scene_name -> [hist, ...] (full-frame) + templates_region: dict of scene_name -> [[hist, ...], ...] (sub-region) + """ + raw = {} + for path in sorted(glob.glob(os.path.join(snapshot_dir, "*.jpg"))): + basename = os.path.splitext(os.path.basename(path))[0] + parts = basename.rsplit("_", 1) + if len(parts) == 2 and parts[1].isdigit(): + scene_name = parts[0] + else: + continue + img = cv2.imread(path) + if img is None: + continue + raw.setdefault(scene_name, []).append(img) + + templates_hist = {} + templates_region = {} + for name, imgs in raw.items(): + templates_hist[name] = [calc_hist(img) for img in imgs] + if name in SCENE_REGIONS: + region_hists = [] + for region in SCENE_REGIONS[name]: + region_hists.append([calc_hist(crop_region(img, region)) for img in imgs]) + templates_region[name] = region_hists + n_regions = len(SCENE_REGIONS.get(name, [])) + print(f" template: {name:35s} x{len(imgs)}" + f"{' +' + str(n_regions) + ' regions' if n_regions else ''}") + return templates_hist, templates_region + + +def scene_similarity(frame, templates_hist, templates_region): + """Compare frame against all scene templates. + + Full-frame histogram + sub-region boost for scenes with defined regions. + When rival scenes (SCENE_CUSTOMS) both score high, apply differential + region weights to disambiguate. + Returns list of (scene_name, combined_score) sorted descending. + """ + frame_hist = calc_hist(frame) + score_map = {} + region_map = {} + for name, hists in templates_hist.items(): + base = max(cv2.compareHist(frame_hist, h, cv2.HISTCMP_CORREL) for h in hists) + + if name in templates_region: + region_scores = [] + for ri, region in enumerate(SCENE_REGIONS[name]): + frame_crop = crop_region(frame, region) + frame_crop_hist = calc_hist(frame_crop) + rs = max( + cv2.compareHist(frame_crop_hist, h, cv2.HISTCMP_CORREL) + for h in templates_region[name][ri] + ) + region_scores.append(rs) + region_map[name] = region_scores + # Compare avg of regions (excluding last = home bar) vs base + content_regions = region_scores[:-1] if len(region_scores) > 1 else region_scores + avg_content = sum(content_regions) / len(content_regions) + diff = avg_content - base + if diff >= 0: + # Positive: regions match better than base → boost + score = 0.15 * base + 0.85 * avg_content + else: + # Negative: regions match worse than base → penalise (log curve) + score = base - 0.70 * math.log(1.0 + abs(diff)) + else: + score = base + + score_map[name] = score + + # Apply SCENE_CUSTOMS: boost scene whose key_region > avg(other_regions) + for name, custom in SCENE_CUSTOMS.items(): + rival = custom["rival"] + if name not in score_map or rival not in score_map: + continue + if name not in region_map: + continue + rs = region_map[name] + key_idx = custom["key_region"] + other_idxs = custom["other_regions"] + key_score = rs[key_idx] + other_avg = sum(rs[i] for i in other_idxs) / len(other_idxs) + diff = key_score - other_avg + before = score_map[name] + delta = custom["weight"] * diff + score_map[name] += delta + # print(f" CUSTOM[{name}] r{key_idx}={key_score:.3f} " + # f"avg({','.join(f'r{i}' for i in other_idxs)})={other_avg:.3f} " + # f"diff={diff:+.4f} delta={delta:+.4f} " + # f"{before:.4f}->{score_map[name]:.4f}") + + results = sorted(score_map.items(), key=lambda x: x[1], reverse=True) + return results + + +# --------------- Moore FSM --------------- + +# States +S_UNKNOWN = "UNKNOWN" +S_HOME = "HOME" +S_QUEST = "QUEST" +S_NORMAL_QUEST = "NORMAL-QUEST" +S_NORMAL_QUEST_UIJIN = "NORMAL-QUEST-UIJIN" +S_NORMAL_QUEST_UIJIN_KARYU = "NORMAL-QUEST-UIJIN-KARYU" +S_HELPER_SELECT = "HELPER-SELECT" +S_DECK_SELECT = "DECK-SELECT" +S_IN_PLAY = "NORMAL-QUEST-UIJIN-IN-PLAY" +S_CLEAR_OK = "CLEAR-OK" +S_SPECIAL_REWARD = "SPECIAL-REWARD" +S_EVENT = "EVENT" +S_REWARD_NEXT = "REWARD-NEXT" + +# --- FSM transition table --- +# state -> [allowed next states] (game flow order) +FSM_TRANSITIONS = { + S_UNKNOWN: [S_HOME, S_EVENT, S_QUEST, S_NORMAL_QUEST, S_NORMAL_QUEST_UIJIN, + S_NORMAL_QUEST_UIJIN_KARYU, S_HELPER_SELECT, S_DECK_SELECT, + S_IN_PLAY, S_CLEAR_OK, S_SPECIAL_REWARD, S_REWARD_NEXT], + S_HOME: [S_EVENT, S_QUEST, S_NORMAL_QUEST_UIJIN], + S_EVENT: [S_QUEST, S_NORMAL_QUEST_UIJIN, S_HOME], + S_QUEST: [S_NORMAL_QUEST, S_HOME], + S_NORMAL_QUEST: [S_NORMAL_QUEST_UIJIN, S_QUEST, S_HOME], + S_NORMAL_QUEST_UIJIN: [S_NORMAL_QUEST_UIJIN_KARYU, S_NORMAL_QUEST, S_HOME], + S_NORMAL_QUEST_UIJIN_KARYU:[S_HELPER_SELECT, S_HOME], + S_HELPER_SELECT: [S_DECK_SELECT, S_HOME], + S_DECK_SELECT: [S_IN_PLAY, S_HOME], + S_IN_PLAY: [S_CLEAR_OK], + S_CLEAR_OK: [S_SPECIAL_REWARD, S_REWARD_NEXT, S_HOME], + S_SPECIAL_REWARD: [S_REWARD_NEXT], + S_REWARD_NEXT: [S_HOME], +} + + +# --- FSM state -> HID action mapping --- +FSM_ACTIONS = { + S_HOME: "quest_bt_click", + S_EVENT: "normal_ikusei_bt_click", + S_QUEST: "normal_bt_click", + S_NORMAL_QUEST: "shojin_bt_click", + S_NORMAL_QUEST_UIJIN: "karyu_bt_click", + S_NORMAL_QUEST_UIJIN_KARYU:"solo_bt_click", + S_HELPER_SELECT: "helper_select", + S_DECK_SELECT: "shutsugeki_bt_click", + S_IN_PLAY: "play_turn", + S_CLEAR_OK: "clear_ok", + S_SPECIAL_REWARD: "special_reward", + S_REWARD_NEXT: "reward_next", +} + +_action_queue = queue.Queue() +_worker_idle = threading.Event() +_worker_idle.set() + + +def _action_worker(): + """Worker thread: consume actions from FIFO queue sequentially.""" + while True: + action_name, hid_args = _action_queue.get() + _worker_idle.clear() + print(f" [HID] dispatching: {action_name} (queue size: {_action_queue.qsize()})") + try: + hid.ACTIONS[action_name](hid_args) + print(f" [HID] done: {action_name}") + except Exception as e: + print(f" [HID] error in {action_name}: {e}") + _action_queue.task_done() + if _action_queue.empty(): + _worker_idle.set() + + +threading.Thread(target=_action_worker, daemon=True).start() + + +def dispatch_action(action_name, hid_args): + """Enqueue an HID action for sequential execution.""" + print(f" [HID] enqueue: {action_name} (queue size: {_action_queue.qsize()})") + _action_queue.put((action_name, hid_args)) + + +def dispatch_if_idle(action_name, hid_args): + """Dispatch only if worker is idle. Skip otherwise.""" + if _worker_idle.is_set(): + print(f" [HID] enqueue (idle): {action_name}") + _action_queue.put((action_name, hid_args)) + else: + print(f" [HID] skip (busy): {action_name}") + + +def _score_of(scores, name): + """Return score for a scene name, or -1 if not found.""" + for n, s in scores: + if n == name: + return s + return -1.0 + + +def _top_names(scores, n): + """Return the top-n scene names in order.""" + return [name for name, _ in scores[:n]] + + +def _evaluate_state(scores): + """Determine which state the scores indicate, ignoring transitions.""" + if not scores: + return S_UNKNOWN + + top_name, top_score = scores[0] + + # Guard: nothing looks similar enough + if top_score < MIN_SIMILARITY: + return S_UNKNOWN + + names = _top_names(scores, 3) + + # HOME stable + if (top_name == "home" and top_score >= 0.8 + and len(names) >= 3 + and names[1] == "clear-ok" + and names[2] in ("helper-select", "deck-select")): + return S_HOME + + # EVENT stable + if (top_name == "event" and top_score >= 0.8 + and len(names) >= 2 + and names[1] == "quest"): + return S_EVENT + + # QUEST stable + if (top_name == "quest" and top_score >= 0.8 + and len(names) >= 2 + and names[1] == "event"): + return S_QUEST + + # NORMAL-QUEST stable + if (top_name == "normal-quest" and top_score >= 0.8 + and _score_of(scores, "normal-quest-uijin") >= 0.7 + and len(names) >= 2 + and names[1] == "normal-quest-uijin"): + return S_NORMAL_QUEST + + # NORMAL-QUEST-UIJIN stable + if (top_name == "normal-quest-uijin" and top_score >= 0.8 + and _score_of(scores, "normal-quest") >= 0.5 + and _score_of(scores, "deck-select") >= 0.5 + and _score_of(scores, "event") >= 0.45 + and _score_of(scores, "quest") >= 0.45 + and len(names) >= 2 + and names[1] in ("normal-quest", "event")): + return S_NORMAL_QUEST_UIJIN + + # NORMAL-QUEST-UIJIN-KARYU stable + if (top_name == "normal-quest-uijin-karyu" and top_score >= 0.7 + and (_score_of(scores, "helper-select") >= 0.5 + or _score_of(scores, "deck-select") >= 0.5 + or _score_of(scores, "normal-quest") >= 0.6) + and len(names) >= 2 + and names[1] in ("helper-select", "deck-select", "normal-quest", "normal-quest-uijin")): + return S_NORMAL_QUEST_UIJIN_KARYU + + # HELPER-SELECT stable + if (top_name == "helper-select" and top_score >= 0.8 + and _score_of(scores, "clear-ok") >= 0.6 + and _score_of(scores, "deck-select") >= 0.6 + and len(names) >= 2 + and names[1] in ("clear-ok", "deck-select")): + return S_HELPER_SELECT + + # DECK-SELECT stable + if (top_name == "deck-select" and top_score >= 0.8 + and (_score_of(scores, "event") >= 0.6 + or _score_of(scores, "quest") >= 0.6) + and len(names) >= 2 + and names[1] in ("event", "quest")): + return S_DECK_SELECT + + # NORMAL-QUEST-UIJIN-IN-PLAY stable + if (top_name == "normal-quest-uijin-in-play" and top_score >= 0.6 + and sum(1 for n, s in scores[1:] if s <= 0.2) >= 8): + return S_IN_PLAY + + # CLEAR-OK stable + if top_name == "clear-ok" and top_score >= 0.8: + return S_CLEAR_OK + + # SPECIAL-REWARD stable + if (top_name == "special-reward" and top_score >= 0.6 + and (_score_of(scores, "reward-next") >= 0.3 + and all(s <= 0.2 for n, s in scores + if n not in ("special-reward", "reward-next"))) + or (all(s <= 0.2 for n, s in scores + if n not in ("special-reward")))): + return S_SPECIAL_REWARD + + # REWARD-NEXT stable + if (top_name == "reward-next" and top_score >= 0.6 + and _score_of(scores, "special-reward") < 0.6 + and all(s < 0.3 for n, s in scores + if n not in ("reward-next", "special-reward"))): + return S_REWARD_NEXT + + return S_UNKNOWN + + +MIN_SIMILARITY = 0.4 # below this, top match is treated as UNKNOWN + +_fsm_pending = None # candidate state awaiting confirmation +_fsm_pending_count = 0 # consecutive times candidate has been seen +FSM_CONFIRM_COUNT = 3 # required consecutive hits before transition + + +def fsm_update(state, scores): + """Evaluate Moore FSM transition based on current scores. + + Only transitions defined in FSM_TRANSITIONS are allowed. + Requires FSM_CONFIRM_COUNT consecutive detections of the same + candidate before actually transitioning. + Returns (new_state, changed). + """ + global _fsm_pending, _fsm_pending_count + + candidate = _evaluate_state(scores) + if state == S_QUEST and candidate == S_NORMAL_QUEST_UIJIN: + candidate = S_NORMAL_QUEST + # candidate が現在と異なる場合だけログ出力 + if candidate != state: + print(f" [FSM] candidate={candidate} ({_fsm_pending_count}/{FSM_CONFIRM_COUNT})") + if candidate == state: + _fsm_pending = None + _fsm_pending_count = 0 + return state, False + + allowed = FSM_TRANSITIONS.get(state, []) + if candidate in allowed: + if _fsm_pending == candidate: + _fsm_pending_count += 1 + else: + _fsm_pending = candidate + _fsm_pending_count = 1 + if _fsm_pending_count >= FSM_CONFIRM_COUNT: + _fsm_pending = None + _fsm_pending_count = 0 + return candidate, True + return state, False + + # candidate not allowed — reset pending + print(f" [FSM] BLOCKED: {candidate} (allowed={allowed})") + _fsm_pending = None + _fsm_pending_count = 0 + return state, False + + +# --------------- Bar chart drawing --------------- + +def draw_chart(scores, chart_w, chart_h): + """Draw horizontal bar chart. Returns BGR image.""" + panel = np.zeros((chart_h, chart_w, 3), dtype=np.uint8) + panel[:] = (30, 30, 30) + + label_x = 8 + bar_x0 = 105 + y = 10 + + for i, (name, score) in enumerate(scores): + cy = y + BAR_H // 2 + + if i == 0: + colour = (0, 220, 100) + elif score < 0: + colour = (60, 60, 180) + else: + colour = (160, 160, 160) + + short = name if len(name) <= 14 else name[:13] + ".." + cv2.putText(panel, short, (label_x, cy + 5), + cv2.FONT_HERSHEY_SIMPLEX, 0.33, (200, 200, 200), 1) + + bar_w = int(max(score, 0.0) * BAR_MAX_W) + if bar_w > 0: + cv2.rectangle(panel, (bar_x0, y + 2), (bar_x0 + bar_w, y + BAR_H - 2), + colour, -1) + + cv2.putText(panel, f"{score:.2f}", (bar_x0 + bar_w + 4, cy + 5), + cv2.FONT_HERSHEY_SIMPLEX, 0.38, colour, 1) + + y += BAR_H + BAR_GAP + + return panel + + +TOAST_DURATION = 3.0 # seconds before fully faded + + +def draw_toast(display, text, elapsed): + """Draw a centred toast message with fade-out. elapsed = time since toast fired.""" + if elapsed >= TOAST_DURATION: + return + alpha = 1.0 - (elapsed / TOAST_DURATION) + h, w = display.shape[:2] + font = cv2.FONT_HERSHEY_SIMPLEX + scale = 1.0 + thickness = 3 + (tw, th), baseline = cv2.getTextSize(text, font, scale, thickness) + tx = (w - tw) // 2 + ty = (h + th) // 2 + # Semi-transparent background + pad = 12 + overlay = display.copy() + cv2.rectangle(overlay, + (tx - pad, ty - th - pad), + (tx + tw + pad, ty + baseline + pad), + (0, 0, 0), -1) + cv2.addWeighted(overlay, alpha * 0.6, display, 1.0 - alpha * 0.6, 0, display) + # Text + colour = (0, 255, 200) + faded = tuple(int(c * alpha) for c in colour) + cv2.putText(display, text, (tx, ty), font, scale, faded, thickness) + + +def draw_region_boxes(display, fsm_state, alpha=0.5): + """Draw sub-region rectangles with semi-transparent red fill.""" + scene_name = fsm_state.lower() + regions = SCENE_REGIONS.get(scene_name, []) + if not regions: + return + overlay = display.copy() + for rx, ry, rw, rh in regions: + x1 = int(OUTPUT_W * rx) + y1 = int(OUTPUT_H * ry) + x2 = int(OUTPUT_W * (rx + rw)) + y2 = int(OUTPUT_H * (ry + rh)) + cv2.rectangle(overlay, (x1, y1), (x2, y2), (0, 0, 255), -1) + cv2.addWeighted(overlay, alpha, display, 1.0 - alpha, 0, display) + + +def make_roi_rect(frame_h, frame_w): + """Convert normalized ROI to pixel coordinates.""" + x1 = int(frame_w * ROI_X) + y1 = int(frame_h * ROI_Y) + x2 = int(frame_w * (ROI_X + ROI_W)) + y2 = int(frame_h * (ROI_Y + ROI_H)) + return x1, y1, x2, y2 + + +# --------------- Main loop --------------- + +def main(): + parser = argparse.ArgumentParser(description="Monster Strike scene detector") + parser.add_argument("--device-id", + default=os.environ.get("DEVICE_ID", ""), + help="SFU device ID for HID control (env: DEVICE_ID)") + parser.add_argument("--hid-w", type=int, + default=int(os.environ.get("HID_W", "0")), + help="HID screen width (env: HID_W)") + parser.add_argument("--hid-h", type=int, + default=int(os.environ.get("HID_H", "0")), + help="HID screen height (env: HID_H)") + args = parser.parse_args() + + print(f" [DEBUG] device_id='{args.device_id}' hid_w={args.hid_w} hid_h={args.hid_h}") + hid_enabled = bool(args.device_id and args.hid_w and args.hid_h) + hid_args = [str(args.hid_w), str(args.hid_h)] + print(f" [DEBUG] hid_enabled={hid_enabled} hid_args={hid_args}") + if hid_enabled: + hid.init(args.device_id) + print(f"HID control enabled: device={args.device_id} " + f"size={args.hid_w}x{args.hid_h}") + print(f" API: {hid.API_BASE}") + else: + print("HID control disabled " + "(set --device-id --hid-w --hid-h to enable)") + + print("Loading templates...") + templates_hist, templates_region = load_templates(SAVE_DIR) + if not templates_hist: + print(f"WARNING: no templates found in {SAVE_DIR}") + + picam2 = Picamera2() + config = picam2.create_video_configuration( + main={"size": (CAP_W, CAP_H), "format": "RGB888"}, + ) + picam2.configure(config) + picam2.start() + time.sleep(0.5) + + print(f"Capture: {CAP_W}x{CAP_H} ROI: ({ROI_X},{ROI_Y},{ROI_W},{ROI_H}) Output: {OUTPUT_W}x{OUTPUT_H}") + print("Keys: q=quit s=save snapshot m=click at cursor") + + # Track mouse position for 'm' key click + _mouse_pos = [0, 0] + + def on_mouse(event, x, y, flags, param): + if event == cv2.EVENT_MOUSEMOVE: + _mouse_pos[0] = x + _mouse_pos[1] = y + + cv2.namedWindow("Scene Detect") + cv2.setMouseCallback("Scene Detect", on_mouse) + + frame_count = 0 + fps_time = time.monotonic() + last_detect = 0.0 + scores = [(name, 0.0) for name in templates_hist] + fsm_state = S_UNKNOWN + toast_text = "" + toast_time = 0.0 + last_play_turn = 0.0 + PLAY_TURN_INTERVAL = 5.0 + last_reward_next = 0.0 + REWARD_NEXT_INTERVAL = 10.0 + + try: + while True: + frame = picam2.capture_array() + h, w = frame.shape[:2] + + x1, y1, x2, y2 = make_roi_rect(h, w) + roi = frame[y1:y2, x1:x2] + roi_resized = cv2.resize(roi, (OUTPUT_W, OUTPUT_H)) + display = roi_resized.copy() + + # --- Scene detection (1Hz) --- + now = time.monotonic() + if templates_hist and now - last_detect >= 1.0: + scores = scene_similarity(roi_resized, templates_hist, templates_region) + last_detect = now + # FSM update + fsm_state, fsm_changed = fsm_update(fsm_state, scores) + if fsm_changed: + print(f" FSM -> {fsm_state}") + toast_text = fsm_state + toast_time = now + print(f" [DEBUG] hid_enabled={hid_enabled} fsm_state={fsm_state} " + f"in_actions={fsm_state in FSM_ACTIONS}") + if hid_enabled and fsm_state in FSM_ACTIONS: + print(f" [DEBUG] calling dispatch_action({FSM_ACTIONS[fsm_state]}, {hid_args})") + dispatch_action(FSM_ACTIONS[fsm_state], hid_args) + else: + print(f" [DEBUG] SKIPPED: hid_enabled={hid_enabled}") + # IN-PLAY: periodic play_turn (skip if busy) + if (hid_enabled and fsm_state == S_IN_PLAY + and now - last_play_turn >= PLAY_TURN_INTERVAL): + dispatch_if_idle("play_turn", hid_args) + last_play_turn = now + # REWARD-NEXT: periodic reward_next (skip if busy) + if (hid_enabled and fsm_state == S_REWARD_NEXT + and now - last_reward_next >= REWARD_NEXT_INTERVAL): + dispatch_if_idle("reward_next", hid_args) + last_reward_next = now + # Debug: show base vs region for scenes with regions + for sname, sscore in scores[:3]: + if sname in templates_region: + frame_hist = calc_hist(roi_resized) + base = max(cv2.compareHist(frame_hist, h, cv2.HISTCMP_CORREL) + for h in templates_hist[sname]) + rs = [] + for ri, region in enumerate(SCENE_REGIONS[sname]): + fc = crop_region(roi_resized, region) + fch = calc_hist(fc) + r = max(cv2.compareHist(fch, h, cv2.HISTCMP_CORREL) + for h in templates_region[sname][ri]) + rs.append(r) + rs_str = " ".join(f"r{i}={v:.3f}" for i, v in enumerate(rs)) + print(f" [{sname}] base={base:.3f} {rs_str} -> {sscore:.3f}") + else: + print(f" [{sname}] base={sscore:.3f}") + + # --- FPS --- + frame_count += 1 + elapsed = now - fps_time + if elapsed >= 1.0: + fps = frame_count / elapsed + frame_count = 0 + fps_time = now + else: + fps = frame_count / max(elapsed, 0.001) + + # Overlay + cv2.putText(display, f"FPS: {fps:.1f}", (10, 30), + cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) + if scores: + top = scores[0] + if top[1] < MIN_SIMILARITY: + cv2.putText(display, f"-- ({top[1]:.3f})", (10, 60), + cv2.FONT_HERSHEY_SIMPLEX, 0.7, (100, 100, 100), 2) + else: + cv2.putText(display, f"{top[0]} ({top[1]:.3f})", (10, 60), + cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) + cv2.putText(display, f"FSM: {fsm_state}", (10, 90), + cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 200, 255), 2) + draw_region_boxes(display, fsm_state) + if toast_text: + draw_toast(display, toast_text, now - toast_time) + if now - toast_time >= TOAST_DURATION: + toast_text = "" + + # Composite + chart = draw_chart(scores, CHART_W, OUTPUT_H) + composite = np.hstack([display, chart]) + cv2.imshow("Scene Detect", composite) + + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break + elif key == ord("m"): + mx, my = _mouse_pos + if mx < OUTPUT_W and hid_enabled: + xx = mx / OUTPUT_W + yy = my / OUTPUT_H + print(f" [CLICK] pct({xx:.3f}, {yy:.3f})") + def do_click(px=xx, py=yy): + hid.calibrate(manual_size=(args.hid_w, args.hid_h)) + hid.click_pct(px, py, repeat=1) + threading.Thread(target=do_click, daemon=True).start() + elif key == ord("s"): + os.makedirs(SAVE_DIR, exist_ok=True) + default = f"snapshot_{int(time.time())}" + name = input(f"Filename [{default}]: ").strip() + if not name: + name = default + if not name.endswith((".jpg", ".png")): + name += ".jpg" + fpath = os.path.join(SAVE_DIR, name) + cv2.imwrite(fpath, roi_resized) + print(f"Saved: {fpath}") + + except KeyboardInterrupt: + pass + finally: + picam2.stop() + cv2.destroyAllWindows() + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/examples/hid-usb/src/atomic_compat.c b/examples/hid-usb/src/atomic_compat.c new file mode 100644 index 000000000..0ef410cdf --- /dev/null +++ b/examples/hid-usb/src/atomic_compat.c @@ -0,0 +1,64 @@ +/** + * atomic_compat.c - Software atomic operations for Cortex-M0/M0+ (ARMv6-M) + * + * ARMv6-M lacks hardware atomic instructions (LDREX/STREX), so we implement + * these using interrupt disabling (PRIMASK). + */ + +#include +#include + +// __sync_fetch_and_add_4 +uint32_t __sync_fetch_and_add_4(volatile uint32_t *ptr, uint32_t val) { + uint32_t primask; + __asm__ volatile ("mrs %0, primask" : "=r" (primask)); + __asm__ volatile ("cpsid i"); + + uint32_t old = *ptr; + *ptr = old + val; + + __asm__ volatile ("msr primask, %0" : : "r" (primask)); + return old; +} + +// __sync_add_and_fetch_4 (returns NEW value) +uint32_t __sync_add_and_fetch_4(volatile uint32_t *ptr, uint32_t val) { + uint32_t primask; + __asm__ volatile ("mrs %0, primask" : "=r" (primask)); + __asm__ volatile ("cpsid i"); + + uint32_t newval = *ptr + val; + *ptr = newval; + + __asm__ volatile ("msr primask, %0" : : "r" (primask)); + return newval; +} + +// __sync_fetch_and_sub_4 +uint32_t __sync_fetch_and_sub_4(volatile uint32_t *ptr, uint32_t val) { + uint32_t primask; + __asm__ volatile ("mrs %0, primask" : "=r" (primask)); + __asm__ volatile ("cpsid i"); + + uint32_t old = *ptr; + *ptr = old - val; + + __asm__ volatile ("msr primask, %0" : : "r" (primask)); + return old; +} + +// __sync_bool_compare_and_swap_4 +bool __sync_bool_compare_and_swap_4(volatile uint32_t *ptr, uint32_t oldval, uint32_t newval) { + uint32_t primask; + __asm__ volatile ("mrs %0, primask" : "=r" (primask)); + __asm__ volatile ("cpsid i"); + + bool success = false; + if (*ptr == oldval) { + *ptr = newval; + success = true; + } + + __asm__ volatile ("msr primask, %0" : : "r" (primask)); + return success; +} diff --git a/examples/hid-usb/src/inet_compat.c b/examples/hid-usb/src/inet_compat.c new file mode 100644 index 000000000..d3137d623 --- /dev/null +++ b/examples/hid-usb/src/inet_compat.c @@ -0,0 +1,223 @@ +// POSIX compatibility functions for RP2040 bare metal. +// +// Provides inet_pton, inet_ntop, gettimeofday, localtime_r, getaddrinfo for: +// - libpeermx/src/address.c (IP address parsing/formatting) +// - libpeermx/src/ports.c (DNS resolution) +// - usrsctp internal address and time handling + +#include +#include +#include +#include +#include +#include "pico/time.h" +#include "pico/cyw43_arch.h" +#include "lwip/dns.h" +#include "lwip/ip_addr.h" +#include "sys/socket.h" +#include "sys/time.h" +#include "netdb.h" + +//============================================================================= +// inet_pton / inet_ntop - IP address conversion +// lwIP only provides these when LWIP_SOCKET=1 +//============================================================================= + +int inet_pton(int af, const char *src, void *dst) { + if (af == AF_INET) { + struct in_addr *addr = (struct in_addr *)dst; + unsigned int a, b, c, d; + if (sscanf(src, "%u.%u.%u.%u", &a, &b, &c, &d) == 4) { + if (a <= 255 && b <= 255 && c <= 255 && d <= 255) { + addr->s_addr = ((uint32_t)a) | ((uint32_t)b << 8) | + ((uint32_t)c << 16) | ((uint32_t)d << 24); + return 1; + } + } + return 0; + } else if (af == AF_INET6) { + // Simplified IPv6 parsing - only full form supported + struct in6_addr *addr = (struct in6_addr *)dst; + unsigned int parts[8]; + if (sscanf(src, "%x:%x:%x:%x:%x:%x:%x:%x", + &parts[0], &parts[1], &parts[2], &parts[3], + &parts[4], &parts[5], &parts[6], &parts[7]) == 8) { + for (int i = 0; i < 8; i++) { + addr->s6_addr[i*2] = (parts[i] >> 8) & 0xff; + addr->s6_addr[i*2+1] = parts[i] & 0xff; + } + return 1; + } + return 0; + } + return -1; +} + +const char *inet_ntop(int af, const void *src, char *dst, uint32_t size) { + if (af == AF_INET) { + const struct in_addr *addr = (const struct in_addr *)src; + uint32_t ip = addr->s_addr; + int len = snprintf(dst, size, "%u.%u.%u.%u", + ip & 0xff, (ip >> 8) & 0xff, + (ip >> 16) & 0xff, (ip >> 24) & 0xff); + if (len < 0 || (uint32_t)len >= size) return NULL; + return dst; + } else if (af == AF_INET6) { + const struct in6_addr *addr = (const struct in6_addr *)src; + int len = snprintf(dst, size, "%02x%02x:%02x%02x:%02x%02x:%02x%02x:" + "%02x%02x:%02x%02x:%02x%02x:%02x%02x", + addr->s6_addr[0], addr->s6_addr[1], + addr->s6_addr[2], addr->s6_addr[3], + addr->s6_addr[4], addr->s6_addr[5], + addr->s6_addr[6], addr->s6_addr[7], + addr->s6_addr[8], addr->s6_addr[9], + addr->s6_addr[10], addr->s6_addr[11], + addr->s6_addr[12], addr->s6_addr[13], + addr->s6_addr[14], addr->s6_addr[15]); + if (len < 0 || (uint32_t)len >= size) return NULL; + return dst; + } + return NULL; +} + +//============================================================================= +// gettimeofday - using Pico SDK time functions +//============================================================================= + +int gettimeofday(struct timeval *tv, void *tz) { + (void)tz; + if (tv) { + uint64_t us = to_us_since_boot(get_absolute_time()); + tv->tv_sec = us / 1000000; + tv->tv_usec = us % 1000000; + } + return 0; +} + +//============================================================================= +// localtime_r - stub for usrsctp_dumppacket logging +// Returns a zeroed struct tm (not accurate, but sufficient for logging) +//============================================================================= + +struct tm *localtime_r(const time_t *timep, struct tm *result) { + (void)timep; + if (result) { + memset(result, 0, sizeof(struct tm)); + result->tm_mday = 1; + result->tm_year = 70; // 1970 + } + return result; +} + +//============================================================================= +// nanosleep - for usrsctp timer +//============================================================================= + +int nanosleep(const struct timespec *req, struct timespec *rem) { + (void)rem; + if (req) { + uint64_t us = (uint64_t)req->tv_sec * 1000000 + req->tv_nsec / 1000; + sleep_us(us); + } + return 0; +} + +//============================================================================= +// getaddrinfo / freeaddrinfo - DNS resolution using lwIP +//============================================================================= + +// DNS callback state +static volatile int dns_done = 0; +static ip_addr_t dns_result; + +static void dns_callback(const char *name, const ip_addr_t *ipaddr, void *arg) { + (void)name; + (void)arg; + if (ipaddr) { + dns_result = *ipaddr; + } else { + ip_addr_set_zero(&dns_result); + } + dns_done = 1; +} + +int getaddrinfo_impl(const char *node, const char *service, + const struct addrinfo *hints, + struct addrinfo **res) { + (void)service; + (void)hints; + + if (!node || !res) { + return EAI_FAIL; + } + + // Try to parse as IP address first + ip_addr_t addr; + if (ipaddr_aton(node, &addr)) { + // It's already an IP address + struct addrinfo *ai = (struct addrinfo *)calloc(1, sizeof(struct addrinfo) + sizeof(struct sockaddr_in)); + if (!ai) return EAI_FAIL; + + struct sockaddr_in *sa = (struct sockaddr_in *)(ai + 1); + sa->sin_family = AF_INET; + sa->sin_addr.s_addr = ip_addr_get_ip4_u32(&addr); + + ai->ai_family = AF_INET; + ai->ai_socktype = SOCK_STREAM; + ai->ai_addrlen = sizeof(struct sockaddr_in); + ai->ai_addr = (struct sockaddr *)sa; + ai->ai_next = NULL; + + *res = ai; + return 0; + } + + // DNS lookup + dns_done = 0; + ip_addr_set_zero(&dns_result); + + err_t err = dns_gethostbyname(node, &dns_result, dns_callback, NULL); + + if (err == ERR_OK) { + // Result was cached + dns_done = 1; + } else if (err == ERR_INPROGRESS) { + // Wait for DNS callback with timeout + uint32_t start = to_ms_since_boot(get_absolute_time()); + while (!dns_done) { + cyw43_arch_poll(); + sleep_ms(10); + if (to_ms_since_boot(get_absolute_time()) - start > 5000) { + // 5 second timeout + return EAI_FAIL; + } + } + } else { + return EAI_FAIL; + } + + if (ip_addr_isany(&dns_result)) { + return EAI_NONAME; + } + + // Allocate result + struct addrinfo *ai = (struct addrinfo *)calloc(1, sizeof(struct addrinfo) + sizeof(struct sockaddr_in)); + if (!ai) return EAI_FAIL; + + struct sockaddr_in *sa = (struct sockaddr_in *)(ai + 1); + sa->sin_family = AF_INET; + sa->sin_addr.s_addr = ip_addr_get_ip4_u32(&dns_result); + + ai->ai_family = AF_INET; + ai->ai_socktype = SOCK_STREAM; + ai->ai_addrlen = sizeof(struct sockaddr_in); + ai->ai_addr = (struct sockaddr *)sa; + ai->ai_next = NULL; + + *res = ai; + return 0; +} + +void freeaddrinfo_impl(struct addrinfo *res) { + free(res); +} diff --git a/examples/rp2040-beremetal/.envrc b/examples/rp2040-beremetal/.envrc new file mode 100644 index 000000000..39b23dba8 --- /dev/null +++ b/examples/rp2040-beremetal/.envrc @@ -0,0 +1,7 @@ +export PICO_SDK_PATH="$HOME/git/pico-sdk" +export PICO_EXAMPLES_PATH="$HOME/git/pico-examples" +export PICO_BOARD=pico2_w +export WIFI_SSID="mx-free24" +export WIFI_PASSWORD="ckuv7482" +export SIGNALING_URL="https://running-ian-vision-acceptable.trycloudflare.com/whip/00/00/00" +export SIGNALING_TOKEN="" diff --git a/examples/rp2040-beremetal/.envrc.rp2040 b/examples/rp2040-beremetal/.envrc.rp2040 new file mode 100644 index 000000000..493567220 --- /dev/null +++ b/examples/rp2040-beremetal/.envrc.rp2040 @@ -0,0 +1,7 @@ +export PICO_SDK_PATH="$HOME/git/pico-sdk" +export PICO_EXAMPLES_PATH="$HOME/git/pico-examples" +export PICO_BOARD=pico_w +export WIFI_SSID="mx-free24" +export WIFI_PASSWORD="ckuv7482" +export SIGNALING_URL="https://targets-findarticles-trek-cooler.trycloudflare.com/whip/00/00/00" +export SIGNALING_TOKEN="" diff --git a/examples/rp2040-beremetal/.gitignore b/examples/rp2040-beremetal/.gitignore new file mode 100644 index 000000000..e2b085bc9 --- /dev/null +++ b/examples/rp2040-beremetal/.gitignore @@ -0,0 +1,2 @@ +_build +.patches_applied diff --git a/examples/rp2040-beremetal/CMakeLists.txt b/examples/rp2040-beremetal/CMakeLists.txt new file mode 100644 index 000000000..3e9d9e0ec --- /dev/null +++ b/examples/rp2040-beremetal/CMakeLists.txt @@ -0,0 +1,262 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 3.12) + +# Apply patches to third-party libraries (only once) +set(PATCH_MARKER "${CMAKE_CURRENT_LIST_DIR}/.patches_applied") +if(NOT EXISTS ${PATCH_MARKER}) + message(STATUS "Applying patches to third-party libraries...") + execute_process( + COMMAND patch -p1 --forward -i ${CMAKE_CURRENT_LIST_DIR}/libsrtp.patch + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../../third_party/libsrtp + RESULT_VARIABLE LIBSRTP_PATCH_RESULT + ) + execute_process( + COMMAND patch -p1 --forward -i ${CMAKE_CURRENT_LIST_DIR}/mbedtls.patch + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../../third_party/mbedtls + RESULT_VARIABLE MBEDTLS_PATCH_RESULT + ) + execute_process( + COMMAND patch -p1 --forward -i ${CMAKE_CURRENT_LIST_DIR}/usrsctp.patch + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../../third_party/usrsctp + RESULT_VARIABLE USRSCTP_PATCH_RESULT + ) + file(WRITE ${PATCH_MARKER} "Patches applied at ${CMAKE_CURRENT_LIST_DIR}") + message(STATUS "Patches applied successfully") +endif() + +SET(PRJ rp2350bm) +SET(PICO_BOARD pico2_w) +SET(HOME $ENV{HOME}) +SET(PICO_SDK_FETCH_FROM_GIT on) +INCLUDE(pico_sdk_import.cmake) + +PROJECT(${PRJ} C CXX ASM) +SET(CMAKE_C_STANDARD 11) +SET(CMAKE_CXX_STANDARD 17) + +# pico_sdk_init must be called early, right after project() +pico_sdk_init() +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g") + +# Include stdint.h/stddef.h for C/CXX only (not for assembler) +add_compile_options( + "$<$:-includestdint.h>" + "$<$:-includestddef.h>" +) + +# Fix for pico-sdk runtime.c missing CLOCKS_PER_SEC +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCLOCKS_PER_SEC=1000000") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCLOCKS_PER_SEC=1000000") + +# Configuration from environment variables +message(STATUS "=== RP2040 Build Configuration ===") +if(DEFINED ENV{SIGNALING_URL}) + message(STATUS "SIGNALING_URL: $ENV{SIGNALING_URL}") + add_definitions(-DSIGNALING_URL="$ENV{SIGNALING_URL}") +else() + message(STATUS "SIGNALING_URL: (not set, using default)") +endif() +if(DEFINED ENV{SIGNALING_TOKEN}) + message(STATUS "SIGNALING_TOKEN: (set)") + add_definitions(-DSIGNALING_TOKEN="$ENV{SIGNALING_TOKEN}") +else() + message(STATUS "SIGNALING_TOKEN: (not set)") +endif() +if(DEFINED ENV{WIFI_SSID}) + message(STATUS "WIFI_SSID: $ENV{WIFI_SSID}") + add_definitions(-DWIFI_SSID="$ENV{WIFI_SSID}") +else() + message(STATUS "WIFI_SSID: (not set, using default)") +endif() +if(DEFINED ENV{WIFI_PASSWORD}) + message(STATUS "WIFI_PASSWORD: (set)") + add_definitions(-DWIFI_PASSWORD="$ENV{WIFI_PASSWORD}") +else() + message(STATUS "WIFI_PASSWORD: (not set)") +endif() +message(STATUS "==================================") + +# libsrtp: use standard integer types +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_STDINT_H -DHAVE_UINT8_T -DHAVE_UINT16_T -DHAVE_UINT32_T -DHAVE_INT32_T -DHAVE_UINT64_T") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_STDINT_H -DHAVE_UINT8_T -DHAVE_UINT16_T -DHAVE_UINT32_T -DHAVE_INT32_T -DHAVE_UINT64_T") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_NETINET_IN_H") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_NETINET_IN_H") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Dbe32_to_cpu=lwip_ntohl") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Dbe32_to_cpu=lwip_ntohl") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Dbe64_to_cpu=__builtin_bswap64") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Dbe64_to_cpu=__builtin_bswap64") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_STDLIB_H") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_STDLIB_H") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DPACKAGE_STRING=\\\"libsrtp2\\\" -DPACKAGE_VERSION=\\\"2.0.0\\\"") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPACKAGE_STRING=\\\"libsrtp2\\\" -DPACKAGE_VERSION=\\\"2.0.0\\\"") + +IF (PICO_SDK_VERSION_STRING VERSION_LESS "1.3.0") + message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.3.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}") +ENDIF() +SET(CMAKE_INCLUDE_CURRENT_DIR ON) + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__RP2040_BM__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__RP2040_BM__") + +# Note: CONFIG_USE_LWIP=1 causes header conflicts with our socket stubs +# Instead, ports.c checks __RP2040_BM__ to use lwIP netif_list directly +# SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCONFIG_USE_LWIP=1") +# SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCONFIG_USE_LWIP=1") + +# Use mbedtls 2.x API (Pico SDK uses mbedtls 2.x) +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCONFIG_MBEDTLS_2_X=1") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCONFIG_MBEDTLS_2_X=1") + +# libsrtp: use mbedtls for AES and define CPU type +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMBEDTLS") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMBEDTLS") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCPU_RISC") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCPU_RISC") + +# RP2040 is little-endian ARM Cortex-M0+ +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__LITTLE_ENDIAN=1234 -D__BIG_ENDIAN=4321 -D__BYTE_ORDER=__LITTLE_ENDIAN") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__LITTLE_ENDIAN=1234 -D__BIG_ENDIAN=4321 -D__BYTE_ORDER=__LITTLE_ENDIAN") + +# usrsctp defines +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSCTP_SIMPLE_ALLOCATOR -DSCTP_PROCESS_LEVEL_LOCKS -D__Userspace__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSCTP_SIMPLE_ALLOCATOR -DSCTP_PROCESS_LEVEL_LOCKS -D__Userspace__") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DERESTART=85") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DERESTART=85") +# lwIP sockaddr has sa_len field +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_SCONN_LEN") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_SCONN_LEN") + + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHTTP_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHTTP_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDMQTT_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMQTT_DO_NOT_USE_CUSTOM_CONFIG") + + +IF (OLD_SRM) + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__OLD_SRM__") + MESSAGE(">> OLD SRM mode") +ELSE() + MESSAGE(">> Stages Mode") +ENDIF() + + +# SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__DRIVER_INFO__ -D__DRIVER_DEBUG__") +# SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__DRIVER_INFO__ -D__DRIVER_DEBUG__") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__DRIVER_INFO__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__DRIVER_INFO__") + +INCLUDE_DIRECTORIES(".") + + +INCLUDE(${CMAKE_CURRENT_LIST_DIR}/../../third_party/coreHTTP/httpFilePaths.cmake) +INCLUDE(${CMAKE_CURRENT_LIST_DIR}/../../third_party/coreMQTT/mqttFilePaths.cmake) +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_LIST_DIR}/ + ${CMAKE_CURRENT_LIST_DIR}/inc/ + ${CMAKE_BINARY_DIR}/ + ${CMAKE_CURRENT_LIST_DIR}/../../third_party/libsrtp/include/ + ${CMAKE_CURRENT_LIST_DIR}/../../third_party/libsrtp/crypto/include/ + ${CMAKE_CURRENT_LIST_DIR}/../../third_party/cJSON/ + ${CMAKE_CURRENT_LIST_DIR}/../../third_party/usrsctp/usrsctplib/ + ${CMAKE_CURRENT_LIST_DIR}/../../src/ + ${HTTP_INCLUDE_PUBLIC_DIRS} + ${MQTT_INCLUDE_PUBLIC_DIRS} +) + +FILE(GLOB C_SOURCE_FILES ../../src/*.c) +# Build libsrtp +file(GLOB LIBSRC + ../../third_party/libsrtp/srtp/srtp.c + ../../third_party/libsrtp/crypto/cipher/cipher.c + ../../third_party/libsrtp/crypto/cipher/null_cipher.c + ../../third_party/libsrtp/crypto/cipher/aes.c + ../../third_party/libsrtp/crypto/cipher/aes_icm.c + ../../third_party/libsrtp/crypto/cipher/cipher_test_cases.c + ../../third_party/libsrtp/crypto/hash/auth.c + ../../third_party/libsrtp/crypto/hash/null_auth.c + ../../third_party/libsrtp/crypto/hash/hmac.c + ../../third_party/libsrtp/crypto/hash/sha1.c + ../../third_party/libsrtp/crypto/hash/auth_test_cases.c + ../../third_party/libsrtp/crypto/kernel/alloc.c + ../../third_party/libsrtp/crypto/kernel/crypto_kernel.c + ../../third_party/libsrtp/crypto/kernel/err.c + ../../third_party/libsrtp/crypto/kernel/key.c + ../../third_party/libsrtp/crypto/math/datatypes.c + ../../third_party/libsrtp/crypto/math/stat.c + ../../third_party/libsrtp/crypto/replay/rdb.c + ../../third_party/libsrtp/crypto/replay/rdbx.c + ../../third_party/libsrtp/crypto/replay/ut_sim.c + # cJSON + ../../third_party/cJSON/cJSON.c + # usrsctp (nothreads mode - no user_recv_thread.c) + ../../third_party/usrsctp/usrsctplib/user_environment.c + ../../third_party/usrsctp/usrsctplib/user_mbuf.c + ../../third_party/usrsctp/usrsctplib/user_socket.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_asconf.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_auth.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_bsd_addr.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_callout.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_cc_functions.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_crc32.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_indata.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_input.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_output.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_pcb.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_peeloff.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_sha1.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_ss_functions.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_sysctl.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_timer.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_userspace.c + ../../third_party/usrsctp/usrsctplib/netinet/sctp_usrreq.c + ../../third_party/usrsctp/usrsctplib/netinet/sctputil.c +) + +ADD_EXECUTABLE( + ${PRJ} + ${C_SOURCE_FILES} + ${LIBSRC} + ${HTTP_SOURCES} + ${CMAKE_CURRENT_LIST_DIR}/atomic_compat.c + ${CMAKE_CURRENT_LIST_DIR}/inet_compat.c + ${CMAKE_CURRENT_LIST_DIR}/main.c +) +FILE(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/srtp2/) +FILE(COPY ${CMAKE_CURRENT_LIST_DIR}/../../third_party/libsrtp/include/srtp.h + DESTINATION ${CMAKE_BINARY_DIR}/srtp2/) + +PROJECT(${PRJ} VERSION 1.0.0) + +TARGET_LINK_LIBRARIES( + ${PRJ} + pico_stdlib + hardware_spi + hardware_i2c + tinyusb_host + tinyusb_device + tinyusb_board + pico_cyw43_arch_lwip_threadsafe_background + # pico_cyw43_arch_lwip_poll + pico_mbedtls +) + +pico_enable_stdio_usb(${PRJ} 0) +pico_enable_stdio_uart(${PRJ} 1) + +# Explicit UART configuration for RP2350 +target_compile_definitions(${PRJ} PRIVATE + PICO_DEFAULT_UART=0 + PICO_DEFAULT_UART_TX_PIN=0 + PICO_DEFAULT_UART_RX_PIN=1 + PICO_DEFAULT_UART_BAUD_RATE=115200 +) + +pico_add_extra_outputs(${PRJ}) + +ADD_COMPILE_OPTIONS( + -Wall + -Wno-format # int != int32_t as far as the compiler is concerned because gcc has int32_t as long int + -Wno-unused-function # we have some for the docs that aren't called + -Wno-maybe-uninitialized +) diff --git a/examples/rp2040-beremetal/README.md b/examples/rp2040-beremetal/README.md new file mode 100644 index 000000000..82ce73a5b --- /dev/null +++ b/examples/rp2040-beremetal/README.md @@ -0,0 +1,111 @@ +# libpeer + RP2040 Baremetal Example + +## Overview + +| Item | Description | +|------|-------------| +| OS | None | +| RTOS | None | +| pico-sdk | Yes (used as HAL) | + +### WebRTC Stack + +- ICE +- DTLS +- SCTP +- DataChannel + +DataChannel ping-pong works on real hardware. + +## Install +``` +brew install cmake openocd libraspberrypi-dev +brew tap ArmMbed/homebrew-formulae +brew install arm-none-eabi-gcc arm-none-eabi-gdb +``` + +## Build + +``` +mkdir -p ./_build +cd ./_build +cmake .. +make +sudo openocd -f interface/raspberrypi-swd.cfg -f target/rp2350.cfg -c "program rp2350bm.elf verify reset exit" +``` + +## Patches + +Patches for third-party libraries are automatically applied during the first `cmake` run. + +If you need to apply them manually: + +``` +cd ../../third_party/libsrtp && patch -p1 < ../../examples/rp2040-beremetal/libsrtp.patch +cd ../../third_party/mbedtls && patch -p1 < ../../examples/rp2040-beremetal/mbedtls.patch +cd ../../third_party/usrsctp && patch -p1 < ../../examples/rp2040-beremetal/usrsctp.patch +``` + +### libsrtp.patch + +| Item | Detail | +|------|--------| +| File | `include/srtp.h` | +| Change | `srtp_remove_stream(srtp_t session, unsigned int ssrc)` → `uint32_t ssrc` | +| Reason | Fix type mismatch on 32-bit ARM (RP2040). `unsigned int` size varies by platform, while `uint32_t` guarantees 32-bit width for SSRC values. | + +### mbedtls.patch + +| Item | Detail | +|------|--------| +| File | `include/mbedtls/mbedtls_config.h` | +| Change | Uncomment `#define MBEDTLS_SSL_DTLS_SRTP` | +| Reason | Enable DTLS-SRTP extension required for WebRTC key exchange. This feature is disabled by default in mbedtls. | + +### usrsctp.patch + +This is the largest patch with multiple modifications for RP2040 bare-metal support. + +| File | Change | Reason | +|------|--------|--------| +| `usrsctplib/usrsctp.h` | Add lwIP header includes when `CONFIG_USE_LWIP` is defined | RP2040 uses lwIP instead of BSD sockets. Include `` and `` for socket types. | +| `usrsctplib/usrsctp.h` | Add `__RP2040_BM__` to BSD-style `sockaddr_conn` layout | Fix `sockaddr_conn` struct alignment. RP2040 lwIP uses BSD-style layout with `sconn_len` + `sconn_family` (1 byte each) instead of Linux-style `sconn_family` (2 bytes). | +| `usrsctplib/user_environment.c` | Add RP2040 hardware RNG implementation | Use Pico SDK `pico/rand.h` and `get_rand_32()` for cryptographic random number generation. Essential for SCTP security. | +| `usrsctplib/user_socket.c` | Add debugging logs to `getsockaddr()` and `usrsctp_bind()` | Debug output for troubleshooting SCTP bind issues on RP2040. Shows byte-level sockaddr data and family values. | + +## Compatibility Layer (Stubs) + +RP2040 bare-metal has no OS, so POSIX/BSD headers and functions must be stubbed or reimplemented. These files provide the minimal compatibility layer for usrsctp, mbedtls, and libpeermx. + +### Source Files + +| File | Description | +|------|-------------| +| `inet_compat.c` | POSIX function implementations: `inet_pton`, `inet_ntop` (IP address conversion), `gettimeofday` (Pico SDK time), `localtime_r` (stub for logging), `nanosleep` (Pico SDK sleep), `getaddrinfo`/`freeaddrinfo` (DNS via lwIP). | +| `atomic_compat.c` | Software atomic operations for Cortex-M0+ (ARMv6-M lacks LDREX/STREX). Implements `__sync_fetch_and_add_4`, `__sync_add_and_fetch_4`, `__sync_fetch_and_sub_4`, `__sync_bool_compare_and_swap_4` using interrupt masking (PRIMASK). | + +### Header Stubs (`inc/`) + +| File | Description | +|------|-------------| +| `arpa/inet.h` | Redirects to `sys/socket.h` for `inet_pton`/`inet_ntop` declarations and lwIP address types. | +| `net/if.h` | Defines `IFNAMSIZ`, `struct ifnet` (opaque), and `if_nametoindex()` stub (returns 0). Never called because `getifaddrs()` fails first. | +| `netinet/in.h` | Redirects to `sys/socket.h` for `sockaddr_in`, `sockaddr_in6`, `in_addr` types via lwIP. | +| `netinet/ip.h` | Defines `struct ip` (IPv4 header), `IPVERSION`, `IPTOS_*` constants. Required by `sctp_os_userspace.h`. | +| `netinet/in_systm.h` | Empty stub. Included by usrsctp but types (`n_short`, `n_long`) not actually used. | +| `sys/socket.h` | Central socket types stub. Defines `sockaddr`, `sockaddr_in`, `sockaddr_in6`, `sockaddr_storage`, `iovec`, `msghdr`, `cmsghdr`, socket constants (`AF_*`, `SOCK_*`, `MSG_*`, `SO_*`), and CMSG macros. Includes ``. | +| `sys/time.h` | Defines `struct timeval`, timer macros (`timercmp`, `timeradd`, `timersub`), declares `gettimeofday()`. | +| `sys/select.h` | Implements `select()` stub that polls `cyw43_arch_poll()` with timeout. Gives lwIP time to process packets. | +| `sys/ioctl.h` | Empty stub. Included by usrsctp but `ioctl()` code path never executed (no BSD sockets). | +| `sys/uio.h` | Redirects to `sys/socket.h` for `struct iovec` definition. | +| `netdb.h` | DNS resolution stub. Defines `struct addrinfo`, `EAI_*` errors, redirects `getaddrinfo`/`freeaddrinfo` to `inet_compat.c` implementations. | +| `pthread.h` | POSIX threads stub for single-threaded mode. All mutex/rwlock/condvar/thread functions are no-ops. Uses ARM toolchain's `sys/_pthreadtypes.h`. | +| `ifaddrs.h` | Network interface enumeration stub. `getifaddrs()` returns -1, causing usrsctp to skip interface enumeration. OK because we use AF_CONN mode. | + +### Configuration Headers (`inc/`) + +| File | Description | +|------|-------------| +| `lwipopts.h` | lwIP configuration for RP2040. `NO_SYS=1` (no OS), `LWIP_SOCKET=0` (raw API only), IPv4/IPv6, DNS, DHCP enabled. Memory tuned for Pico W. | +| `tusb_config.h` | TinyUSB configuration. Device mode, full speed, HID endpoint for USB communication. | +| `mbedtls_config.h` | mbedtls configuration for RP2040. Hardware entropy, memory optimizations (`SHA256_SMALLER`, `AES_FEWER_TABLES`), ECDHE key exchange, TLS 1.2 + DTLS 1.2, DTLS-SRTP, self-signed cert generation. | \ No newline at end of file diff --git a/examples/rp2040-beremetal/atomic_compat.c b/examples/rp2040-beremetal/atomic_compat.c new file mode 100644 index 000000000..0ef410cdf --- /dev/null +++ b/examples/rp2040-beremetal/atomic_compat.c @@ -0,0 +1,64 @@ +/** + * atomic_compat.c - Software atomic operations for Cortex-M0/M0+ (ARMv6-M) + * + * ARMv6-M lacks hardware atomic instructions (LDREX/STREX), so we implement + * these using interrupt disabling (PRIMASK). + */ + +#include +#include + +// __sync_fetch_and_add_4 +uint32_t __sync_fetch_and_add_4(volatile uint32_t *ptr, uint32_t val) { + uint32_t primask; + __asm__ volatile ("mrs %0, primask" : "=r" (primask)); + __asm__ volatile ("cpsid i"); + + uint32_t old = *ptr; + *ptr = old + val; + + __asm__ volatile ("msr primask, %0" : : "r" (primask)); + return old; +} + +// __sync_add_and_fetch_4 (returns NEW value) +uint32_t __sync_add_and_fetch_4(volatile uint32_t *ptr, uint32_t val) { + uint32_t primask; + __asm__ volatile ("mrs %0, primask" : "=r" (primask)); + __asm__ volatile ("cpsid i"); + + uint32_t newval = *ptr + val; + *ptr = newval; + + __asm__ volatile ("msr primask, %0" : : "r" (primask)); + return newval; +} + +// __sync_fetch_and_sub_4 +uint32_t __sync_fetch_and_sub_4(volatile uint32_t *ptr, uint32_t val) { + uint32_t primask; + __asm__ volatile ("mrs %0, primask" : "=r" (primask)); + __asm__ volatile ("cpsid i"); + + uint32_t old = *ptr; + *ptr = old - val; + + __asm__ volatile ("msr primask, %0" : : "r" (primask)); + return old; +} + +// __sync_bool_compare_and_swap_4 +bool __sync_bool_compare_and_swap_4(volatile uint32_t *ptr, uint32_t oldval, uint32_t newval) { + uint32_t primask; + __asm__ volatile ("mrs %0, primask" : "=r" (primask)); + __asm__ volatile ("cpsid i"); + + bool success = false; + if (*ptr == oldval) { + *ptr = newval; + success = true; + } + + __asm__ volatile ("msr primask, %0" : : "r" (primask)); + return success; +} diff --git a/examples/rp2040-beremetal/cmake.rp2040/CMakeLists.txt b/examples/rp2040-beremetal/cmake.rp2040/CMakeLists.txt new file mode 100644 index 000000000..f225db101 --- /dev/null +++ b/examples/rp2040-beremetal/cmake.rp2040/CMakeLists.txt @@ -0,0 +1,240 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 3.12) + +SET(PRJ rp2040bm) +SET(PICO_BOARD pico_w) +SET(HOME $ENV{HOME}) +SET(PICO_SDK_FETCH_FROM_GIT on) +INCLUDE(pico_sdk_import.cmake) + +PROJECT(${PRJ} C CXX ASM) +SET(CMAKE_C_STANDARD 11) +SET(CMAKE_CXX_STANDARD 17) + +# pico_sdk_init must be called early, right after project() +pico_sdk_init() +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g") + +# Include stdint.h/stddef.h for C/CXX only (not for assembler) +add_compile_options( + "$<$:-includestdint.h>" + "$<$:-includestddef.h>" +) + +# Fix for pico-sdk runtime.c missing CLOCKS_PER_SEC +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCLOCKS_PER_SEC=1000000") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCLOCKS_PER_SEC=1000000") + +# Configuration from environment variables +message(STATUS "=== RP2040 Build Configuration ===") +if(DEFINED ENV{SIGNALING_URL}) + message(STATUS "SIGNALING_URL: $ENV{SIGNALING_URL}") + add_definitions(-DSIGNALING_URL="$ENV{SIGNALING_URL}") +else() + message(STATUS "SIGNALING_URL: (not set, using default)") +endif() +if(DEFINED ENV{SIGNALING_TOKEN}) + message(STATUS "SIGNALING_TOKEN: (set)") + add_definitions(-DSIGNALING_TOKEN="$ENV{SIGNALING_TOKEN}") +else() + message(STATUS "SIGNALING_TOKEN: (not set)") +endif() +if(DEFINED ENV{WIFI_SSID}) + message(STATUS "WIFI_SSID: $ENV{WIFI_SSID}") + add_definitions(-DWIFI_SSID="$ENV{WIFI_SSID}") +else() + message(STATUS "WIFI_SSID: (not set, using default)") +endif() +if(DEFINED ENV{WIFI_PASSWORD}) + message(STATUS "WIFI_PASSWORD: (set)") + add_definitions(-DWIFI_PASSWORD="$ENV{WIFI_PASSWORD}") +else() + message(STATUS "WIFI_PASSWORD: (not set)") +endif() +message(STATUS "==================================") + +# libsrtp: use standard integer types +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_STDINT_H -DHAVE_UINT8_T -DHAVE_UINT16_T -DHAVE_UINT32_T -DHAVE_INT32_T -DHAVE_UINT64_T") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_STDINT_H -DHAVE_UINT8_T -DHAVE_UINT16_T -DHAVE_UINT32_T -DHAVE_INT32_T -DHAVE_UINT64_T") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_NETINET_IN_H") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_NETINET_IN_H") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Dbe32_to_cpu=lwip_ntohl") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Dbe32_to_cpu=lwip_ntohl") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Dbe64_to_cpu=__builtin_bswap64") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Dbe64_to_cpu=__builtin_bswap64") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_STDLIB_H") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_STDLIB_H") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DPACKAGE_STRING=\\\"libsrtp2\\\" -DPACKAGE_VERSION=\\\"2.0.0\\\"") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DPACKAGE_STRING=\\\"libsrtp2\\\" -DPACKAGE_VERSION=\\\"2.0.0\\\"") + +IF (PICO_SDK_VERSION_STRING VERSION_LESS "1.3.0") + message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.3.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}") +ENDIF() +SET(CMAKE_INCLUDE_CURRENT_DIR ON) + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__RP2040_BM__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__RP2040_BM__") + +# Note: CONFIG_USE_LWIP=1 causes header conflicts with our socket stubs +# Instead, ports.c checks __RP2040_BM__ to use lwIP netif_list directly +# SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCONFIG_USE_LWIP=1") +# SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCONFIG_USE_LWIP=1") + +# Use mbedtls 2.x API (Pico SDK uses mbedtls 2.x) +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCONFIG_MBEDTLS_2_X=1") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCONFIG_MBEDTLS_2_X=1") + +# libsrtp: use mbedtls for AES and define CPU type +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DMBEDTLS") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMBEDTLS") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DCPU_RISC") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCPU_RISC") + +# RP2040 is little-endian ARM Cortex-M0+ +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__LITTLE_ENDIAN=1234 -D__BIG_ENDIAN=4321 -D__BYTE_ORDER=__LITTLE_ENDIAN") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__LITTLE_ENDIAN=1234 -D__BIG_ENDIAN=4321 -D__BYTE_ORDER=__LITTLE_ENDIAN") + +# usrsctp defines +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSCTP_SIMPLE_ALLOCATOR -DSCTP_PROCESS_LEVEL_LOCKS -D__Userspace__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSCTP_SIMPLE_ALLOCATOR -DSCTP_PROCESS_LEVEL_LOCKS -D__Userspace__") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DERESTART=85") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DERESTART=85") +# lwIP sockaddr has sa_len field +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHAVE_SCONN_LEN") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHAVE_SCONN_LEN") + + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DHTTP_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DHTTP_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DDMQTT_DO_NOT_USE_CUSTOM_CONFIG") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DMQTT_DO_NOT_USE_CUSTOM_CONFIG") + + +IF (OLD_SRM) + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__OLD_SRM__") + MESSAGE(">> OLD SRM mode") +ELSE() + MESSAGE(">> Stages Mode") +ENDIF() + + +# SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__DRIVER_INFO__ -D__DRIVER_DEBUG__") +# SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__DRIVER_INFO__ -D__DRIVER_DEBUG__") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__DRIVER_INFO__") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D__DRIVER_INFO__") + +INCLUDE_DIRECTORIES(".") + + +INCLUDE(${CMAKE_CURRENT_LIST_DIR}/../../../third_party/coreHTTP/httpFilePaths.cmake) +INCLUDE(${CMAKE_CURRENT_LIST_DIR}/../../../third_party/coreMQTT/mqttFilePaths.cmake) +INCLUDE_DIRECTORIES( + ${CMAKE_CURRENT_LIST_DIR}/ + ${CMAKE_CURRENT_LIST_DIR}/inc/ + ${CMAKE_CURRENT_LIST_DIR}/../inc/ + ${CMAKE_BINARY_DIR}/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/libsrtp/include/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/libsrtp/crypto/include/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/cJSON/ + ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/usrsctp/usrsctplib/ + ${CMAKE_CURRENT_LIST_DIR}/../../../src/ + ${HTTP_INCLUDE_PUBLIC_DIRS} + ${MQTT_INCLUDE_PUBLIC_DIRS} +) + +FILE(GLOB C_SOURCE_FILES ../../../src/*.c) +# Build libsrtp +file(GLOB LIBSRC + ../../../third_party/libsrtp/srtp/srtp.c + ../../../third_party/libsrtp/crypto/cipher/cipher.c + ../../../third_party/libsrtp/crypto/cipher/null_cipher.c + ../../../third_party/libsrtp/crypto/cipher/aes.c + ../../../third_party/libsrtp/crypto/cipher/aes_icm.c + ../../../third_party/libsrtp/crypto/cipher/cipher_test_cases.c + ../../../third_party/libsrtp/crypto/hash/auth.c + ../../../third_party/libsrtp/crypto/hash/null_auth.c + ../../../third_party/libsrtp/crypto/hash/hmac.c + ../../../third_party/libsrtp/crypto/hash/sha1.c + ../../../third_party/libsrtp/crypto/hash/auth_test_cases.c + ../../../third_party/libsrtp/crypto/kernel/alloc.c + ../../../third_party/libsrtp/crypto/kernel/crypto_kernel.c + ../../../third_party/libsrtp/crypto/kernel/err.c + ../../../third_party/libsrtp/crypto/kernel/key.c + ../../../third_party/libsrtp/crypto/math/datatypes.c + ../../../third_party/libsrtp/crypto/math/stat.c + ../../../third_party/libsrtp/crypto/replay/rdb.c + ../../../third_party/libsrtp/crypto/replay/rdbx.c + ../../../third_party/libsrtp/crypto/replay/ut_sim.c + # cJSON + ../../../third_party/cJSON/cJSON.c + # usrsctp (nothreads mode - no user_recv_thread.c) + ../../../third_party/usrsctp/usrsctplib/user_environment.c + ../../../third_party/usrsctp/usrsctplib/user_mbuf.c + ../../../third_party/usrsctp/usrsctplib/user_socket.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_asconf.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_auth.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_bsd_addr.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_callout.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_cc_functions.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_crc32.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_indata.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_input.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_output.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_pcb.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_peeloff.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_sha1.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_ss_functions.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_sysctl.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_timer.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_userspace.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctp_usrreq.c + ../../../third_party/usrsctp/usrsctplib/netinet/sctputil.c +) + +ADD_EXECUTABLE( + ${PRJ} + ${C_SOURCE_FILES} + ${LIBSRC} + ${HTTP_SOURCES} + ${CMAKE_CURRENT_LIST_DIR}/../atomic_compat.c + ${CMAKE_CURRENT_LIST_DIR}/../inet_compat.c + ${CMAKE_CURRENT_LIST_DIR}/../main.c +) +FILE(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/srtp2/) +FILE(COPY ${CMAKE_CURRENT_LIST_DIR}/../../../third_party/libsrtp/include/srtp.h + DESTINATION ${CMAKE_BINARY_DIR}/srtp2/) + +PROJECT(${PRJ} VERSION 1.0.0) + +TARGET_LINK_LIBRARIES( + ${PRJ} + pico_stdlib + hardware_spi + hardware_i2c + tinyusb_host + tinyusb_device + tinyusb_board + pico_cyw43_arch_lwip_threadsafe_background + # pico_cyw43_arch_lwip_poll + pico_mbedtls +) + +pico_enable_stdio_usb(${PRJ} 0) +pico_enable_stdio_uart(${PRJ} 1) + +# Explicit UART configuration for RP2350 +target_compile_definitions(${PRJ} PRIVATE + PICO_DEFAULT_UART=0 + PICO_DEFAULT_UART_TX_PIN=0 + PICO_DEFAULT_UART_RX_PIN=1 + PICO_DEFAULT_UART_BAUD_RATE=115200 +) + +pico_add_extra_outputs(${PRJ}) + +ADD_COMPILE_OPTIONS( + -Wall + -Wno-format # int != int32_t as far as the compiler is concerned because gcc has int32_t as long int + -Wno-unused-function # we have some for the docs that aren't called + -Wno-maybe-uninitialized +) diff --git a/examples/rp2040-beremetal/cmake.rp2040/pico_sdk_import.cmake b/examples/rp2040-beremetal/cmake.rp2040/pico_sdk_import.cmake new file mode 100644 index 000000000..65f8a6f7d --- /dev/null +++ b/examples/rp2040-beremetal/cmake.rp2040/pico_sdk_import.cmake @@ -0,0 +1,73 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + GIT_SUBMODULES_RECURSE FALSE + ) + else () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + ) + endif () + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/examples/rp2040-beremetal/inc/arpa/inet.h b/examples/rp2040-beremetal/inc/arpa/inet.h new file mode 100644 index 000000000..61d89edf5 --- /dev/null +++ b/examples/rp2040-beremetal/inc/arpa/inet.h @@ -0,0 +1,9 @@ +#ifndef _ARPA_INET_H_STUB +#define _ARPA_INET_H_STUB + +// Redirect to sys/socket.h which provides inet_pton/inet_ntop declarations +// and includes lwip/inet.h for address types. +// Required by usrsctp and libpeermx for IP address handling. +#include + +#endif diff --git a/examples/rp2040-beremetal/inc/ifaddrs.h b/examples/rp2040-beremetal/inc/ifaddrs.h new file mode 100644 index 000000000..f09edae78 --- /dev/null +++ b/examples/rp2040-beremetal/inc/ifaddrs.h @@ -0,0 +1,48 @@ +#ifndef _IFADDRS_H_STUB +#define _IFADDRS_H_STUB + +// Stub for network interface enumeration on RP2040 bare metal. +// +// usrsctp calls getifaddrs() in sctp_init_ifns_for_vrf() to enumerate +// network interfaces. Our stub returns -1 (failure), causing usrsctp +// to skip interface enumeration. This is acceptable because: +// - RP2040 has a single WiFi interface managed by lwIP +// - We use AF_CONN mode with UDP encapsulation, not raw sockets +// - Interface binding is not needed for our use case + +#include +#include + +// Interface flags - referenced by usrsctp but not actually used +// when getifaddrs() returns failure +#ifndef IFF_UP +#define IFF_UP 0x1 +#endif +#ifndef IFF_RUNNING +#define IFF_RUNNING 0x40 +#endif +#ifndef IFF_LOOPBACK +#define IFF_LOOPBACK 0x8 +#endif + +struct ifaddrs { + struct ifaddrs *ifa_next; + char *ifa_name; + unsigned int ifa_flags; + struct sockaddr *ifa_addr; + struct sockaddr *ifa_netmask; + struct sockaddr *ifa_broadaddr; + void *ifa_data; +}; + +// getifaddrs stub - returns failure to skip interface enumeration +static inline int getifaddrs(struct ifaddrs **ifap) { + *ifap = NULL; + return -1; +} + +static inline void freeifaddrs(struct ifaddrs *ifa) { + (void)ifa; +} + +#endif diff --git a/examples/rp2040-beremetal/inc/lwipopts.h b/examples/rp2040-beremetal/inc/lwipopts.h new file mode 100644 index 000000000..07cce9ccd --- /dev/null +++ b/examples/rp2040-beremetal/inc/lwipopts.h @@ -0,0 +1,97 @@ +#ifndef _LWIPOPTS_EXAMPLE_COMMONH_H +#define _LWIPOPTS_EXAMPLE_COMMONH_H + + +// Common settings used in most of the pico_w examples +// (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html for details) + +// allow override in some examples +#ifndef NO_SYS +#define NO_SYS 1 +#endif +// Socket API requires NO_SYS=0, not available in bare metal mode +// usrsctp uses UDP encapsulation, mbedtls uses altcp_tls +#ifndef LWIP_SOCKET +#define LWIP_SOCKET 0 +#endif +// Prevent redefinition of struct timeval (already defined in ARM toolchain) +#define LWIP_TIMEVAL_PRIVATE 0 +// Enable IGMP for multicast support +#define LWIP_IGMP 1 + +#if PICO_CYW43_ARCH_POLL +#define MEM_LIBC_MALLOC 1 +#else +// MEM_LIBC_MALLOC is incompatible with non polling versions +#define MEM_LIBC_MALLOC 0 +#endif +#define MEM_ALIGNMENT 4 +#define MEM_SIZE 4000 +#define MEMP_NUM_TCP_SEG 32 +#define MEMP_NUM_ARP_QUEUE 10 +#define PBUF_POOL_SIZE 16 +#define LWIP_ARP 1 +#define LWIP_ETHERNET 1 +#define LWIP_ICMP 1 +#define LWIP_RAW 1 +#define TCP_WND (4 * TCP_MSS) +#define TCP_MSS 1460 +#define TCP_SND_BUF (4 * TCP_MSS) +#define TCP_SND_QUEUELEN ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS)) +#define LWIP_NETIF_STATUS_CALLBACK 1 +#define LWIP_NETIF_LINK_CALLBACK 1 +#define LWIP_NETIF_HOSTNAME 1 +#define LWIP_NETCONN 0 +#define MEM_STATS 0 +#define SYS_STATS 0 +#define MEMP_STATS 0 +#define LINK_STATS 0 +// #define ETH_PAD_SIZE 2 +#define LWIP_CHKSUM_ALGORITHM 3 +#define LWIP_DHCP 1 +#define LWIP_IPV4 1 +#define LWIP_IPV6 1 +#define LWIP_TCP 1 +#define LWIP_UDP 1 +#define LWIP_DNS 1 +#define LWIP_TCP_KEEPALIVE 1 +#define LWIP_NETIF_TX_SINGLE_PBUF 1 +#define DHCP_DOES_ARP_CHECK 0 +#define LWIP_DHCP_DOES_ACD_CHECK 0 + +#ifndef NDEBUG +#define LWIP_DEBUG 1 +#define LWIP_STATS 1 +#define LWIP_STATS_DISPLAY 1 +#endif + +#define ETHARP_DEBUG LWIP_DBG_OFF +#define NETIF_DEBUG LWIP_DBG_OFF +#define PBUF_DEBUG LWIP_DBG_OFF +#define API_LIB_DEBUG LWIP_DBG_OFF +#define API_MSG_DEBUG LWIP_DBG_OFF +#define SOCKETS_DEBUG LWIP_DBG_OFF +#define ICMP_DEBUG LWIP_DBG_OFF +#define INET_DEBUG LWIP_DBG_OFF +#define IP_DEBUG LWIP_DBG_OFF +#define IP_REASS_DEBUG LWIP_DBG_OFF +#define RAW_DEBUG LWIP_DBG_OFF +#define MEM_DEBUG LWIP_DBG_OFF +#define MEMP_DEBUG LWIP_DBG_OFF +#define SYS_DEBUG LWIP_DBG_OFF +#define TCP_DEBUG LWIP_DBG_ON +#define TCP_INPUT_DEBUG LWIP_DBG_OFF +#define TCP_OUTPUT_DEBUG LWIP_DBG_OFF +#define TCP_RTO_DEBUG LWIP_DBG_OFF +#define TCP_CWND_DEBUG LWIP_DBG_OFF +#define TCP_WND_DEBUG LWIP_DBG_OFF +#define TCP_FR_DEBUG LWIP_DBG_OFF +#define TCP_QLEN_DEBUG LWIP_DBG_OFF +#define TCP_RST_DEBUG LWIP_DBG_OFF +#define UDP_DEBUG LWIP_DBG_OFF +#define TCPIP_DEBUG LWIP_DBG_ON +#define PPP_DEBUG LWIP_DBG_OFF +#define SLIP_DEBUG LWIP_DBG_OFF +#define DHCP_DEBUG LWIP_DBG_ON + +#endif /* __LWIPOPTS_H__ */ diff --git a/examples/rp2040-beremetal/inc/mbedtls_config.h b/examples/rp2040-beremetal/inc/mbedtls_config.h new file mode 100644 index 000000000..530e7b924 --- /dev/null +++ b/examples/rp2040-beremetal/inc/mbedtls_config.h @@ -0,0 +1,125 @@ +#ifndef MBEDTLS_CONFIG_H +#define MBEDTLS_CONFIG_H + +/* Workaround for some mbedtls source files using INT_MAX without including limits.h */ +#include + +/*============================================================================ + * Memory optimizations for RP2350 (520KB RAM) + *============================================================================*/ +#define MBEDTLS_NO_PLATFORM_ENTROPY +#define MBEDTLS_ENTROPY_HARDWARE_ALT +#define MBEDTLS_ALLOW_PRIVATE_ACCESS +#define MBEDTLS_HAVE_TIME + +/* Smaller implementations */ +#define MBEDTLS_SHA256_SMALLER +#define MBEDTLS_AES_FEWER_TABLES +#define MBEDTLS_AES_ROM_TABLES +#define MBEDTLS_ECP_NIST_OPTIM + +/* SSL buffer sizes - RP2350 has enough RAM for full 16KB buffers */ +#define MBEDTLS_SSL_MAX_CONTENT_LEN 16384 +#define MBEDTLS_SSL_IN_CONTENT_LEN 16384 +#define MBEDTLS_SSL_OUT_CONTENT_LEN 4096 + +/* Reduce MPI (bignum) window size to save RAM */ +#define MBEDTLS_MPI_WINDOW_SIZE 2 +#define MBEDTLS_MPI_MAX_SIZE 384 + +/* SSL client and server (server needed for DTLS in WebRTC) */ +#define MBEDTLS_SSL_CLI_C +#define MBEDTLS_SSL_SRV_C + +/*============================================================================ + * ECC curves - only what Cloudflare/modern servers use + *============================================================================*/ +#define MBEDTLS_ECP_DP_SECP256R1_ENABLED /* Required for most servers */ +#define MBEDTLS_ECP_DP_SECP384R1_ENABLED /* Fallback */ +#define MBEDTLS_ECP_DP_CURVE25519_ENABLED /* For DTLS/WebRTC */ + +/*============================================================================ + * Key exchange - ECDHE only (no RSA key exchange, save ~15KB) + *============================================================================*/ +#define MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED +#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED + +/*============================================================================ + * Ciphers - minimal set for TLS 1.2 + *============================================================================*/ +#define MBEDTLS_CIPHER_MODE_CBC +#define MBEDTLS_GCM_C +#define MBEDTLS_AES_C +#define MBEDTLS_CIPHER_C + +/*============================================================================ + * Core crypto + *============================================================================*/ +#define MBEDTLS_BIGNUM_C +#define MBEDTLS_CTR_DRBG_C +#define MBEDTLS_ENTROPY_C +#define MBEDTLS_ENTROPY_SHA256_ACCUMULATOR +#define MBEDTLS_MD_C +#define MBEDTLS_SHA1_C +#define MBEDTLS_SHA224_C +#define MBEDTLS_SHA256_C + +/* RSA for certificate verification (servers use RSA certs) */ +#define MBEDTLS_RSA_C +#define MBEDTLS_PKCS1_V15 +#define MBEDTLS_PKCS1_V21 + +/* ECDSA/ECDH */ +#define MBEDTLS_ECDH_C +#define MBEDTLS_ECP_C +#define MBEDTLS_ECDSA_C + +/*============================================================================ + * X.509 / Certificates + *============================================================================*/ +#define MBEDTLS_ASN1_PARSE_C +#define MBEDTLS_ASN1_WRITE_C +#define MBEDTLS_OID_C +#define MBEDTLS_PK_C +#define MBEDTLS_PK_PARSE_C +#define MBEDTLS_X509_CRT_PARSE_C +#define MBEDTLS_X509_USE_C +#define MBEDTLS_PEM_PARSE_C +#define MBEDTLS_BASE64_C +#define MBEDTLS_SSL_SERVER_NAME_INDICATION + +/*============================================================================ + * TLS 1.2 (for HTTPS signaling) + *============================================================================*/ +#define MBEDTLS_SSL_TLS_C +#define MBEDTLS_SSL_PROTO_TLS1_2 +#define MBEDTLS_PLATFORM_C + +/*============================================================================ + * DTLS (for WebRTC) + *============================================================================*/ +#define MBEDTLS_SSL_PROTO_DTLS +#define MBEDTLS_SSL_DTLS_SRTP +#define MBEDTLS_SSL_EXPORT_KEYS +#define MBEDTLS_SSL_DTLS_HELLO_VERIFY +#define MBEDTLS_SSL_COOKIE_C +#define MBEDTLS_TIMING_C + +/*============================================================================ + * Certificate generation (for DTLS self-signed cert) + *============================================================================*/ +#define MBEDTLS_PEM_WRITE_C +#define MBEDTLS_X509_CRT_WRITE_C +#define MBEDTLS_X509_CREATE_C +#define MBEDTLS_GENPRIME +#define MBEDTLS_PK_WRITE_C + +/*============================================================================ + * Misc + *============================================================================*/ +#define MBEDTLS_ERROR_C +/* #define MBEDTLS_DEBUG_C */ /* Disabled: save RAM, enable for debugging */ +#define MBEDTLS_SSL_KEEP_PEER_CERTIFICATE /* Required for DTLS fingerprint verification */ +#define __unix__ + +#endif // MBEDTLS_CONFIG_H diff --git a/examples/rp2040-beremetal/inc/net/if.h b/examples/rp2040-beremetal/inc/net/if.h new file mode 100644 index 000000000..c6fbffeae --- /dev/null +++ b/examples/rp2040-beremetal/inc/net/if.h @@ -0,0 +1,23 @@ +#ifndef _NET_IF_H_STUB +#define _NET_IF_H_STUB + +// Minimal net/if.h stub for RP2040 bare metal + +#ifndef IFNAMSIZ +#define IFNAMSIZ 16 +#endif + +// Forward declaration - used as opaque pointer in usrsctp +struct ifnet; + +// Stub for if_nametoindex - only needed for linking. +// Never actually called because getifaddrs() returns -1, +// causing sctp_init_ifns_for_vrf() to early-return before +// reaching if_nametoindex(). usrsctp uses AF_CONN sockets +// with UDP encapsulation, so interface info is not required. +static inline unsigned int if_nametoindex(const char *ifname) { + (void)ifname; + return 0; +} + +#endif diff --git a/examples/rp2040-beremetal/inc/netdb.h b/examples/rp2040-beremetal/inc/netdb.h new file mode 100644 index 000000000..4b62d998a --- /dev/null +++ b/examples/rp2040-beremetal/inc/netdb.h @@ -0,0 +1,48 @@ +#ifndef _NETDB_H_STUB +#define _NETDB_H_STUB + +// Stub for DNS/name resolution on RP2040 bare metal. +// +// usrsctp's sctp_pcb.c includes but does not actually call +// getaddrinfo() or related functions. This header exists only for +// compilation compatibility. +// +// If DNS resolution is needed in the future, lwIP's dns.h can be used +// with dns_gethostbyname() callback API. + +#include +#include + +#ifndef AF_UNSPEC +#define AF_UNSPEC 0 +#endif + +// addrinfo flags - not used, but defined for compilation +#define AI_PASSIVE 0x01 +#define AI_CANONNAME 0x02 +#define AI_NUMERICHOST 0x04 + +#define EAI_NONAME -2 +#define EAI_FAIL -4 + +struct addrinfo { + int ai_flags; + int ai_family; + int ai_socktype; + int ai_protocol; + size_t ai_addrlen; + struct sockaddr *ai_addr; + char *ai_canonname; + struct addrinfo *ai_next; +}; + +// DNS resolution implemented in inet_compat.c using lwIP +int getaddrinfo_impl(const char *node, const char *service, + const struct addrinfo *hints, + struct addrinfo **res); +void freeaddrinfo_impl(struct addrinfo *res); + +#define getaddrinfo getaddrinfo_impl +#define freeaddrinfo freeaddrinfo_impl + +#endif diff --git a/examples/rp2040-beremetal/inc/netinet/in.h b/examples/rp2040-beremetal/inc/netinet/in.h new file mode 100644 index 000000000..15b11ec7a --- /dev/null +++ b/examples/rp2040-beremetal/inc/netinet/in.h @@ -0,0 +1,9 @@ +#ifndef NETINET_INET_H +#define NETINET_INET_H + +// Redirect to sys/socket.h which provides all necessary types +// (sockaddr_in, sockaddr_in6, in_addr, etc.) via lwip/inet.h. +// Required by sctp_os_userspace.h. +#include + +#endif // NETINET_INET_H diff --git a/examples/rp2040-beremetal/inc/netinet/in_systm.h b/examples/rp2040-beremetal/inc/netinet/in_systm.h new file mode 100644 index 000000000..69dea3c04 --- /dev/null +++ b/examples/rp2040-beremetal/inc/netinet/in_systm.h @@ -0,0 +1,9 @@ +#ifndef _NETINET_IN_SYSTM_H_STUB +#define _NETINET_IN_SYSTM_H_STUB + +// Empty stub - only needed for compilation. +// sctp_os_userspace.h includes , but +// the types (n_short, n_long, n_time) are not actually used +// by usrsctp in our configuration. + +#endif diff --git a/examples/rp2040-beremetal/inc/netinet/ip.h b/examples/rp2040-beremetal/inc/netinet/ip.h new file mode 100644 index 000000000..edda30a1a --- /dev/null +++ b/examples/rp2040-beremetal/inc/netinet/ip.h @@ -0,0 +1,34 @@ +#ifndef _NETINET_IP_H_STUB +#define _NETINET_IP_H_STUB + +// Minimal netinet/ip.h for RP2040 bare metal. +// Required because sctp_os_userspace.h only defines struct ip +// for _WIN32 or __native_client__. For other platforms (including +// RP2040), it expects to provide the definition. + +#include +#include // for struct in_addr + +#define IPVERSION 4 + +#define IPTOS_LOWDELAY 0x10 +#define IPTOS_THROUGHPUT 0x08 +#define IPTOS_RELIABILITY 0x04 + +struct ip { + uint8_t ip_hl:4; + uint8_t ip_v:4; + uint8_t ip_tos; + uint16_t ip_len; + uint16_t ip_id; + uint16_t ip_off; + uint8_t ip_ttl; + uint8_t ip_p; + uint16_t ip_sum; + struct in_addr ip_src; + struct in_addr ip_dst; +}; + +#define IP_MAXPACKET 65535 + +#endif diff --git a/examples/rp2040-beremetal/inc/pthread.h b/examples/rp2040-beremetal/inc/pthread.h new file mode 100644 index 000000000..76c3215f1 --- /dev/null +++ b/examples/rp2040-beremetal/inc/pthread.h @@ -0,0 +1,65 @@ +#ifndef _PTHREAD_H_STUB +#define _PTHREAD_H_STUB + +// Stub for POSIX threads on RP2040 bare metal (NO_SYS=1, single-threaded). +// +// usrsctp uses pthread types and functions for locking throughout its code. +// In bare metal single-threaded mode, all lock operations are no-ops since +// there's no concurrent access. +// +// The ARM toolchain provides pthread types in sys/_pthreadtypes.h. + +#include +#include // for clock_t needed by sys/_pthreadtypes.h +#include + +// rwlock types not provided by ARM toolchain +#ifndef _PTHREAD_RWLOCK_T_DEFINED +#define _PTHREAD_RWLOCK_T_DEFINED +typedef int pthread_rwlock_t; +typedef int pthread_rwlockattr_t; +#endif + +#ifndef PTHREAD_MUTEX_INITIALIZER +#define PTHREAD_MUTEX_INITIALIZER 0 +#endif +#ifndef PTHREAD_MUTEX_ERRORCHECK +#define PTHREAD_MUTEX_ERRORCHECK 0 +#endif + +// Mutex stubs - no-op in single-threaded mode +static inline int pthread_mutexattr_init(pthread_mutexattr_t *a) { (void)a; return 0; } +static inline int pthread_mutexattr_destroy(pthread_mutexattr_t *a) { (void)a; return 0; } +static inline int pthread_mutexattr_settype(pthread_mutexattr_t *a, int t) { (void)a; (void)t; return 0; } +static inline int pthread_mutex_init(pthread_mutex_t *m, const pthread_mutexattr_t *a) { (void)m; (void)a; return 0; } +static inline int pthread_mutex_destroy(pthread_mutex_t *m) { (void)m; return 0; } +static inline int pthread_mutex_lock(pthread_mutex_t *m) { (void)m; return 0; } +static inline int pthread_mutex_unlock(pthread_mutex_t *m) { (void)m; return 0; } +static inline int pthread_mutex_trylock(pthread_mutex_t *m) { (void)m; return 0; } + +// RW lock stubs - no-op in single-threaded mode +static inline int pthread_rwlockattr_init(pthread_rwlockattr_t *a) { (void)a; return 0; } +static inline int pthread_rwlockattr_destroy(pthread_rwlockattr_t *a) { (void)a; return 0; } +static inline int pthread_rwlock_init(pthread_rwlock_t *l, const pthread_rwlockattr_t *a) { (void)l; (void)a; return 0; } +static inline int pthread_rwlock_destroy(pthread_rwlock_t *l) { (void)l; return 0; } +static inline int pthread_rwlock_rdlock(pthread_rwlock_t *l) { (void)l; return 0; } +static inline int pthread_rwlock_wrlock(pthread_rwlock_t *l) { (void)l; return 0; } +static inline int pthread_rwlock_unlock(pthread_rwlock_t *l) { (void)l; return 0; } +static inline int pthread_rwlock_tryrdlock(pthread_rwlock_t *l) { (void)l; return 0; } +static inline int pthread_rwlock_trywrlock(pthread_rwlock_t *l) { (void)l; return 0; } + +// Condition variable stubs - no-op in single-threaded mode +static inline int pthread_cond_init(pthread_cond_t *c, const pthread_condattr_t *a) { (void)c; (void)a; return 0; } +static inline int pthread_cond_destroy(pthread_cond_t *c) { (void)c; return 0; } +static inline int pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m) { (void)c; (void)m; return 0; } +static inline int pthread_cond_signal(pthread_cond_t *c) { (void)c; return 0; } +static inline int pthread_cond_broadcast(pthread_cond_t *c) { (void)c; return 0; } + +// Thread stubs - no-op in single-threaded mode +static inline pthread_t pthread_self(void) { return 0; } +static inline int pthread_join(pthread_t t, void **v) { (void)t; (void)v; return 0; } +static inline int pthread_create(pthread_t *t, const pthread_attr_t *a, void *(*f)(void *), void *arg) { + (void)t; (void)a; (void)f; (void)arg; return 0; +} + +#endif diff --git a/examples/rp2040-beremetal/inc/sys/ioctl.h b/examples/rp2040-beremetal/inc/sys/ioctl.h new file mode 100644 index 000000000..f57f12212 --- /dev/null +++ b/examples/rp2040-beremetal/inc/sys/ioctl.h @@ -0,0 +1,10 @@ +#ifndef _SYS_IOCTL_H_STUB +#define _SYS_IOCTL_H_STUB + +// Empty stub - only needed for compilation. +// sctp_os_userspace.h includes , but ioctl() is only +// called after socket() succeeds in sctp_userspace.c. Since we use +// lwIP raw API (not BSD sockets), socket() is not available and +// the ioctl code path is never executed. + +#endif diff --git a/examples/rp2040-beremetal/inc/sys/select.h b/examples/rp2040-beremetal/inc/sys/select.h new file mode 100644 index 000000000..fc473d8b6 --- /dev/null +++ b/examples/rp2040-beremetal/inc/sys/select.h @@ -0,0 +1,80 @@ +#ifndef _SYS_SELECT_H_STUB +#define _SYS_SELECT_H_STUB + +// Stub for select() on RP2040 bare metal. +// +// libpeermx uses select() to poll sockets for incoming data. In RP2040 +// bare metal mode, lwIP callbacks directly fill receive buffers. +// +// This stub polls cyw43_arch_poll() repeatedly during the timeout period, +// giving lwIP time to process incoming packets and fire callbacks. + +#include +#include + +// Forward declarations to avoid header inclusion order issues +extern void cyw43_arch_poll(void); +extern uint64_t time_us_64(void); +extern void sleep_us(uint64_t us); + +// fd_set for select() - only define if not already defined +#ifndef FD_SETSIZE +#define FD_SETSIZE 16 +#endif + +#ifndef _FD_SET_DEFINED +#define _FD_SET_DEFINED +typedef struct { + unsigned long fds_bits[1]; +} fd_set; +#endif + +#ifndef FD_ZERO +#define FD_ZERO(set) ((set)->fds_bits[0] = 0) +#endif +#ifndef FD_SET +#define FD_SET(fd, set) ((set)->fds_bits[0] |= (1UL << (fd))) +#endif +#ifndef FD_CLR +#define FD_CLR(fd, set) ((set)->fds_bits[0] &= ~(1UL << (fd))) +#endif +#ifndef FD_ISSET +#define FD_ISSET(fd, set) ((set)->fds_bits[0] & (1UL << (fd))) +#endif + +// select stub - poll with timeout +static inline int select(int nfds, fd_set *readfds, fd_set *writefds, + fd_set *exceptfds, struct timeval *timeout) { + (void)nfds; + (void)writefds; + (void)exceptfds; + + if (!readfds || !readfds->fds_bits[0]) { + return 0; + } + + // Calculate timeout in microseconds + uint64_t timeout_us = 0; + if (timeout) { + timeout_us = (uint64_t)timeout->tv_sec * 1000000 + timeout->tv_usec; + } + + // Poll with timeout - give lwIP time to process incoming packets + uint64_t start = time_us_64(); + do { + cyw43_arch_poll(); + if (timeout_us > 0) { + uint64_t elapsed = time_us_64() - start; + if (elapsed >= timeout_us) { + break; + } + // Small sleep to avoid busy loop + sleep_us(100); + } + } while (timeout_us > 0 && (time_us_64() - start) < timeout_us); + + // Return 1 to indicate caller should try recv + return 1; +} + +#endif diff --git a/examples/rp2040-beremetal/inc/sys/socket.h b/examples/rp2040-beremetal/inc/sys/socket.h new file mode 100644 index 000000000..9d92c006d --- /dev/null +++ b/examples/rp2040-beremetal/inc/sys/socket.h @@ -0,0 +1,276 @@ +#ifndef _SYS_SOCKET_H_STUB +#define _SYS_SOCKET_H_STUB + +//============================================================================= +// Minimal socket types stub for RP2040 bare metal (NO_SYS=1, LWIP_SOCKET=0) +// +// This header provides type definitions and constants required by usrsctp +// to compile. We use lwIP raw API instead of BSD sockets, so no actual +// socket functions are implemented here. +// +// usrsctp uses AF_CONN mode with UDP encapsulation - it doesn't use the +// BSD socket API internally, but its headers reference these types. +//============================================================================= + +#include +#include + +// lwIP provides in_addr, in6_addr, and related types +#include + +//----------------------------------------------------------------------------- +// Basic socket types - used throughout usrsctp headers +//----------------------------------------------------------------------------- +#ifndef __SOCKLEN_T_DEFINED +#define __SOCKLEN_T_DEFINED +typedef uint32_t socklen_t; +#endif + +#ifndef __SA_FAMILY_T_DEFINED +#define __SA_FAMILY_T_DEFINED +typedef uint8_t sa_family_t; +#endif + +// ssize_t is already defined in lwip/arch.h + +//----------------------------------------------------------------------------- +// Address families - used by usrsctp for socket address handling +//----------------------------------------------------------------------------- +#ifndef AF_INET +#define AF_INET 2 +#endif +#ifndef AF_INET6 +#define AF_INET6 10 +#endif + +//----------------------------------------------------------------------------- +// Socket types - SOCK_SEQPACKET is used by SCTP +//----------------------------------------------------------------------------- +#ifndef SOCK_STREAM +#define SOCK_STREAM 1 +#endif +#ifndef SOCK_DGRAM +#define SOCK_DGRAM 2 +#endif +#ifndef SOCK_SEQPACKET +#define SOCK_SEQPACKET 5 +#endif + +//----------------------------------------------------------------------------- +// Socket constants - used by usrsctp internally +//----------------------------------------------------------------------------- +#ifndef IPPORT_RESERVED +#define IPPORT_RESERVED 1024 +#endif +#ifndef INET_ADDRSTRLEN +#define INET_ADDRSTRLEN 16 +#endif +#ifndef INET6_ADDRSTRLEN +#define INET6_ADDRSTRLEN 46 +#endif +#ifndef SOMAXCONN +#define SOMAXCONN 128 +#endif + +//----------------------------------------------------------------------------- +// Message flags - used by usrsctp's send/recv operations +// MSG_EOR and MSG_NOTIFICATION are especially important for SCTP +//----------------------------------------------------------------------------- +#ifndef MSG_OOB +#define MSG_OOB 0x01 +#endif +#ifndef MSG_PEEK +#define MSG_PEEK 0x02 +#endif +#ifndef MSG_DONTWAIT +#define MSG_DONTWAIT 0x40 +#endif +#ifndef MSG_EOR +#define MSG_EOR 0x80 +#endif +#ifndef MSG_TRUNC +#define MSG_TRUNC 0x20 +#endif +#ifndef MSG_NOTIFICATION +#define MSG_NOTIFICATION 0x1000 +#endif + +//----------------------------------------------------------------------------- +// Protocol levels - referenced by usrsctp socket option handling +//----------------------------------------------------------------------------- +#ifndef IPPROTO_IP +#define IPPROTO_IP 0 +#endif +#ifndef IPPROTO_TCP +#define IPPROTO_TCP 6 +#endif +#ifndef IPPROTO_UDP +#define IPPROTO_UDP 17 +#endif + +//----------------------------------------------------------------------------- +// Socket options - used by usrsctp's setsockopt/getsockopt +//----------------------------------------------------------------------------- +#ifndef SOL_SOCKET +#define SOL_SOCKET 0xfff +#endif +#ifndef SO_LINGER +#define SO_LINGER 0x0080 +#endif +#ifndef SO_REUSEADDR +#define SO_REUSEADDR 0x0004 +#endif +#ifndef SO_RCVBUF +#define SO_RCVBUF 0x1002 +#endif +#ifndef SO_SNDBUF +#define SO_SNDBUF 0x1001 +#endif +#ifndef SO_ERROR +#define SO_ERROR 0x1007 +#endif + +//----------------------------------------------------------------------------- +// Shutdown flags - used by usrsctp's shutdown handling +//----------------------------------------------------------------------------- +#ifndef SHUT_RD +#define SHUT_RD 0 +#endif +#ifndef SHUT_WR +#define SHUT_WR 1 +#endif +#ifndef SHUT_RDWR +#define SHUT_RDWR 2 +#endif + +//----------------------------------------------------------------------------- +// Linger structure - used with SO_LINGER option +//----------------------------------------------------------------------------- +#ifndef __LINGER_DEFINED +#define __LINGER_DEFINED +struct linger { + int l_onoff; + int l_linger; +}; +#endif + +//----------------------------------------------------------------------------- +// Socket address structures - fundamental types used throughout usrsctp +//----------------------------------------------------------------------------- +#ifndef __SOCKADDR_DEFINED +#define __SOCKADDR_DEFINED +struct sockaddr { + uint8_t sa_len; + uint8_t sa_family; + char sa_data[14]; +}; +#endif + +#ifndef __SOCKADDR_IN_DEFINED +#define __SOCKADDR_IN_DEFINED +struct sockaddr_in { + uint8_t sin_len; + uint8_t sin_family; + uint16_t sin_port; + struct in_addr sin_addr; + char sin_zero[8]; +}; +#endif + +#ifndef __SOCKADDR_IN6_DEFINED +#define __SOCKADDR_IN6_DEFINED +struct sockaddr_in6 { + uint8_t sin6_len; + uint8_t sin6_family; + uint16_t sin6_port; + uint32_t sin6_flowinfo; + struct in6_addr sin6_addr; + uint32_t sin6_scope_id; +}; +#endif + +#ifndef __SOCKADDR_STORAGE_DEFINED +#define __SOCKADDR_STORAGE_DEFINED +struct sockaddr_storage { + uint8_t ss_len; + uint8_t ss_family; + char __ss_padding[126]; +}; +#endif + +//----------------------------------------------------------------------------- +// I/O vector for scatter/gather - used by usrsctp's message handling +//----------------------------------------------------------------------------- +#ifndef __IOVEC_DEFINED +#define __IOVEC_DEFINED +struct iovec { + void *iov_base; + size_t iov_len; +}; +#endif + +#ifndef UIO_MAXIOV +#define UIO_MAXIOV 1024 +#endif + +//----------------------------------------------------------------------------- +// Control message header and macros - used for SCTP ancillary data +// (sndrcvinfo, notifications, etc.) +//----------------------------------------------------------------------------- +#ifndef __CMSGHDR_DEFINED +#define __CMSGHDR_DEFINED +struct cmsghdr { + socklen_t cmsg_len; + int cmsg_level; + int cmsg_type; +}; +#endif + +#ifndef CMSG_ALIGN +#define CMSG_ALIGN(len) (((len) + sizeof(long) - 1) & ~(sizeof(long) - 1)) +#endif +#ifndef CMSG_SPACE +#define CMSG_SPACE(len) (CMSG_ALIGN(sizeof(struct cmsghdr)) + CMSG_ALIGN(len)) +#endif +#ifndef CMSG_LEN +#define CMSG_LEN(len) (CMSG_ALIGN(sizeof(struct cmsghdr)) + (len)) +#endif +#ifndef CMSG_DATA +#define CMSG_DATA(cmsg) ((unsigned char *)((struct cmsghdr *)(cmsg) + 1)) +#endif +#ifndef CMSG_FIRSTHDR +#define CMSG_FIRSTHDR(mhdr) \ + ((size_t)(mhdr)->msg_controllen >= sizeof(struct cmsghdr) ? \ + (struct cmsghdr *)(mhdr)->msg_control : (struct cmsghdr *)0) +#endif +#ifndef CMSG_NXTHDR +#define CMSG_NXTHDR(mhdr, cmsg) \ + (((char *)(cmsg) + CMSG_ALIGN((cmsg)->cmsg_len) + sizeof(struct cmsghdr) > \ + (char *)(mhdr)->msg_control + (mhdr)->msg_controllen) ? \ + (struct cmsghdr *)0 : \ + (struct cmsghdr *)((char *)(cmsg) + CMSG_ALIGN((cmsg)->cmsg_len))) +#endif + +//----------------------------------------------------------------------------- +// Message header structure - used by usrsctp's recvmsg/sendmsg +//----------------------------------------------------------------------------- +#ifndef __MSGHDR_DEFINED +#define __MSGHDR_DEFINED +struct msghdr { + void *msg_name; + socklen_t msg_namelen; + struct iovec *msg_iov; + int msg_iovlen; + void *msg_control; + socklen_t msg_controllen; + int msg_flags; +}; +#endif + +//----------------------------------------------------------------------------- +// IP address conversion functions - implemented in lwIP or our stubs +//----------------------------------------------------------------------------- +int inet_pton(int af, const char *src, void *dst); +const char *inet_ntop(int af, const void *src, char *dst, uint32_t size); + +#endif // _SYS_SOCKET_H_STUB diff --git a/examples/rp2040-beremetal/inc/sys/time.h b/examples/rp2040-beremetal/inc/sys/time.h new file mode 100644 index 000000000..cc9d71d40 --- /dev/null +++ b/examples/rp2040-beremetal/inc/sys/time.h @@ -0,0 +1,60 @@ +#ifndef _SYS_TIME_H_STUB +#define _SYS_TIME_H_STUB + +// Stub for sys/time.h on RP2040 bare metal. +// Provides struct timeval and timer macros for usrsctp. + +#include + +#ifndef _TIMEVAL_DEFINED +#define _TIMEVAL_DEFINED +struct timeval { + long tv_sec; + long tv_usec; +}; +#endif + +// Timer comparison and arithmetic macros used by usrsctp +#ifndef timercmp +#define timercmp(a, b, CMP) \ + (((a)->tv_sec == (b)->tv_sec) ? \ + ((a)->tv_usec CMP (b)->tv_usec) : \ + ((a)->tv_sec CMP (b)->tv_sec)) +#endif + +#ifndef timerclear +#define timerclear(tvp) ((tvp)->tv_sec = (tvp)->tv_usec = 0) +#endif + +#ifndef timerisset +#define timerisset(tvp) ((tvp)->tv_sec || (tvp)->tv_usec) +#endif + +#ifndef timeradd +#define timeradd(a, b, result) \ + do { \ + (result)->tv_sec = (a)->tv_sec + (b)->tv_sec; \ + (result)->tv_usec = (a)->tv_usec + (b)->tv_usec; \ + if ((result)->tv_usec >= 1000000) { \ + ++(result)->tv_sec; \ + (result)->tv_usec -= 1000000; \ + } \ + } while (0) +#endif + +#ifndef timersub +#define timersub(a, b, result) \ + do { \ + (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \ + (result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \ + if ((result)->tv_usec < 0) { \ + --(result)->tv_sec; \ + (result)->tv_usec += 1000000; \ + } \ + } while (0) +#endif + +// gettimeofday - implemented in inet_compat.c +int gettimeofday(struct timeval *tv, void *tz); + +#endif diff --git a/examples/rp2040-beremetal/inc/sys/uio.h b/examples/rp2040-beremetal/inc/sys/uio.h new file mode 100644 index 000000000..154aa3c1d --- /dev/null +++ b/examples/rp2040-beremetal/inc/sys/uio.h @@ -0,0 +1,9 @@ +#ifndef _SYS_UIO_H_STUB +#define _SYS_UIO_H_STUB + +// Redirect to our sys/socket.h stub which defines struct iovec. +// UIO_MAXIOV is also defined in usrsctp's user_socketvar.h. +// Required by sctp_os_userspace.h. +#include + +#endif diff --git a/examples/rp2040-beremetal/inc/tusb_config.h b/examples/rp2040-beremetal/inc/tusb_config.h new file mode 100644 index 000000000..db6e5ce92 --- /dev/null +++ b/examples/rp2040-beremetal/inc/tusb_config.h @@ -0,0 +1,8 @@ +#pragma once + +/* CFG_TUSB_MCU is auto-detected by pico-sdk based on PICO_BOARD */ +#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED) + +#define CFG_TUD_ENDPOINT0_SIZE 64 +#define CFG_TUD_HID 1 +#define CFG_TUD_HID_EP_BUFSIZE 16 diff --git a/examples/rp2040-beremetal/inet_compat.c b/examples/rp2040-beremetal/inet_compat.c new file mode 100644 index 000000000..d3137d623 --- /dev/null +++ b/examples/rp2040-beremetal/inet_compat.c @@ -0,0 +1,223 @@ +// POSIX compatibility functions for RP2040 bare metal. +// +// Provides inet_pton, inet_ntop, gettimeofday, localtime_r, getaddrinfo for: +// - libpeermx/src/address.c (IP address parsing/formatting) +// - libpeermx/src/ports.c (DNS resolution) +// - usrsctp internal address and time handling + +#include +#include +#include +#include +#include +#include "pico/time.h" +#include "pico/cyw43_arch.h" +#include "lwip/dns.h" +#include "lwip/ip_addr.h" +#include "sys/socket.h" +#include "sys/time.h" +#include "netdb.h" + +//============================================================================= +// inet_pton / inet_ntop - IP address conversion +// lwIP only provides these when LWIP_SOCKET=1 +//============================================================================= + +int inet_pton(int af, const char *src, void *dst) { + if (af == AF_INET) { + struct in_addr *addr = (struct in_addr *)dst; + unsigned int a, b, c, d; + if (sscanf(src, "%u.%u.%u.%u", &a, &b, &c, &d) == 4) { + if (a <= 255 && b <= 255 && c <= 255 && d <= 255) { + addr->s_addr = ((uint32_t)a) | ((uint32_t)b << 8) | + ((uint32_t)c << 16) | ((uint32_t)d << 24); + return 1; + } + } + return 0; + } else if (af == AF_INET6) { + // Simplified IPv6 parsing - only full form supported + struct in6_addr *addr = (struct in6_addr *)dst; + unsigned int parts[8]; + if (sscanf(src, "%x:%x:%x:%x:%x:%x:%x:%x", + &parts[0], &parts[1], &parts[2], &parts[3], + &parts[4], &parts[5], &parts[6], &parts[7]) == 8) { + for (int i = 0; i < 8; i++) { + addr->s6_addr[i*2] = (parts[i] >> 8) & 0xff; + addr->s6_addr[i*2+1] = parts[i] & 0xff; + } + return 1; + } + return 0; + } + return -1; +} + +const char *inet_ntop(int af, const void *src, char *dst, uint32_t size) { + if (af == AF_INET) { + const struct in_addr *addr = (const struct in_addr *)src; + uint32_t ip = addr->s_addr; + int len = snprintf(dst, size, "%u.%u.%u.%u", + ip & 0xff, (ip >> 8) & 0xff, + (ip >> 16) & 0xff, (ip >> 24) & 0xff); + if (len < 0 || (uint32_t)len >= size) return NULL; + return dst; + } else if (af == AF_INET6) { + const struct in6_addr *addr = (const struct in6_addr *)src; + int len = snprintf(dst, size, "%02x%02x:%02x%02x:%02x%02x:%02x%02x:" + "%02x%02x:%02x%02x:%02x%02x:%02x%02x", + addr->s6_addr[0], addr->s6_addr[1], + addr->s6_addr[2], addr->s6_addr[3], + addr->s6_addr[4], addr->s6_addr[5], + addr->s6_addr[6], addr->s6_addr[7], + addr->s6_addr[8], addr->s6_addr[9], + addr->s6_addr[10], addr->s6_addr[11], + addr->s6_addr[12], addr->s6_addr[13], + addr->s6_addr[14], addr->s6_addr[15]); + if (len < 0 || (uint32_t)len >= size) return NULL; + return dst; + } + return NULL; +} + +//============================================================================= +// gettimeofday - using Pico SDK time functions +//============================================================================= + +int gettimeofday(struct timeval *tv, void *tz) { + (void)tz; + if (tv) { + uint64_t us = to_us_since_boot(get_absolute_time()); + tv->tv_sec = us / 1000000; + tv->tv_usec = us % 1000000; + } + return 0; +} + +//============================================================================= +// localtime_r - stub for usrsctp_dumppacket logging +// Returns a zeroed struct tm (not accurate, but sufficient for logging) +//============================================================================= + +struct tm *localtime_r(const time_t *timep, struct tm *result) { + (void)timep; + if (result) { + memset(result, 0, sizeof(struct tm)); + result->tm_mday = 1; + result->tm_year = 70; // 1970 + } + return result; +} + +//============================================================================= +// nanosleep - for usrsctp timer +//============================================================================= + +int nanosleep(const struct timespec *req, struct timespec *rem) { + (void)rem; + if (req) { + uint64_t us = (uint64_t)req->tv_sec * 1000000 + req->tv_nsec / 1000; + sleep_us(us); + } + return 0; +} + +//============================================================================= +// getaddrinfo / freeaddrinfo - DNS resolution using lwIP +//============================================================================= + +// DNS callback state +static volatile int dns_done = 0; +static ip_addr_t dns_result; + +static void dns_callback(const char *name, const ip_addr_t *ipaddr, void *arg) { + (void)name; + (void)arg; + if (ipaddr) { + dns_result = *ipaddr; + } else { + ip_addr_set_zero(&dns_result); + } + dns_done = 1; +} + +int getaddrinfo_impl(const char *node, const char *service, + const struct addrinfo *hints, + struct addrinfo **res) { + (void)service; + (void)hints; + + if (!node || !res) { + return EAI_FAIL; + } + + // Try to parse as IP address first + ip_addr_t addr; + if (ipaddr_aton(node, &addr)) { + // It's already an IP address + struct addrinfo *ai = (struct addrinfo *)calloc(1, sizeof(struct addrinfo) + sizeof(struct sockaddr_in)); + if (!ai) return EAI_FAIL; + + struct sockaddr_in *sa = (struct sockaddr_in *)(ai + 1); + sa->sin_family = AF_INET; + sa->sin_addr.s_addr = ip_addr_get_ip4_u32(&addr); + + ai->ai_family = AF_INET; + ai->ai_socktype = SOCK_STREAM; + ai->ai_addrlen = sizeof(struct sockaddr_in); + ai->ai_addr = (struct sockaddr *)sa; + ai->ai_next = NULL; + + *res = ai; + return 0; + } + + // DNS lookup + dns_done = 0; + ip_addr_set_zero(&dns_result); + + err_t err = dns_gethostbyname(node, &dns_result, dns_callback, NULL); + + if (err == ERR_OK) { + // Result was cached + dns_done = 1; + } else if (err == ERR_INPROGRESS) { + // Wait for DNS callback with timeout + uint32_t start = to_ms_since_boot(get_absolute_time()); + while (!dns_done) { + cyw43_arch_poll(); + sleep_ms(10); + if (to_ms_since_boot(get_absolute_time()) - start > 5000) { + // 5 second timeout + return EAI_FAIL; + } + } + } else { + return EAI_FAIL; + } + + if (ip_addr_isany(&dns_result)) { + return EAI_NONAME; + } + + // Allocate result + struct addrinfo *ai = (struct addrinfo *)calloc(1, sizeof(struct addrinfo) + sizeof(struct sockaddr_in)); + if (!ai) return EAI_FAIL; + + struct sockaddr_in *sa = (struct sockaddr_in *)(ai + 1); + sa->sin_family = AF_INET; + sa->sin_addr.s_addr = ip_addr_get_ip4_u32(&dns_result); + + ai->ai_family = AF_INET; + ai->ai_socktype = SOCK_STREAM; + ai->ai_addrlen = sizeof(struct sockaddr_in); + ai->ai_addr = (struct sockaddr *)sa; + ai->ai_next = NULL; + + *res = ai; + return 0; +} + +void freeaddrinfo_impl(struct addrinfo *res) { + free(res); +} diff --git a/examples/rp2040-beremetal/libsrtp.patch b/examples/rp2040-beremetal/libsrtp.patch new file mode 100644 index 000000000..5bac2f57d --- /dev/null +++ b/examples/rp2040-beremetal/libsrtp.patch @@ -0,0 +1,13 @@ +diff --git a/include/srtp.h b/include/srtp.h +index fa34daf..222ef69 100644 +--- a/include/srtp.h ++++ b/include/srtp.h +@@ -614,7 +614,7 @@ srtp_err_status_t srtp_add_stream(srtp_t session, const srtp_policy_t *policy); + * - [other] otherwise. + * + */ +-srtp_err_status_t srtp_remove_stream(srtp_t session, unsigned int ssrc); ++srtp_err_status_t srtp_remove_stream(srtp_t session, uint32_t ssrc); + + /** + * @brief srtp_update() updates all streams in the session. diff --git a/examples/rp2040-beremetal/main.c b/examples/rp2040-beremetal/main.c new file mode 100644 index 000000000..e29a6e782 --- /dev/null +++ b/examples/rp2040-beremetal/main.c @@ -0,0 +1,484 @@ +#include +#include +#include + +#include "pico/stdlib.h" +#include "hardware/uart.h" +#include "hardware/gpio.h" +#include "pico/cyw43_arch.h" +#include "bsp/board.h" +#include "lwip/dns.h" +#include "lwip/ip_addr.h" + +#include "peer.h" + +//============================================================================= +// Configuration - set via environment variables or defaults below +// Build with: SIGNALING_URL=... WIFI_SSID=... cmake .. +//============================================================================= +#ifndef WIFI_SSID +#define WIFI_SSID "your-wifi-ssid" +#endif +#ifndef WIFI_PASSWORD +#define WIFI_PASSWORD "your-wifi-password" +#endif +#ifndef SIGNALING_URL +#define SIGNALING_URL "http://localhost:8080/webrtc" +#endif +#ifndef SIGNALING_TOKEN +#define SIGNALING_TOKEN "" +#endif + +//============================================================================= +// Global state +//============================================================================= +static PeerConnection* g_pc = NULL; +static PeerConnectionState g_state = PEER_CONNECTION_NEW; +static uint32_t g_dcmsg_time = 0; +static int g_count = 0; + +//============================================================================= +// Timing measurement +//============================================================================= +static uint32_t g_time_boot = 0; // Boot time (reference) +static uint32_t g_time_wifi_start = 0; // WiFi connection start +static uint32_t g_time_wifi_done = 0; // WiFi connected +static uint32_t g_time_signaling_start = 0;// Signaling server connect start +static uint32_t g_time_ice_checking = 0; // ICE checking started +static uint32_t g_time_ice_connected = 0; // ICE connected (DTLS starts) +static uint32_t g_time_ice_completed = 0; // ICE completed +static uint32_t g_time_datachannel_open = 0; // DataChannel opened (SCTP ready) +static uint32_t g_time_first_rx = 0; // First message received +static uint32_t g_time_first_tx = 0; // First message sent +static bool g_first_rx_logged = false; +static bool g_first_tx_logged = false; + +#define LOG_TIMING(label) printf("[TIMING] %s: %lu ms\n", (label), (unsigned long)board_millis()) + +//============================================================================= +// LED blink state machine (non-blocking) +//============================================================================= +typedef struct { + uint32_t interval_ms; // Blink interval + int remaining; // Remaining blink count + uint32_t last_toggle; // Last toggle time + bool led_on; // Current LED state +} LedBlinkState; + +static LedBlinkState g_led_blink = {0, 0, 0, false}; + +// Start a blink pattern +static void led_blink_start(uint32_t interval_ms, int count) { + g_led_blink.interval_ms = interval_ms; + g_led_blink.remaining = count * 2; // Each blink = on + off + g_led_blink.last_toggle = board_millis(); + g_led_blink.led_on = true; + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1); +} + +// Process LED blink (call from main loop) +static void led_blink_loop(void) { + if (g_led_blink.remaining <= 0) return; + + uint32_t now = board_millis(); + if ((now - g_led_blink.last_toggle) >= g_led_blink.interval_ms) { + g_led_blink.last_toggle = now; + g_led_blink.remaining--; + + if (g_led_blink.remaining > 0) { + g_led_blink.led_on = !g_led_blink.led_on; + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, g_led_blink.led_on ? 1 : 0); + } else { + // Done blinking - turn off LED + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0); + g_led_blink.led_on = false; + } + } +} + +// TX: 100ms x 3 blinks (ピッ・ピッ・ピッ) +static void led_blink_tx(void) { + led_blink_start(100, 3); +} + +// RX: 20ms x 15 blinks (ピピピピピ...) +static void led_blink_rx(void) { + led_blink_start(20, 15); +} + +//============================================================================= +// Callbacks +//============================================================================= +static void onconnectionstatechange(PeerConnectionState state, void* data) { + uint32_t now = board_millis(); + printf("[TIMING] State -> %s: %lu ms\n", peer_connection_state_to_string(state), (unsigned long)now); + + switch (state) { + case PEER_CONNECTION_CHECKING: + g_time_ice_checking = now; + printf("[TIMING] ICE checking started: %lu ms (WiFi+%lu ms)\n", + (unsigned long)now, (unsigned long)(now - g_time_wifi_done)); + break; + case PEER_CONNECTION_CONNECTED: + g_time_ice_connected = now; + printf("[TIMING] ICE connected (DTLS starting): %lu ms (ICE took %lu ms)\n", + (unsigned long)now, (unsigned long)(now - g_time_ice_checking)); + break; + case PEER_CONNECTION_COMPLETED: + g_time_ice_completed = now; + printf("[TIMING] ICE completed: %lu ms\n", (unsigned long)now); + break; + default: + break; + } + g_state = state; +} + +static void onopen(void* user_data) { + g_time_datachannel_open = board_millis(); + printf("[TIMING] DataChannel opened (SCTP ready): %lu ms\n", (unsigned long)g_time_datachannel_open); + printf("[TIMING] DTLS+SCTP took: %lu ms (from ICE connected)\n", + (unsigned long)(g_time_datachannel_open - g_time_ice_connected)); + printf("[TIMING] Total connection time: %lu ms (from boot)\n", + (unsigned long)g_time_datachannel_open); +} + +static void onclose(void* user_data) { + printf("DataChannel closed\n"); +} + +static void onmessage(char* msg, size_t len, void* user_data, uint16_t sid) { + // Log first RX timing + if (!g_first_rx_logged) { + g_time_first_rx = board_millis(); + g_first_rx_logged = true; + printf("[TIMING] First message RX: %lu ms (from boot)\n", (unsigned long)g_time_first_rx); + printf("[TIMING] Time from DataChannel open to first RX: %lu ms\n", + (unsigned long)(g_time_first_rx - g_time_datachannel_open)); + } + + printf("Message [%d]: %.*s", sid, (int)len, msg); + + // RX blink: 20ms x 15 (ピピピピピ...) + led_blink_rx(); + + if (len >= 4 && strncmp(msg, "ping", 4) == 0) { + printf(" -> pong"); + peer_connection_datachannel_send(g_pc, "pong", 4); + // TX blink: 100ms x 3 (ピッ・ピッ・ピッ) + led_blink_tx(); + } + printf("\n"); +} + +//============================================================================= +// WiFi timing variables +//============================================================================= +static uint32_t g_time_wifi_scan_start = 0; +static uint32_t g_time_wifi_auth_start = 0; +static uint32_t g_time_wifi_auth_done = 0; +static uint32_t g_time_dhcp_start = 0; +static uint32_t g_time_dhcp_done = 0; + +//============================================================================= +// WiFi initialization (detailed timing) +//============================================================================= +static int wifi_init(void) { + int last_status = CYW43_LINK_DOWN; + int status; + uint32_t now; + uint32_t timeout_start; + + if (cyw43_arch_init()) { + printf("WiFi init failed\n"); + return -1; + } + + // Quick blink to confirm cyw43 init succeeded (disabled for timing measurement) + // for (int i = 0; i < 3; i++) { + // cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1); + // sleep_ms(100); + // cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0); + // sleep_ms(100); + // } + + cyw43_arch_enable_sta_mode(); + + // Start async WiFi connection + g_time_wifi_start = board_millis(); + g_time_wifi_scan_start = g_time_wifi_start; + printf("[TIMING] WiFi scan start: %lu ms\n", (unsigned long)g_time_wifi_scan_start); + printf("Connecting to WiFi '%s'...\n", WIFI_SSID); + + if (cyw43_arch_wifi_connect_async(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK) != 0) { + printf("WiFi connect_async failed\n"); + return -1; + } + + // Poll for connection status with detailed timing + timeout_start = board_millis(); + while (1) { + cyw43_arch_poll(); + sleep_ms(10); + + now = board_millis(); + if ((now - timeout_start) > 30000) { + printf("WiFi connection timeout\n"); + return -1; + } + + status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + + // Detect state transitions + if (status != last_status) { + switch (status) { + case CYW43_LINK_JOIN: + // Auth/Assoc completed (scan also done at this point) + g_time_wifi_auth_done = now; + printf("[TIMING] WiFi scan done: %lu ms (took %lu ms)\n", + (unsigned long)now, (unsigned long)(now - g_time_wifi_scan_start)); + printf("[TIMING] WiFi auth/assoc done: %lu ms\n", (unsigned long)now); + break; + + case CYW43_LINK_NOIP: + // Link up, waiting for DHCP + g_time_dhcp_start = now; + printf("[TIMING] DHCP start: %lu ms\n", (unsigned long)now); + break; + + case CYW43_LINK_UP: + // DHCP completed, got IP + g_time_dhcp_done = now; + g_time_wifi_done = now; + printf("[TIMING] DHCP bound: %lu ms (took %lu ms)\n", + (unsigned long)now, (unsigned long)(now - g_time_dhcp_start)); + printf("[TIMING] WiFi fully connected: %lu ms (total %lu ms)\n", + (unsigned long)now, (unsigned long)(now - g_time_wifi_start)); + goto wifi_connected; + + case CYW43_LINK_FAIL: + printf("WiFi connection failed\n"); + return -1; + + case CYW43_LINK_NONET: + printf("WiFi: No matching SSID found\n"); + return -1; + + case CYW43_LINK_BADAUTH: + printf("WiFi: Authentication failure\n"); + return -1; + + default: + break; + } + last_status = status; + } + } + +wifi_connected: + // Blink LED to confirm WiFi connected (disabled for timing measurement) + // for (int i = 0; i < 5; i++) { + // cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1); + // sleep_ms(100); + // cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0); + // sleep_ms(100); + // } + // cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1); + + return 0; +} + +//============================================================================= +// DNS resolution timing +//============================================================================= +static uint32_t g_time_dns_start = 0; +static uint32_t g_time_dns_done = 0; +static volatile bool g_dns_done = false; +static ip_addr_t g_dns_result; + +static void dns_callback(const char *name, const ip_addr_t *ipaddr, void *arg) { + g_time_dns_done = board_millis(); + if (ipaddr) { + g_dns_result = *ipaddr; + printf("[TIMING] DNS resolved: %lu ms (took %lu ms) -> %s\n", + (unsigned long)g_time_dns_done, + (unsigned long)(g_time_dns_done - g_time_dns_start), + ipaddr_ntoa(ipaddr)); + } else { + printf("[TIMING] DNS resolution failed: %lu ms\n", (unsigned long)g_time_dns_done); + } + g_dns_done = true; +} + +// Extract hostname from URL (e.g., "http://example.com:8080/path" -> "example.com") +static void extract_hostname(const char *url, char *hostname, size_t hostname_size) { + const char *start = url; + const char *end; + + // Skip protocol + if (strncmp(url, "http://", 7) == 0) start = url + 7; + else if (strncmp(url, "https://", 8) == 0) start = url + 8; + + // Find end of hostname (port or path) + end = start; + while (*end && *end != ':' && *end != '/') end++; + + size_t len = end - start; + if (len >= hostname_size) len = hostname_size - 1; + memcpy(hostname, start, len); + hostname[len] = '\0'; +} + +//============================================================================= +// WebRTC initialization +//============================================================================= +static int webrtc_init(void) { + PeerConfiguration config = { + .ice_servers = { + {.urls = "stun:stun.l.google.com:19302"}, + }, + .datachannel = DATA_CHANNEL_STRING, + .video_codec = CODEC_NONE, + .audio_codec = CODEC_NONE, + }; + + printf("Initializing WebRTC...\n"); + peer_init(); + + g_pc = peer_connection_create(&config); + if (!g_pc) { + printf("Failed to create PeerConnection\n"); + return -1; + } + + peer_connection_oniceconnectionstatechange(g_pc, onconnectionstatechange); + peer_connection_ondatachannel(g_pc, onmessage, onopen, onclose); + + // DNS resolution test for signaling server + char hostname[128]; + extract_hostname(SIGNALING_URL, hostname, sizeof(hostname)); + + g_time_dns_start = board_millis(); + printf("[TIMING] DNS lookup start: %lu ms (host: %s)\n", (unsigned long)g_time_dns_start, hostname); + + g_dns_done = false; + err_t err = dns_gethostbyname(hostname, &g_dns_result, dns_callback, NULL); + if (err == ERR_OK) { + // Already cached + g_time_dns_done = board_millis(); + printf("[TIMING] DNS resolved (cached): %lu ms -> %s\n", + (unsigned long)g_time_dns_done, ipaddr_ntoa(&g_dns_result)); + } else if (err == ERR_INPROGRESS) { + // Wait for DNS callback + uint32_t dns_timeout = board_millis(); + while (!g_dns_done && (board_millis() - dns_timeout) < 10000) { + cyw43_arch_poll(); + sleep_ms(10); + } + if (!g_dns_done) { + printf("[TIMING] DNS timeout\n"); + } + } else { + printf("[TIMING] DNS lookup failed: err=%d\n", err); + } + + g_time_signaling_start = board_millis(); + printf("[TIMING] Signaling server connect start: %lu ms\n", (unsigned long)g_time_signaling_start); + printf("Connecting to signaling server: %s\n", SIGNALING_URL); + peer_signaling_connect(SIGNALING_URL, + SIGNALING_TOKEN[0] ? SIGNALING_TOKEN : NULL, + g_pc); + + return 0; +} + +//============================================================================= +// Main loop +//============================================================================= +int main() { + uint32_t now; + + // Explicit UART setup for RP2350 compatibility + uart_init(uart0, 115200); + gpio_set_function(0, GPIO_FUNC_UART); // TX + gpio_set_function(1, GPIO_FUNC_UART); // RX + + stdio_init_all(); + + // Wait for serial + sleep_ms(2000); + g_time_boot = board_millis(); + printf("\n\n=== RP2350 WebRTC Demo ===\n"); + printf("[TIMING] Boot complete: %lu ms\n", (unsigned long)g_time_boot); + + // USB HID init + board_init(); + tusb_init(); + + // WiFi init + if (wifi_init() != 0) { + printf("WiFi init failed, halting\n"); + while (1) { + sleep_ms(1000); + } + } + + // WebRTC init + if (webrtc_init() != 0) { + printf("WebRTC init failed, halting\n"); + while (1) { + sleep_ms(1000); + } + } + + printf("Entering main loop...\n"); + + // Main loop - single threaded + while (1) { + // Process lwIP network events + cyw43_arch_poll(); + + // Process signaling + peer_signaling_loop(); + + // Process peer connection + peer_connection_loop(g_pc); + + // Send periodic datachannel message when connected + if (g_state == PEER_CONNECTION_COMPLETED) { + now = board_millis(); + if ((now - g_dcmsg_time) > 10000) { + g_dcmsg_time = now; + char msg[64]; + snprintf(msg, sizeof(msg), "datachannel message: %05d", g_count++); + peer_connection_datachannel_send(g_pc, msg, strlen(msg)); + + // Log first TX timing + if (!g_first_tx_logged) { + g_time_first_tx = board_millis(); + g_first_tx_logged = true; + printf("[TIMING] First message TX: %lu ms (from boot)\n", (unsigned long)g_time_first_tx); + printf("[TIMING] Time from DataChannel open to first TX: %lu ms\n", + (unsigned long)(g_time_first_tx - g_time_datachannel_open)); + } + + // TX blink: 100ms x 3 (ピッ・ピッ・ピッ) + led_blink_tx(); + } + } + + // Process LED blink pattern + led_blink_loop(); + + // Small delay to prevent busy loop + sleep_us(100); + } + + // Cleanup (never reached in bare metal) + peer_signaling_disconnect(); + peer_connection_destroy(g_pc); + peer_deinit(); + cyw43_arch_deinit(); + + return 0; +} diff --git a/examples/rp2040-beremetal/mbedtls.patch b/examples/rp2040-beremetal/mbedtls.patch new file mode 100644 index 000000000..87f907ee3 --- /dev/null +++ b/examples/rp2040-beremetal/mbedtls.patch @@ -0,0 +1,13 @@ +diff --git a/include/mbedtls/mbedtls_config.h b/include/mbedtls/mbedtls_config.h +index 4292b493bd..738b032c03 100644 +--- a/include/mbedtls/mbedtls_config.h ++++ b/include/mbedtls/mbedtls_config.h +@@ -1788,7 +1788,7 @@ + * + * Uncomment this to enable support for use_srtp extension. + */ +-//#define MBEDTLS_SSL_DTLS_SRTP ++#define MBEDTLS_SSL_DTLS_SRTP + + /** + * \def MBEDTLS_SSL_DTLS_CLIENT_PORT_REUSE diff --git a/examples/rp2040-beremetal/pico_sdk_import.cmake b/examples/rp2040-beremetal/pico_sdk_import.cmake new file mode 100644 index 000000000..65f8a6f7d --- /dev/null +++ b/examples/rp2040-beremetal/pico_sdk_import.cmake @@ -0,0 +1,73 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + GIT_SUBMODULES_RECURSE FALSE + ) + else () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + ) + endif () + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/examples/rp2040-beremetal/usrsctp.patch b/examples/rp2040-beremetal/usrsctp.patch new file mode 100644 index 000000000..a84c5f6f6 --- /dev/null +++ b/examples/rp2040-beremetal/usrsctp.patch @@ -0,0 +1,148 @@ +diff --git a/usrsctplib/user_environment.c b/usrsctplib/user_environment.c +index 3deb3ef..3b339d0 100755 +--- a/usrsctplib/user_environment.c ++++ b/usrsctplib/user_environment.c +@@ -374,6 +374,38 @@ read_random(void *buf, size_t size) + return; + } + ++void ++finish_random(void) ++{ ++ return; ++} ++#elif defined(__RP2040_BM__) ++// RP2040 bare metal - use Pico SDK hardware RNG ++#include "pico/rand.h" ++ ++void ++init_random(void) ++{ ++ return; ++} ++ ++void ++read_random(void *buf, size_t size) ++{ ++ uint8_t *p = (uint8_t *)buf; ++ while (size >= 4) { ++ uint32_t r = get_rand_32(); ++ memcpy(p, &r, 4); ++ p += 4; ++ size -= 4; ++ } ++ if (size > 0) { ++ uint32_t r = get_rand_32(); ++ memcpy(p, &r, size); ++ } ++ return; ++} ++ + void + finish_random(void) + { +diff --git a/usrsctplib/user_socket.c b/usrsctplib/user_socket.c +index ce9daed..6053fed 100755 +--- a/usrsctplib/user_socket.c ++++ b/usrsctplib/user_socket.c +@@ -664,6 +664,17 @@ out: + int + getsockaddr(struct sockaddr **namp, caddr_t uaddr, size_t len) + { ++ printf("getsockaddr: namp=%p, uaddr=%p, len=%zu\n", ++ (void*)namp, (void*)uaddr, len); ++ if (len >= 8) { ++ printf("getsockaddr: input bytes[0..7]: %02x %02x %02x %02x %02x %02x %02x %02x\n", ++ ((unsigned char*)uaddr)[0], ((unsigned char*)uaddr)[1], ++ ((unsigned char*)uaddr)[2], ((unsigned char*)uaddr)[3], ++ ((unsigned char*)uaddr)[4], ((unsigned char*)uaddr)[5], ++ ((unsigned char*)uaddr)[6], ((unsigned char*)uaddr)[7]); ++ } ++ printf("getsockaddr: sizeof(struct sockaddr)=%zu, offsetof(sa_data)=%zu\n", ++ sizeof(struct sockaddr), offsetof(struct sockaddr, sa_data)); + struct sockaddr *sa; + int error; + +@@ -673,12 +684,24 @@ getsockaddr(struct sockaddr **namp, caddr_t uaddr, size_t len) + return (EINVAL); + MALLOC(sa, struct sockaddr *, len, M_SONAME, M_WAITOK); + error = copyin(uaddr, sa, len); ++ printf("getsockaddr: copyin returned %d, sa=%p\n", error, (void*)sa); ++ if (len >= 8) { ++ printf("getsockaddr: copied bytes[0..7]: %02x %02x %02x %02x %02x %02x %02x %02x\n", ++ ((unsigned char*)sa)[0], ((unsigned char*)sa)[1], ++ ((unsigned char*)sa)[2], ((unsigned char*)sa)[3], ++ ((unsigned char*)sa)[4], ((unsigned char*)sa)[5], ++ ((unsigned char*)sa)[6], ((unsigned char*)sa)[7]); ++ } + if (error) { + FREE(sa, M_SONAME); + } else { + #ifdef HAVE_SA_LEN ++ printf("getsockaddr: HAVE_SA_LEN defined, setting sa_len=%zu\n", len); + sa->sa_len = len; ++#else ++ printf("getsockaddr: HAVE_SA_LEN not defined\n"); + #endif ++ printf("getsockaddr: sa->sa_family=%d (at offset %zu)\n", sa->sa_family, offsetof(struct sockaddr, sa_family)); + *namp = sa; + } + return (error); +@@ -1476,17 +1499,30 @@ int + usrsctp_bind(struct socket *so, struct sockaddr *name, int namelen) + { + struct sockaddr *sa; ++ int gsa_err, bind_err; ++ ++ printf("usrsctp_bind: so=%p, name=%p, namelen=%d\n", ++ (void*)so, (void*)name, namelen); + + if (so == NULL) { + errno = EBADF; + return (-1); + } +- if ((errno = getsockaddr(&sa, (caddr_t)name, namelen)) != 0) ++ ++ gsa_err = getsockaddr(&sa, (caddr_t)name, namelen); ++ printf("usrsctp_bind: getsockaddr returned %d, sa=%p\n", gsa_err, (void*)sa); ++ if (gsa_err != 0) { ++ errno = gsa_err; + return (-1); ++ } + +- errno = sobind(so, sa); ++ printf("usrsctp_bind: calling sobind, sa->sa_family=%d\n", sa->sa_family); ++ bind_err = sobind(so, sa); ++ printf("usrsctp_bind: sobind returned %d\n", bind_err); + FREE(sa, M_SONAME); +- if (errno) { ++ errno = bind_err; ++ if (bind_err) { ++ printf("usrsctp_bind: failed with errno=%d\n", bind_err); + return (-1); + } else { + return (0); +diff --git a/usrsctplib/usrsctp.h b/usrsctplib/usrsctp.h +index e0c17c3..919a08e 100644 +--- a/usrsctplib/usrsctp.h ++++ b/usrsctplib/usrsctp.h +@@ -43,6 +43,9 @@ extern "C" { + #endif + #include + #include ++#elif defined(CONFIG_USE_LWIP) ++#include ++#include + #else + #include + #include +@@ -120,7 +123,8 @@ struct sctp_common_header { + * tune with other sockaddr_* structures. + */ + #if defined(__APPLE__) || defined(__Bitrig__) || defined(__DragonFly__) || \ +- defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) ++ defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || \ ++ defined(__RP2040_BM__) + struct sockaddr_conn { + uint8_t sconn_len; + uint8_t sconn_family; diff --git a/examples/unity/macos/LibPeerSample/Assets/Editor.meta b/examples/unity/macos/LibPeerSample/Assets/Editor.meta new file mode 100644 index 000000000..43ff1ede7 --- /dev/null +++ b/examples/unity/macos/LibPeerSample/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b2247055e704448caa529904865c0ddb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/examples/unity/macos/LibPeerSample/Assets/Editor/LibPeerUISetup.cs b/examples/unity/macos/LibPeerSample/Assets/Editor/LibPeerUISetup.cs new file mode 100644 index 000000000..39481b244 --- /dev/null +++ b/examples/unity/macos/LibPeerSample/Assets/Editor/LibPeerUISetup.cs @@ -0,0 +1,238 @@ +using UnityEngine; +using UnityEngine.UI; +using UnityEditor; + +public class LibPeerUISetup : Editor +{ + [MenuItem("Tools/LibPeer/Setup UI")] + public static void SetupUI() + { + // Find LibPeerSample component + var libPeerSample = FindObjectOfType(); + if (libPeerSample == null) + { + Debug.LogError("LibPeerSample component not found in scene!"); + return; + } + + // Delete existing Canvas + var existingCanvas = GameObject.Find("Canvas"); + if (existingCanvas != null) + { + DestroyImmediate(existingCanvas); + Debug.Log("Deleted existing Canvas"); + } + + // Delete existing EventSystem + var existingEventSystem = GameObject.Find("EventSystem"); + if (existingEventSystem != null) + { + DestroyImmediate(existingEventSystem); + Debug.Log("Deleted existing EventSystem"); + } + + // Create Canvas + var canvasGO = new GameObject("Canvas"); + var canvas = canvasGO.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + + var scaler = canvasGO.AddComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.referenceResolution = new Vector2(1920, 1080); + scaler.matchWidthOrHeight = 0.5f; + + canvasGO.AddComponent(); + + // Create EventSystem if not exists + if (FindObjectOfType() == null) + { + var eventSystemGO = new GameObject("EventSystem"); + eventSystemGO.AddComponent(); + eventSystemGO.AddComponent(); + } + + // Create URL Input Field + var inputFieldGO = CreateInputField(canvasGO.transform, "UrlInputField", + new Vector2(0, -50), new Vector2(600, 40), "Enter Signaling URL..."); + var inputField = inputFieldGO.GetComponent(); + inputField.text = "https://"; + + // Create Connect Button + var connectBtnGO = CreateButton(canvasGO.transform, "ConnectButton", + new Vector2(-110, -100), new Vector2(200, 40), "Connect", + new Color(0.2f, 0.6f, 0.2f, 1f)); + var connectBtn = connectBtnGO.GetComponent