From d8734e4cb727ae6b97a301b6117c24bc7c580f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Thu, 16 Jul 2026 17:37:25 +0200 Subject: [PATCH 1/5] Add native Windows build of meshtasticd Add [env:native-windows], a Portduino build for Windows x86_64 via the MSYS2 UCRT64 MinGW-w64 toolchain, alongside the existing Linux and macOS native targets. Runs headless in SimRadio mode or against a CH341 USB LoRa adapter. MSVC is not viable: platform-native's builder calls env.Tool("gcc") and the firmware builds as gnu17/gnu++17. Wire the build into main_matrix.yml next to the MacOS job, and allow workflow_dispatch. Link statically. Windows resolves DLLs from System32 before PATH, so an unrelated libusb-1.0.dll installed there by another product's driver package hijacks a dynamically linked build and kills it at startup with STATUS_ENTRYPOINT_NOT_FOUND. Static linking also leaves a single self-contained meshtasticd.exe needing no MSYS2 runtime. -static cannot ride in build_flags, which PlatformIO routes only to the compile step, so extra_scripts/windows_link_flags.py appends it to LINKFLAGS, as wasm_link_flags.py does for native-wasm. CH341 radio support: add a libpinedio backend over WCH's CH341DLL in platform/portduino/windows/, mirroring what wasm/libpinedio_webusb.c does over WebUSB. Ch341Hal is unchanged; only the backend behind the pinedio_* API differs, so the pine64 libch341 lib_dep is ignored for this env. libusb is not usable here: on Windows it can only reach a device bound to WinUSB, which means running Zadig per machine and trusting a self-signed CA, whereas CH341DLL ships with WCH's WHQL-signed CH341PAR driver. The DLL is closed-source and resolved at runtime, so a host without the driver just gets a clear error. The 64-bit library is named CH341DLLA64.DLL; plain CH341DLL.DLL is the 32-bit build. Interrupts are polled at upstream's rate because CH341SetIntRoutine only fires on the CH341's INT# pin, while the adapter wires the radio's DIO1 to D6. HardwareRNG.cpp: use BCryptGenRandom() where Linux uses getrandom() and Darwin uses arc4random_buf(). Not std::random_device, whose libstdc++ Windows backend reports entropy() == 0 and so promises no cryptographic source, and this buffer seeds key material. PortduinoGlue.cpp: derive the MAC from the host's primary adapter via GetAdaptersAddresses(), standing in for the BlueZ path on Linux and the en0 path on macOS. Without it the MAC stays all-zero, device_id is left unset and every user has to pass --hwid. The call lives in WindowsMacAddr.cpp because pulls in RPC/OLE headers that collide with the Arduino API. Also guard the ioctl() include, which only the Linux hardware path uses. USBHal.h: include for gettimeofday(), which was previously reaching it transitively through libusb.h. GpsdSerial.cpp: port the gpsd TCP client to Winsock. PowerHAL.cpp: define the defaults strongly on Windows. PE/COFF has no ELF-style weak definitions; GNU as lowers __attribute__((weak)) to a COFF weak external that the linker treats as undefined, so these would not link. Only nrf52 and nrf54l15 override them. RTC.cpp: copy timeval::tv_sec through a time_t before taking its address, as time_t is 64-bit on Windows while long is 32-bit. MQTT.cpp: take ntohl() from . AdminModule.h: uint32_t for session_time, as uint is a glibc typedef. main.cpp: only run the timedatectl shell-out on Linux. It is systemd-only, so macOS, Windows and WASM all stayed at RTCQualityDevice anyway, and on those hosts it printed a "not recognized" error on every boot. Bump platform-native to 86c62ed for the Windows builder support, which pulls framework-portduino 0fdf803. Point libch341-spi-userspace at meshtastic/libch341-spi-userspace 03bf505, a fork of pine64/libch341-spi-userspace at the previously pinned commit plus fixes for four data races between the caller and the interrupt poll thread (GPIO shadow state read-modify-written outside the mutex, callback pointer re-read after unlocking, a second poll thread spawned on re-attach, and pthread_join on a handle read after unlocking). Those affect the libusb backend, so Linux and macOS, not Windows, which uses the CH341DLL backend added here. Upstream is slow-moving: three PRs have been open since Dec 2024 and main has not moved since January. The Renovate opt-out from #9587 is preserved. Build with the MSYS2 UCRT64 toolchain; see the env comment for the argp prerequisite, which is not packaged for MSYS2's mingw environments. --- .github/workflows/build_windows_bin.yml | 116 +++++ .github/workflows/main_matrix.yml | 12 + extra_scripts/windows_link_flags.py | 17 + src/gps/RTC.cpp | 15 +- src/main.cpp | 4 + src/mesh/HardwareRNG.cpp | 15 + src/modules/AdminModule.h | 2 +- src/mqtt/MQTT.cpp | 4 + src/platform/portduino/GpsdSerial.cpp | 108 ++++- src/platform/portduino/PortduinoGlue.cpp | 24 +- src/platform/portduino/USBHal.h | 1 + src/platform/portduino/WindowsMacAddr.cpp | 65 +++ .../windows/include/libpinedio-usb.h | 79 ++++ .../portduino/windows/libpinedio_ch341dll.c | 418 ++++++++++++++++++ src/power/PowerHAL.cpp | 16 +- variants/native/portduino.ini | 6 +- variants/native/portduino/platformio.ini | 77 ++++ 17 files changed, 956 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/build_windows_bin.yml create mode 100644 extra_scripts/windows_link_flags.py create mode 100644 src/platform/portduino/WindowsMacAddr.cpp create mode 100644 src/platform/portduino/windows/include/libpinedio-usb.h create mode 100644 src/platform/portduino/windows/libpinedio_ch341dll.c diff --git a/.github/workflows/build_windows_bin.yml b/.github/workflows/build_windows_bin.yml new file mode 100644 index 00000000000..31fdc8151ef --- /dev/null +++ b/.github/workflows/build_windows_bin.yml @@ -0,0 +1,116 @@ +name: Build Windows Binary + +on: + workflow_call: + inputs: + windows_ver: + required: false + default: "2025" + type: string + workflow_dispatch: + inputs: + windows_ver: + required: false + default: "2025" + type: string + +permissions: + contents: read + +jobs: + build-Windows: + runs-on: windows-${{ inputs.windows_ver }} + defaults: + run: + # UCRT64 is the MinGW-w64 environment the native-windows env targets. + # (The `msys` environment would link msys-2.0.dll and produce a + # Cygwin-style binary rather than a native one.) + shell: msys2 {0} + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + submodules: recursive + # Keep the token out of .git/config so later steps and artifacts can't leak it. + persist-credentials: false + + - name: Setup MSYS2 / UCRT64 + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + # argp is deliberately absent here - it is not packaged for MSYS2's + # mingw environments and is built from source below. + install: >- + mingw-w64-ucrt-x86_64-gcc + mingw-w64-ucrt-x86_64-pkgconf + mingw-w64-ucrt-x86_64-yaml-cpp + mingw-w64-ucrt-x86_64-libuv + mingw-w64-ucrt-x86_64-jsoncpp + mingw-w64-ucrt-x86_64-openssl + mingw-w64-ucrt-x86_64-libusb + mingw-w64-ucrt-x86_64-cmake + mingw-w64-ucrt-x86_64-ninja + mingw-w64-ucrt-x86_64-python + mingw-w64-ucrt-x86_64-python-pip + git + + # framework-portduino's Arduino.h includes and its main.cpp calls + # argp_parse(). glibc has argp built in and macOS gets it from + # `brew install argp-standalone`, but MSYS2 packages it only for the + # msys runtime (msys/libargp), which can't be linked into a native + # binary - so build it from source. The project ships no install() rules, + # hence the manual copy. + - name: Build and install argp-standalone + run: | + git clone --depth 1 https://github.com/tom42/argp-standalone /tmp/argp + cd /tmp/argp + cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release . + cmake --build build + cp include/argp-standalone/argp.h /ucrt64/include/argp.h + cp build/src/libargp-standalone.a /ucrt64/lib/libargp.a + + - name: Install PlatformIO + run: | + python -m pip install --upgrade pip + pip install platformio + + - name: Get release version string + run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + id: version + + - name: Build for Windows + run: platformio run -e native-windows + env: + PKG_VERSION: ${{ steps.version.outputs.long }} + # Errors in this step should not fail the entire workflow while Windows + # support is in development (mirrors the macOS job). + continue-on-error: true + + - name: List output files + run: ls -lah .pio/build/native-windows/ + + # The env links statically, so this should report only Windows system + # DLLs. A third-party DLL appearing here means the static link regressed + # and the artifact would need those DLLs shipped alongside it. + - name: Verify the binary is self-contained + continue-on-error: true + run: | + objdump -p .pio/build/native-windows/meshtasticd.exe \ + | grep -i 'DLL Name' \ + | grep -viE 'api-ms-win|KERNEL32|WS2_32|ADVAPI32|USER32|msvcrt|bcrypt|IPHLPAPI|SHELL32|ole32|CRYPT32|SETUPAPI|CFGMGR32|WINMM|dbghelp' \ + && { echo "::error::meshtasticd.exe has non-system DLL dependencies (static link regressed)"; exit 1; } \ + || echo "OK: no third-party DLL dependencies" + + - name: Smoke test the binary + continue-on-error: true + run: | + .pio/build/native-windows/meshtasticd.exe --version + + - name: Store binaries as an artifact + uses: actions/upload-artifact@v7 + with: + name: firmware-windows-${{ inputs.windows_ver }}-${{ steps.version.outputs.long }} + overwrite: true + path: | + .pio/build/native-windows/meshtasticd.exe diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index a0097697318..5057b3263da 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -144,6 +144,18 @@ jobs: macos_ver: ${{ matrix.macos_ver }} # secrets: inherit + Windows: + if: ${{ github.event_name != 'schedule' && github.event.inputs.nightly != 'true' }} + strategy: + fail-fast: false + matrix: + windows_ver: + - "2025" # x86_64 + uses: ./.github/workflows/build_windows_bin.yml + with: + windows_ver: ${{ matrix.windows_ver }} + # secrets: inherit + package-pio-deps-native-tft: if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }} uses: ./.github/workflows/package_pio_deps.yml diff --git a/extra_scripts/windows_link_flags.py b/extra_scripts/windows_link_flags.py new file mode 100644 index 00000000000..915305b6cf1 --- /dev/null +++ b/extra_scripts/windows_link_flags.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# trunk-ignore-all(ruff/F821) +# trunk-ignore-all(flake8/F821): For SConstruct imports +# +# PlatformIO routes build_flags to the compile step only, so the static link for +# [env:native-windows] has to be appended to LINKFLAGS here, as +# extra_scripts/wasm_link_flags.py does for [env:native-wasm]. +Import("env") + +if env["PIOENV"].startswith("native-windows"): + env.Append( + LINKFLAGS=[ + "-static", + "-static-libgcc", + "-static-libstdc++", + ] + ) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index ad0bdec0407..99b4d995701 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -291,7 +291,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd #else rtc.initI2C(); #endif - tm *t = gmtime(&tv->tv_sec); + // tv_sec is a long, which is not time_t everywhere: on Windows + // time_t is 64-bit while long is 32-bit. Copy before taking &. + time_t setSecs = tv->tv_sec; + tm *t = gmtime(&setSecs); rtc.setTime(t->tm_year + 1900, t->tm_mon + 1, t->tm_wday, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); LOG_DEBUG("RV3028_RTC setTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); @@ -313,7 +316,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd #else rtc.begin(Wire); #endif - tm *t = gmtime(&tv->tv_sec); + // tv_sec is a long, which is not time_t everywhere: on Windows + // time_t is 64-bit while long is 32-bit. Copy before taking &. + time_t setSecs = tv->tv_sec; + tm *t = gmtime(&setSecs); rtc.setDateTime(*t); LOG_DEBUG("%s setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", rtc.getChipName(), t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); @@ -327,7 +333,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd #else ArtronShop_RX8130CE rtc(&Wire); #endif - tm *t = gmtime(&tv->tv_sec); + // tv_sec is a long, which is not time_t everywhere: on Windows + // time_t is 64-bit while long is 32-bit. Copy before taking &. + time_t setSecs = tv->tv_sec; + tm *t = gmtime(&setSecs); if (rtc.setTime(*t)) { LOG_DEBUG("RX8130CE setDateTime %02d-%02d-%02d %02d:%02d:%02d (%ld)", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, printableEpoch); diff --git a/src/main.cpp b/src/main.cpp index cc7c239a4e0..bb8fe551d18 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -428,10 +428,14 @@ void setup() #if ARCH_PORTDUINO RTCQuality ourQuality = RTCQualityDevice; +#ifdef __linux__ + // timedatectl is systemd-only, so macOS, Windows and WASM stay at + // RTCQualityDevice rather than claim NTP quality we have not verified. std::string timeCommandResult = exec("timedatectl status | grep synchronized | grep yes -c"); if (timeCommandResult[0] == '1') { ourQuality = RTCQualityNTP; } +#endif struct timeval tv; tv.tv_sec = time(NULL); diff --git a/src/mesh/HardwareRNG.cpp b/src/mesh/HardwareRNG.cpp index 94045305302..7d9f7e15950 100644 --- a/src/mesh/HardwareRNG.cpp +++ b/src/mesh/HardwareRNG.cpp @@ -22,6 +22,12 @@ extern Adafruit_nRFCrypto nRFCrypto; #include #ifdef __linux__ #include // getrandom() +#elif defined(_WIN32) +// Order is load-bearing, hence the blank line: bcrypt.h uses LONG/ULONG from +// windows.h and does not include it itself. +#include + +#include // BCryptGenRandom() #else #include // arc4random_buf() on Darwin/BSD #endif @@ -128,6 +134,15 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy) if (generated == static_cast(length)) { filled = true; } +#elif defined(_WIN32) + // No getrandom/arc4random on Windows. BCryptGenRandom with the + // system-preferred RNG is the documented CSPRNG, preferred over the + // std::random_device fallback below because libstdc++'s Windows backend + // reports entropy() == 0, promising no cryptographic source, and this + // buffer seeds key material. + if (BCryptGenRandom(NULL, buffer, static_cast(length), BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0) { // STATUS_SUCCESS + filled = true; + } #elif defined(__EMSCRIPTEN__) // Browser/wasm: no getrandom/arc4random - fall through to std::random_device, // which emscripten backs with crypto.getRandomValues(). diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index 15923988bc9..b9d12bd8144 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -41,7 +41,7 @@ class AdminModule : public ProtobufModule, public Obser bool hasOpenEditTransaction = false; uint8_t session_passkey[8] = {0}; - uint session_time = 0; + uint32_t session_time = 0; void saveChanges(int saveWhat, bool shouldReboot = true); diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index b6f0ce24d5b..47189c2d7d5 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -33,7 +33,11 @@ #include "IPAddress.h" #if defined(ARCH_PORTDUINO) +#if defined(_WIN32) +#include // ntohl() +#else #include +#endif #elif !defined(ntohl) #include #define ntohl __ntohl diff --git a/src/platform/portduino/GpsdSerial.cpp b/src/platform/portduino/GpsdSerial.cpp index 038ba21f67d..7c8c52ff8b4 100644 --- a/src/platform/portduino/GpsdSerial.cpp +++ b/src/platform/portduino/GpsdSerial.cpp @@ -3,13 +3,97 @@ #include "GpsdSerial.h" #include "configuration.h" -#include #include + +#ifdef _WIN32 +#include +#include +#include +#else +#include #include #include #include #include #include +#endif + +namespace +{ +// Winsock needs explicit initialization, closes with closesocket(), sets +// non-blocking via ioctlsocket() rather than fcntl(), and reports errors through +// WSAGetLastError() rather than errno. +// +// GpsdSerial.h stores the descriptor in an `int`. That is safe on Win64 even +// though SOCKET is a UINT_PTR: Windows documents socket handles as fitting in 32 +// bits, and INVALID_SOCKET narrows to -1, so the `_sockfd >= 0` checks hold. +#ifdef _WIN32 +// Done lazily to keep the dependency local to the one file that needs it. +void initSocketsOnce() +{ + static std::once_flag flag; + std::call_once(flag, [] { + WSADATA wsaData; + WSAStartup(MAKEWORD(2, 2), &wsaData); + }); +} + +void closeSocket(int fd) +{ + ::closesocket(static_cast(fd)); +} + +void setNonBlocking(int fd) +{ + u_long mode = 1; + ::ioctlsocket(static_cast(fd), FIONBIO, &mode); +} + +// Winsock has no MSG_DONTWAIT, but the socket is already non-blocking so a plain +// recv() has the same semantics. +int recvNonBlocking(int fd, void *buf, size_t len) +{ + return ::recv(static_cast(fd), static_cast(buf), static_cast(len), 0); +} + +int sendAll(int fd, const void *buf, size_t len) +{ + return ::send(static_cast(fd), static_cast(buf), static_cast(len), 0); +} + +bool lastErrorWasWouldBlock() +{ + return WSAGetLastError() == WSAEWOULDBLOCK; +} +#else +void initSocketsOnce() {} + +void closeSocket(int fd) +{ + ::close(fd); +} + +void setNonBlocking(int fd) +{ + ::fcntl(fd, F_SETFL, O_NONBLOCK); +} + +int recvNonBlocking(int fd, void *buf, size_t len) +{ + return static_cast(::recv(fd, buf, len, MSG_DONTWAIT)); +} + +int sendAll(int fd, const void *buf, size_t len) +{ + return static_cast(::write(fd, buf, len)); +} + +bool lastErrorWasWouldBlock() +{ + return errno == EAGAIN || errno == EWOULDBLOCK; +} +#endif +} // namespace namespace arduino { @@ -28,6 +112,8 @@ bool GpsdSerial::connectToGpsd() if (_host.empty()) return false; + initSocketsOnce(); + struct addrinfo hints = {}, *res = nullptr; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; @@ -41,12 +127,12 @@ bool GpsdSerial::connectToGpsd() // Try every address returned by getaddrinfo (e.g. ::1 before 127.0.0.1). int fd = -1; for (struct addrinfo *rp = res; rp != nullptr; rp = rp->ai_next) { - fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + fd = static_cast(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)); if (fd < 0) continue; - if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) + if (connect(fd, rp->ai_addr, static_cast(rp->ai_addrlen)) == 0) break; // connected - ::close(fd); + closeSocket(fd); fd = -1; } freeaddrinfo(res); @@ -57,11 +143,11 @@ bool GpsdSerial::connectToGpsd() } // Switch to non-blocking so available()/read() never stall the GPS thread. - fcntl(fd, F_SETFL, O_NONBLOCK); + setNonBlocking(fd); // Ask gpsd to stream raw NMEA sentences. const char watchCmd[] = "?WATCH={\"enable\":true,\"nmea\":true}\n"; - ::write(fd, watchCmd, sizeof(watchCmd) - 1); + sendAll(fd, watchCmd, sizeof(watchCmd) - 1); _sockfd = fd; _rxBuf.clear(); @@ -80,7 +166,7 @@ void GpsdSerial::begin(unsigned long /*baud*/, uint16_t /*config*/) void GpsdSerial::end() { if (_sockfd >= 0) { - ::close(_sockfd); + closeSocket(_sockfd); _sockfd = -1; } _rxBuf.clear(); @@ -96,18 +182,18 @@ void GpsdSerial::fillBuffer() return; uint8_t tmp[256]; - ssize_t n; - while (_rxBuf.size() < RX_BUF_MAX && (n = recv(_sockfd, tmp, sizeof(tmp), MSG_DONTWAIT)) > 0) { + int n; + while (_rxBuf.size() < RX_BUF_MAX && (n = recvNonBlocking(_sockfd, tmp, sizeof(tmp))) > 0) { size_t space = RX_BUF_MAX - _rxBuf.size(); size_t toCopy = (static_cast(n) < space) ? static_cast(n) : space; for (size_t i = 0; i < toCopy; i++) _rxBuf.push_back(tmp[i]); } - if (n == 0 || (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)) { + if (n == 0 || (n < 0 && !lastErrorWasWouldBlock())) { // gpsd closed the connection or a real error occurred. LOG_WARN("gpsdSerial: disconnected, will retry"); - ::close(_sockfd); + closeSocket(_sockfd); _sockfd = -1; _rxBuf.clear(); } diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 3903da2e876..3de38d5cf49 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -21,8 +21,12 @@ #include #include #include -#include #include +#ifndef _WIN32 +// Only the PORTDUINO_LINUX_HARDWARE block below calls ioctl() (HCIGETDEVINFO, +// for the BlueZ-derived MAC address); Windows has no . +#include +#endif #ifdef PORTDUINO_LINUX_HARDWARE #include "linux/gpio/LinuxGPIOPin.h" @@ -34,6 +38,12 @@ #include #endif +#ifdef _WIN32 +// Defined in WindowsMacAddr.cpp, which keeps out of this TU: it +// pulls in RPC/OLE headers that collide with the Arduino API. +bool portduinoWindowsPrimaryMac(uint8_t *dmac); +#endif + #ifdef __APPLE__ // Used by getMacAddr()'s macOS fallback to read the en0 link-layer address. // `getifaddrs()` is the BSD-portable way; `` provides the @@ -193,6 +203,13 @@ void getMacAddr(uint8_t *dmac) } freeifaddrs(ifap); } +#elif defined(_WIN32) + // No BlueZ on Windows, so fall back to the host's primary adapter MAC, + // the same stable host-derived identifier BlueZ gives on Linux and en0 + // gives on macOS. Otherwise the MAC stays all-zero, device_id is left + // unset, and every user has to pass --hwid. On failure dmac is untouched + // and the caller's "Blank MAC Address not allowed!" check still fires. + portduinoWindowsPrimaryMac(dmac); #else // No platform-specific MAC source; leave dmac at its default. Caller // can override via the --hwid CLI flag or the YAML config. @@ -292,7 +309,10 @@ void portduinoSetup() std::filesystem::directory_iterator{portduino_config.config_directory}) { if (ends_with(entry.path().string(), ".yaml")) { std::cout << "Also using " << entry << " as additional config file" << std::endl; - loadConfig(entry.path().c_str()); + // .string() rather than .c_str(): path::value_type is wchar_t on + // Windows, and loadConfig() takes a const char *. The temporary + // outlives the call. + loadConfig(entry.path().string().c_str()); } } } diff --git a/src/platform/portduino/USBHal.h b/src/platform/portduino/USBHal.h index 9496b2ccb45..2c00ed3904a 100644 --- a/src/platform/portduino/USBHal.h +++ b/src/platform/portduino/USBHal.h @@ -8,6 +8,7 @@ #include #include #include +#include // gettimeofday(), previously pulled in via libusb.h #include extern uint32_t rebootAtMsec; diff --git a/src/platform/portduino/WindowsMacAddr.cpp b/src/platform/portduino/WindowsMacAddr.cpp new file mode 100644 index 00000000000..9878136d9f5 --- /dev/null +++ b/src/platform/portduino/WindowsMacAddr.cpp @@ -0,0 +1,65 @@ +#if defined(ARCH_PORTDUINO) && defined(_WIN32) + +// Host-MAC lookup for PortduinoGlue's getMacAddr(), standing in for the BlueZ +// HCI path on Linux and the en0/getifaddrs path on macOS. +// +// Its own translation unit on purpose: pulls in the RPC/OLE chain, +// whose `boolean` and MSG types collide with the Arduino API, which is why the +// native-windows env builds with -DNOUSER -DWIN32_LEAN_AND_MEAN -DNOGDI. Those +// trims in turn break itself (oleidl.h needs LPMSG). Keeping this +// file free of Arduino headers lets us undo them locally. +#undef WIN32_LEAN_AND_MEAN +#undef NOUSER +#undef NOGDI + +// Order is load-bearing, hence the blank lines: winsock2.h must come first, or +// windows.h pulls in the winsock v1 header and the two collide. iphlpapi.h +// needs both. +#include + +#include + +#include + +#include +#include +#include + +// Fill dmac with the first up, non-loopback adapter's 6-byte physical address. +// Returns false and leaves dmac untouched if none is found. +// +// GetAdaptersAddresses() replaces the deprecated GetAdaptersInfo(). Its ordering +// is driver-determined but stable across reboots, so this picks the same adapter +// each run and the node keeps a stable identity. +bool portduinoWindowsPrimaryMac(uint8_t *dmac) +{ + const ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_SKIP_FRIENDLY_NAME; + + ULONG bufLen = 15000; // starting size recommended by the API docs + std::unique_ptr buf(new char[bufLen]); + auto *addrs = reinterpret_cast(buf.get()); + + ULONG ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, addrs, &bufLen); + if (ret == ERROR_BUFFER_OVERFLOW) { + // bufLen now holds the required size; retry once. + buf.reset(new char[bufLen]); + addrs = reinterpret_cast(buf.get()); + ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, addrs, &bufLen); + } + if (ret != NO_ERROR) + return false; + + for (auto *a = addrs; a != nullptr; a = a->Next) { + if (a->IfType == IF_TYPE_SOFTWARE_LOOPBACK) + continue; + if (a->OperStatus != IfOperStatusUp) + continue; + if (a->PhysicalAddressLength != 6) + continue; + std::memcpy(dmac, a->PhysicalAddress, 6); + return true; + } + return false; +} + +#endif // ARCH_PORTDUINO && _WIN32 diff --git a/src/platform/portduino/windows/include/libpinedio-usb.h b/src/platform/portduino/windows/include/libpinedio-usb.h new file mode 100644 index 00000000000..ee8a1a77bde --- /dev/null +++ b/src/platform/portduino/windows/include/libpinedio-usb.h @@ -0,0 +1,79 @@ +// Windows drop-in for libch341-spi-userspace's public header. +// +// Same API surface as the upstream libpinedio-usb.h that Ch341Hal +// (src/platform/portduino/USBHal.h) compiles against, but without libusb: the +// implementation (libpinedio_ch341dll.c) drives WCH's CH341DLL instead. See the +// .c for why. Mirrors the wasm drop-in in ../../wasm/include/. +#ifndef PINEDIO_USB_CH341DLL_H +#define PINEDIO_USB_CH341DLL_H +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +enum pinedio_int_pin { + PINEDIO_PIN_D0, + PINEDIO_PIN_D1, + PINEDIO_PIN_D2, + PINEDIO_PIN_D3, + PINEDIO_PIN_D4, + PINEDIO_PIN_D5, + PINEDIO_PIN_D6, + PINEDIO_PIN_D7, + PINEDIO_PIN_ERR, + PINEDIO_PIN_PEMP, + PINEDIO_PIN_INT, + PINEDIO_INT_PIN_MAX +}; + +enum pinedio_int_mode { + PINEDIO_INT_MODE_RISING = 0x01, + PINEDIO_INT_MODE_FALLING = 0x02, +}; + +enum pinedio_option { + PINEDIO_OPTION_AUTO_CS, + PINEDIO_OPTION_SEARCH_SERIAL, + PINEDIO_OPTION_VID, + PINEDIO_OPTION_PID, + PINEDIO_OPTION_MAX +}; + +struct pinedio_inst_int { + uint8_t previous_state; + enum pinedio_int_mode mode; + void (*callback)(void); +}; + +// Ch341Hal embeds this by value and touches in_error, serial_number, +// product_string and options[], so those must keep their names. +struct pinedio_inst { + bool in_error; + struct pinedio_inst_int interrupts[PINEDIO_INT_PIN_MAX]; + uint32_t options[PINEDIO_OPTION_MAX]; + char serial_number[9]; + char product_string[97]; +}; + +typedef struct pinedio_inst pinedio_inst; + +int32_t pinedio_init(struct pinedio_inst *inst, void *driver); +int32_t pinedio_set_option(struct pinedio_inst *inst, enum pinedio_option option, uint32_t value); +int32_t pinedio_set_pin_mode(struct pinedio_inst *inst, uint32_t pin, uint32_t mode); +int32_t pinedio_digital_write(struct pinedio_inst *inst, uint32_t pin, bool active); +int32_t pinedio_set_cs(struct pinedio_inst *inst, bool active); +int32_t pinedio_write_read(struct pinedio_inst *inst, uint8_t *writearr, uint32_t writecnt, uint8_t *readarr, uint32_t readcnt); +int32_t pinedio_transceive(struct pinedio_inst *inst, uint8_t *write_buf, uint8_t *read_buf, uint32_t count); +int32_t pinedio_digital_read(struct pinedio_inst *inst, uint32_t pin); +int32_t pinedio_get_irq_state(struct pinedio_inst *inst, uint32_t pin); +int32_t pinedio_attach_interrupt(struct pinedio_inst *inst, enum pinedio_int_pin int_pin, enum pinedio_int_mode int_mode, + void (*callback)(void)); +int32_t pinedio_deattach_interrupt(struct pinedio_inst *inst, enum pinedio_int_pin int_pin); +void pinedio_deinit(struct pinedio_inst *inst); + +#ifdef __cplusplus +} +#endif +#endif // PINEDIO_USB_CH341DLL_H diff --git a/src/platform/portduino/windows/libpinedio_ch341dll.c b/src/platform/portduino/windows/libpinedio_ch341dll.c new file mode 100644 index 00000000000..e2b30218cd2 --- /dev/null +++ b/src/platform/portduino/windows/libpinedio_ch341dll.c @@ -0,0 +1,418 @@ +// libpinedio API implemented over WCH's CH341DLL, replacing the libusb backend +// on Windows. Mirrors what wasm/libpinedio_webusb.c does for the browser. +// +// WHY NOT LIBUSB: libusb on Windows can only talk to a device bound to +// WinUSB/libusbK/libusb0, so it needs Zadig to replace the driver by hand on +// every machine, and Zadig installs a self-signed CA into the trust store. +// CH341DLL ships with WCH's signed CH341PAR driver and a normal installer. +// +// The DLL is closed-source and not redistributable, so it is resolved at +// runtime: with no CH341PAR installed, pinedio_init() fails and the caller +// (Ch341Hal's constructor) throws, which is the same path a missing device +// takes. Nothing else in the build depends on it. +// +// Pin numbering is the CH341's D0-D7, as upstream. For the PineDio adapter the +// YAML gives CS: 0, IRQ: 6, Reset: 2 against D3=SCK, D5=MOSI, D7=MISO. + +#if defined(ARCH_PORTDUINO) && defined(_WIN32) + +#include "libpinedio-usb.h" + +#include +#include +#include +#include +#include + +// CH341Set_D5_D0 only reaches D0-D5. That covers every output the adapter uses +// (CS/Reset/SCK/MOSI); D6 (IRQ) and D7 (MISO) are inputs, read via CH341GetInput. +#define CH341_MAX_OUTPUT_PIN 5 + +// iMode bit 7: SPI bit order, 1 = MSB first. The SX1262 is MSB-first, and doing +// it in hardware avoids upstream's per-byte reverse_byte() over the whole buffer. +#define CH341_STREAM_MODE_SPI_MSB_FIRST 0x80 + +// Matches upstream's poll interval, so IRQ latency is the same as on Linux. +#define PIN_POLL_INTERVAL_MS (1000 / 30) + +typedef HANDLE(WINAPI *ch341_open_t)(ULONG); +typedef VOID(WINAPI *ch341_close_t)(ULONG); +typedef BOOL(WINAPI *ch341_set_stream_t)(ULONG, ULONG); +typedef BOOL(WINAPI *ch341_stream_spi4_t)(ULONG, ULONG, ULONG, PVOID); +typedef BOOL(WINAPI *ch341_set_d5_d0_t)(ULONG, ULONG, ULONG); +typedef BOOL(WINAPI *ch341_get_input_t)(ULONG, PULONG); +typedef PVOID(WINAPI *ch341_get_device_name_t)(ULONG); + +static struct { + HMODULE dll; + ch341_open_t open; + ch341_close_t close; + ch341_set_stream_t set_stream; + ch341_stream_spi4_t stream_spi4; + ch341_set_d5_d0_t set_d5_d0; + ch341_get_input_t get_input; + ch341_get_device_name_t get_device_name; +} ch341; + +// Single adapter, matching Ch341Hal's spiChannel of 0. Multiple sticks would +// need this and the device index threaded through pinedio_inst. +static ULONG ch341_index = 0; + +// D0-D5 direction and output state, applied together by CH341Set_D5_D0. +static ULONG pin_dir_out = 0; +static ULONG pin_state = 0; + +// Serializes DLL access between the caller and the poll thread. +static pthread_mutex_t usb_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_t poll_thread; +static volatile bool poll_thread_exit = false; +static int int_running_cnt = 0; + +// CH341PAR installs the 64-bit library as CH341DLLA64.DLL in System32 and the +// 32-bit one as CH341DLL.DLL in SysWOW64, so a 64-bit process must ask for the +// A64 name; plain CH341DLL.DLL would either not be found or be the wrong +// architecture. Try the matching name first and fall back to the other. +static const char *const ch341_dll_names[] = { +#ifdef _WIN64 + "CH341DLLA64.DLL", + "CH341DLL.DLL", +#else + "CH341DLL.DLL", + "CH341DLLA64.DLL", +#endif +}; + +static bool load_dll(void) +{ + if (ch341.dll) + return true; + for (size_t i = 0; i < sizeof(ch341_dll_names) / sizeof(ch341_dll_names[0]); i++) { + ch341.dll = LoadLibraryA(ch341_dll_names[i]); + if (ch341.dll) + break; + } + if (!ch341.dll) { + fprintf(stderr, "CH341DLL not found; install the WCH CH341PAR driver\n"); + return false; + } + ch341.open = (ch341_open_t)(void *)GetProcAddress(ch341.dll, "CH341OpenDevice"); + ch341.close = (ch341_close_t)(void *)GetProcAddress(ch341.dll, "CH341CloseDevice"); + ch341.set_stream = (ch341_set_stream_t)(void *)GetProcAddress(ch341.dll, "CH341SetStream"); + ch341.stream_spi4 = (ch341_stream_spi4_t)(void *)GetProcAddress(ch341.dll, "CH341StreamSPI4"); + ch341.set_d5_d0 = (ch341_set_d5_d0_t)(void *)GetProcAddress(ch341.dll, "CH341Set_D5_D0"); + ch341.get_input = (ch341_get_input_t)(void *)GetProcAddress(ch341.dll, "CH341GetInput"); + ch341.get_device_name = (ch341_get_device_name_t)(void *)GetProcAddress(ch341.dll, "CH341GetDeviceName"); + + if (!ch341.open || !ch341.close || !ch341.set_stream || !ch341.stream_spi4 || !ch341.set_d5_d0 || !ch341.get_input) { + fprintf(stderr, "CH341DLL is missing expected exports\n"); + FreeLibrary(ch341.dll); + ch341.dll = NULL; + return false; + } + return true; +} + +static int32_t apply_pins(void) +{ + if (!ch341.set_d5_d0(ch341_index, pin_dir_out, pin_state)) + return -1; + return 0; +} + +int32_t pinedio_set_option(struct pinedio_inst *inst, enum pinedio_option option, uint32_t value) +{ + if (option < PINEDIO_OPTION_MAX) + inst->options[option] = value; + return 0; +} + +int32_t pinedio_init(struct pinedio_inst *inst, void *driver) +{ + (void)driver; + inst->in_error = false; + for (int i = 0; i < PINEDIO_INT_PIN_MAX; i++) + inst->interrupts[i].callback = NULL; + + if (!load_dll()) + return -1; + + if (ch341.open(ch341_index) == INVALID_HANDLE_VALUE) { + fprintf(stderr, "CH341OpenDevice(%lu) failed; is the adapter plugged in?\n", ch341_index); + return -2; + } + + // Default speed (bits 0-1 = 01) plus MSB-first. + if (!ch341.set_stream(ch341_index, 0x01 | CH341_STREAM_MODE_SPI_MSB_FIRST)) { + fprintf(stderr, "CH341SetStream failed\n"); + ch341.close(ch341_index); + return -3; + } + + pin_dir_out = 0; + pin_state = 0; + + // CH341DLL exposes no USB serial string. Ch341Hal only uses serial_number to + // pick between multiple adapters and to derive a MAC; on Windows getMacAddr() + // falls back to the host adapter, so leaving it empty is safe. The device name + // is the closest stand-in for the product string. + inst->serial_number[0] = '\0'; + inst->product_string[0] = '\0'; + if (ch341.get_device_name) { + const char *name = (const char *)ch341.get_device_name(ch341_index); + if (name) { + strncpy(inst->product_string, name, sizeof(inst->product_string) - 1); + inst->product_string[sizeof(inst->product_string) - 1] = '\0'; + } + } + return 0; +} + +int32_t pinedio_set_pin_mode(struct pinedio_inst *inst, uint32_t pin, uint32_t mode) +{ + (void)inst; + if (pin > CH341_MAX_OUTPUT_PIN) + return 0; // D6/D7 are input-only; nothing to configure + + // Under the lock: the poll thread's callback can drive GPIO concurrently. + pthread_mutex_lock(&usb_mutex); + if (mode) + pin_dir_out |= (1u << pin); + else + pin_dir_out &= ~(1u << pin); + pthread_mutex_unlock(&usb_mutex); + // Upstream defers the device write to the next digital_write; do the same so + // the direction and level land together. + return 0; +} + +int32_t pinedio_digital_write(struct pinedio_inst *inst, uint32_t pin, bool active) +{ + if (pin > CH341_MAX_OUTPUT_PIN) + return -1; + + // Mutate and apply as one critical section, or a concurrent write loses its + // update when apply_pins() reads a half-updated pin_state. + pthread_mutex_lock(&usb_mutex); + if (active) + pin_state |= (1u << pin); + else + pin_state &= ~(1u << pin); + int32_t ret = apply_pins(); + pthread_mutex_unlock(&usb_mutex); + if (ret < 0) + inst->in_error = true; + return ret; +} + +int32_t pinedio_set_cs(struct pinedio_inst *inst, bool active) +{ + return pinedio_digital_write(inst, 0, active); // D0 is CS +} + +static int32_t read_input(uint32_t *out) +{ + ULONG status = 0; + if (!ch341.get_input(ch341_index, &status)) + return -1; + *out = (uint32_t)status; + return 0; +} + +int32_t pinedio_digital_read(struct pinedio_inst *inst, uint32_t pin) +{ + uint32_t status = 0; + pthread_mutex_lock(&usb_mutex); + int32_t ret = read_input(&status); + pthread_mutex_unlock(&usb_mutex); + if (ret < 0) { + inst->in_error = true; + return ret; + } + return (status & (1u << pin)) ? 1 : 0; // bits 7-0 are D7-D0 +} + +int32_t pinedio_get_irq_state(struct pinedio_inst *inst, uint32_t pin) +{ + return pinedio_digital_read(inst, pin); +} + +int32_t pinedio_transceive(struct pinedio_inst *inst, uint8_t *write_buf, uint8_t *read_buf, uint32_t count) +{ + if (count == 0) + return 0; + + // CH341StreamSPI4 is full duplex over one in/out buffer. + uint8_t stack_buf[64]; + uint8_t *buf = count <= sizeof(stack_buf) ? stack_buf : (uint8_t *)malloc(count); + if (!buf) + return -1; + memcpy(buf, write_buf, count); + + // Bit 7 clear: leave chip select alone. Ch341Hal sets PINEDIO_OPTION_AUTO_CS + // to 0 and lets RadioLib drive NSS through digitalWrite. + ULONG cs = 0; + if (inst->options[PINEDIO_OPTION_AUTO_CS]) + cs = 0x80; // enable, D0 active low + + pthread_mutex_lock(&usb_mutex); + BOOL ok = ch341.stream_spi4(ch341_index, cs, count, buf); + pthread_mutex_unlock(&usb_mutex); + + if (ok && read_buf) + memcpy(read_buf, buf, count); + if (buf != stack_buf) + free(buf); + + if (!ok) { + inst->in_error = true; + return -1; + } + return 0; +} + +int32_t pinedio_write_read(struct pinedio_inst *inst, uint8_t *writearr, uint32_t writecnt, uint8_t *readarr, uint32_t readcnt) +{ + uint32_t total = writecnt + readcnt; + uint8_t stack_buf[64]; + uint8_t *buf = total <= sizeof(stack_buf) ? stack_buf : (uint8_t *)malloc(total); + if (!buf) + return -1; + memcpy(buf, writearr, writecnt); + memset(buf + writecnt, 0, readcnt); + + int32_t ret = pinedio_transceive(inst, buf, buf, total); + if (ret == 0) + memcpy(readarr, buf + writecnt, readcnt); + if (buf != stack_buf) + free(buf); + return ret; +} + +// The CH341's own interrupt (CH341SetIntRoutine) only fires on its INT# pin, but +// the adapter wires the radio's DIO1 to D6, so it can't be used here. Poll D6 +// instead, at upstream's rate and with upstream's edge semantics. +static void *pin_poll_thread_fn(void *arg) +{ + struct pinedio_inst *inst = (struct pinedio_inst *)arg; + bool should_exit = false; + + while (!should_exit) { + uint32_t input = 0; + pthread_mutex_lock(&usb_mutex); + int32_t ret = read_input(&input); + pthread_mutex_unlock(&usb_mutex); + if (ret < 0) { + inst->in_error = true; + fprintf(stderr, "CH341 poll: failed to read input\n"); + break; + } + inst->in_error = false; + + pthread_mutex_lock(&usb_mutex); + for (uint8_t int_pin = 0; int_pin < PINEDIO_INT_PIN_MAX; int_pin++) { + struct pinedio_inst_int *inst_int = &inst->interrupts[int_pin]; + // Copy the pointer while holding the lock: a concurrent + // pinedio_deattach_interrupt() could NULL it once we drop the lock + // to make the call. + void (*cb)(void) = inst_int->callback; + if (cb == NULL) + continue; + uint8_t state = (input & (1u << int_pin)) != 0; + if (inst_int->previous_state != 255 && inst_int->previous_state != state) { + enum pinedio_int_mode mode = + (!inst_int->previous_state && state) ? PINEDIO_INT_MODE_RISING : PINEDIO_INT_MODE_FALLING; + if (inst_int->mode & mode) { + // Callback re-enters this library, so drop the lock first. + pthread_mutex_unlock(&usb_mutex); + cb(); + pthread_mutex_lock(&usb_mutex); + } + } + inst_int->previous_state = state; + } + should_exit = poll_thread_exit; + pthread_mutex_unlock(&usb_mutex); + Sleep(PIN_POLL_INTERVAL_MS); + } + return NULL; +} + +int32_t pinedio_attach_interrupt(struct pinedio_inst *inst, enum pinedio_int_pin int_pin, enum pinedio_int_mode int_mode, + void (*callback)(void)) +{ + if (int_pin >= PINEDIO_INT_PIN_MAX) + return -1; + + int32_t res = 0; + pthread_mutex_lock(&usb_mutex); + bool was_attached = inst->interrupts[int_pin].callback != NULL; + inst->interrupts[int_pin].previous_state = 255; + inst->interrupts[int_pin].mode = int_mode; + inst->interrupts[int_pin].callback = callback; + + // Only a new attachment changes the refcount. Re-attaching an already-armed + // pin (RadioLib does this) must not drop the count to 0 and spawn a second + // poll thread alongside the running one. + if (!was_attached) { + if (int_running_cnt == 0) { + poll_thread_exit = false; + res = pthread_create(&poll_thread, NULL, pin_poll_thread_fn, inst); + if (res != 0) { + fprintf(stderr, "CH341: failed to start poll thread: %d\n", res); + inst->interrupts[int_pin].callback = NULL; + pthread_mutex_unlock(&usb_mutex); + return res; + } + } + int_running_cnt++; + } + pthread_mutex_unlock(&usb_mutex); + return res; +} + +int32_t pinedio_deattach_interrupt(struct pinedio_inst *inst, enum pinedio_int_pin int_pin) +{ + if (int_pin >= PINEDIO_INT_PIN_MAX) + return -1; + + pthread_t thread_to_join; + pthread_mutex_lock(&usb_mutex); + bool was_attached = inst->interrupts[int_pin].callback != NULL; + inst->interrupts[int_pin].callback = NULL; + if (was_attached) + int_running_cnt--; + bool stop = was_attached && int_running_cnt == 0; + if (stop) { + poll_thread_exit = true; + // Copy the handle: a concurrent attach could overwrite poll_thread once + // the lock is dropped, and we would join the wrong thread. + thread_to_join = poll_thread; + } + pthread_mutex_unlock(&usb_mutex); + + // Joining under the lock would deadlock against the poll thread taking it. + if (stop && !pthread_equal(thread_to_join, pthread_self())) + pthread_join(thread_to_join, NULL); + return 0; +} + +void pinedio_deinit(struct pinedio_inst *inst) +{ + pthread_t thread_to_join; + pthread_mutex_lock(&usb_mutex); + bool stop = int_running_cnt > 0; + poll_thread_exit = true; + int_running_cnt = 0; + if (stop) + thread_to_join = poll_thread; // copy before dropping the lock, as above + pthread_mutex_unlock(&usb_mutex); + + if (stop && !pthread_equal(thread_to_join, pthread_self())) + pthread_join(thread_to_join, NULL); + + if (ch341.dll) + ch341.close(ch341_index); + inst->in_error = false; +} + +#endif // ARCH_PORTDUINO && _WIN32 diff --git a/src/power/PowerHAL.cpp b/src/power/PowerHAL.cpp index 0a8d5f10b58..3e8d163ff13 100644 --- a/src/power/PowerHAL.cpp +++ b/src/power/PowerHAL.cpp @@ -1,19 +1,29 @@ #include "PowerHAL.h" +// PE/COFF has no ELF-style weak definitions: GNU as lowers __attribute__((weak)) +// to a COFF weak external, which the linker treats as an undefined reference +// rather than a fallback, so these defaults would not link on Windows. Only +// nrf52 and nrf54l15 override them, so define them strongly there. +#ifdef _WIN32 +#define POWERHAL_WEAK_DEFAULT __attribute__((noinline)) +#else +#define POWERHAL_WEAK_DEFAULT __attribute__((weak, noinline)) +#endif + void powerHAL_init() { return powerHAL_platformInit(); } -__attribute__((weak, noinline)) void powerHAL_platformInit() {} +POWERHAL_WEAK_DEFAULT void powerHAL_platformInit() {} -__attribute__((weak, noinline)) bool powerHAL_isPowerLevelSafe() +POWERHAL_WEAK_DEFAULT bool powerHAL_isPowerLevelSafe() { return true; } -__attribute__((weak, noinline)) bool powerHAL_isVBUSConnected() +POWERHAL_WEAK_DEFAULT bool powerHAL_isVBUSConnected() { return false; } diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index 00d2a3a94de..bf881627d4e 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -2,7 +2,7 @@ [portduino_base] platform = # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop - https://github.com/meshtastic/platform-native/archive/61067ac3774e4fe27aa9762c72cebea507f116c8.zip + https://github.com/meshtastic/platform-native/archive/86c62edfb7084d11669aa255411a70580e83823b.zip framework = arduino build_src_filter = @@ -27,8 +27,8 @@ lib_deps = https://github.com/meshtastic/Crypto/archive/591ff9a690e8168ccb7a36abde8d7783e448d395.zip # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.25 - ; # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main - https://github.com/pine64/libch341-spi-userspace/archive/2e5ff751d0c39667993df672cb683740ed5c9394.zip + ; # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/meshtastic/libch341-spi-userspace gitBranch=main + https://github.com/meshtastic/libch341-spi-userspace/archive/03bf505d6e5904092c1c389c45b01098f7a302fe.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library adafruit/Adafruit seesaw Library@1.7.9 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini index e71386b6702..7dd6cb9538a 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -246,6 +246,83 @@ build_flags = ${env:native-macos.build_flags} build_src_filter = ${env:native-macos.build_src_filter} lib_ignore = ${env:native-macos.lib_ignore} +; --------------------------------------------------------------------------- +; Native build for Windows (x86_64) via the MSYS2 UCRT64 MinGW-w64 toolchain. +; Headless meshtasticd.exe running in SimRadio mode (`-s`). No BlueZ, libgpiod or +; Linux I2C, and no UDP multicast: the framework's AsyncUDP.cpp is BSD sockets, +; so HAS_UDP_MULTICAST stays unset here as it does on macOS. +; +; MSVC is not an option: platform-native's builder calls env.Tool("gcc") and the +; firmware builds as gnu17/gnu++17 with GNU extensions throughout. +; +; Prerequisites (MSYS2, https://www.msys2.org/): +; pacman -S --needed mingw-w64-ucrt-x86_64-{gcc,pkgconf,yaml-cpp,libuv,jsoncpp,openssl,libusb} +; +; argp is not packaged for MSYS2's mingw environments (msys/libargp links the +; msys-2.0.dll emulation layer and can't be used for a native binary), yet +; Arduino.h includes and main.cpp calls argp_parse(). Build it once from +; source, the same dependency macOS meets with `brew install argp-standalone`: +; git clone https://github.com/tom42/argp-standalone +; cd argp-standalone && cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release . +; cmake --build build +; cp include/argp-standalone/argp.h /ucrt64/include/argp.h ; ships no install() rules +; cp build/src/libargp-standalone.a /ucrt64/lib/libargp.a +; +; Build from any shell with /ucrt64/bin on PATH: +; pio run -e native-windows +; .pio/build/native-windows/meshtasticd.exe -s +; --------------------------------------------------------------------------- +[env:native-windows] +extends = native_base +build_flags = ${portduino_base.build_flags_common} + -I variants/native/portduino + ; Our drop-in libpinedio-usb.h, replacing the libusb one: libusb can only reach + ; a device bound to WinUSB, which means Zadig on every machine. See + ; src/platform/portduino/windows/libpinedio_ch341dll.c. + -I src/platform/portduino/windows/include + -largp + -lws2_32 ; GpsdSerial.cpp's TCP client + -lbcrypt ; BCryptGenRandom() in HardwareRNG.cpp + -liphlpapi ; GetAdaptersAddresses() host-MAC fallback in PortduinoGlue.cpp + ; libch341's libpinedio-usb.h pulls in libusb.h and so , which + ; collides with the Arduino API: winuser.h's `typedef struct tagINPUT INPUT` vs + ; the PinMode enumerator, and rpcndr.h's `typedef unsigned char boolean` vs + ; Arduino's `typedef bool boolean`. NOUSER and WIN32_LEAN_AND_MEAN keep those + ; headers out, NOMINMAX stops min/max being macroed over std::min/std::max. + -DWIN32_LEAN_AND_MEAN + -DNOMINMAX + -DNOUSER + -DNOGDI + ; yaml-cpp declares its API __declspec(dllimport) unless told the link is + ; static, leaving every YAML symbol undefined as __imp_*. + -DYAML_CPP_STATIC_DEFINE + ; Headless: variant.h would otherwise default HAS_SCREEN to 1 and pull in the + ; screen renderer; EXCLUDE_SCREEN gates the `screen->...` hooks in the sensors. + -DHAS_SCREEN=0 + -DMESHTASTIC_EXCLUDE_SCREEN=1 + !pkg-config --cflags --libs openssl --silence-errors || : +build_unflags = + -fPIC ; ignored on Windows, where all code is position-independent +; Static link, so meshtasticd.exe stands alone and can't be hijacked by a stray +; System32 DLL. PlatformIO only feeds build_flags to the compile step, hence the script. +extra_scripts = + ${env.extra_scripts} + post:extra_scripts/windows_link_flags.py +; LinuxInput drives evdev; Panel_sdl/TFTDisplay pull LovyanGFX. Neither is needed +; for the headless build. +build_src_filter = ${native_base.build_src_filter} + - + - + - + - +; LovyanGFX includes and is only needed by the TFT variants. The pine64 +; libch341 is the libusb backend that libpinedio_ch341dll.c replaces; keeping both +; would duplicate every pinedio_* symbol. +lib_ignore = + ${portduino_base.lib_ignore} + LovyanGFX + Pine libch341-spi Userspace library + ; --------------------------------------------------------------------------- ; WASM (Emscripten) - the portduino node compiled to WebAssembly, driving a real ; LoRa radio over WebUSB through a CH341 (src/platform/portduino/wasm/). The same From 5e719da49d5a56bdca951c56a114f0a0598445c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Fri, 17 Jul 2026 19:19:46 +0200 Subject: [PATCH 2/5] Fix false-green Windows CI build Run PlatformIO under the runner's CPython instead of MSYS2's MinGW Python, which reported a mingw platform tag that matched no PyPI wheel, built grpcio from source, and failed the link with a SCons response-file error. MSYS2 now supplies only the UCRT64 toolchain, reached via PATH. Drop continue-on-error from the build, verify, and smoke steps so a failing pio turns the job red. Rewrite the self-contained check to fail on a missing binary or empty import table instead of printing OK. --- .github/workflows/build_windows_bin.yml | 49 ++++++++++++++++++------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build_windows_bin.yml b/.github/workflows/build_windows_bin.yml index 31fdc8151ef..0dd1b3c5b12 100644 --- a/.github/workflows/build_windows_bin.yml +++ b/.github/workflows/build_windows_bin.yml @@ -35,12 +35,18 @@ jobs: persist-credentials: false - name: Setup MSYS2 / UCRT64 + id: msys2 uses: msys2/setup-msys2@v2 with: msystem: UCRT64 update: true # argp is deliberately absent here - it is not packaged for MSYS2's # mingw environments and is built from source below. + # + # Python is deliberately absent too: MSYS2's interpreter reports a + # `mingw` platform tag, so no PyPI wheel matches it and pip builds + # every dependency from source. PlatformIO runs under the runner's + # CPython instead (see below); MSYS2 only supplies the toolchain. install: >- mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-pkgconf @@ -51,10 +57,13 @@ jobs: mingw-w64-ucrt-x86_64-libusb mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja - mingw-w64-ucrt-x86_64-python - mingw-w64-ucrt-x86_64-python-pip git + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + # framework-portduino's Arduino.h includes and its main.cpp calls # argp_parse(). glibc has argp built in and macOS gets it from # `brew install argp-standalone`, but MSYS2 packages it only for the @@ -71,21 +80,25 @@ jobs: cp build/src/libargp-standalone.a /ucrt64/lib/libargp.a - name: Install PlatformIO + shell: pwsh run: | python -m pip install --upgrade pip pip install platformio - name: Get release version string - run: echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT + shell: pwsh id: version + run: echo "long=$(python ./bin/buildinfo.py long)" >> $env:GITHUB_OUTPUT + # Runs outside the MSYS2 shell so PlatformIO stays on the runner's + # CPython; the UCRT64 toolchain is reached through PATH instead. - name: Build for Windows - run: platformio run -e native-windows + shell: pwsh + run: | + $env:PATH = "${{ steps.msys2.outputs.msys2-location }}\ucrt64\bin;$env:PATH" + platformio run -e native-windows env: PKG_VERSION: ${{ steps.version.outputs.long }} - # Errors in this step should not fail the entire workflow while Windows - # support is in development (mirrors the macOS job). - continue-on-error: true - name: List output files run: ls -lah .pio/build/native-windows/ @@ -94,16 +107,24 @@ jobs: # DLLs. A third-party DLL appearing here means the static link regressed # and the artifact would need those DLLs shipped alongside it. - name: Verify the binary is self-contained - continue-on-error: true run: | - objdump -p .pio/build/native-windows/meshtasticd.exe \ - | grep -i 'DLL Name' \ - | grep -viE 'api-ms-win|KERNEL32|WS2_32|ADVAPI32|USER32|msvcrt|bcrypt|IPHLPAPI|SHELL32|ole32|CRYPT32|SETUPAPI|CFGMGR32|WINMM|dbghelp' \ - && { echo "::error::meshtasticd.exe has non-system DLL dependencies (static link regressed)"; exit 1; } \ - || echo "OK: no third-party DLL dependencies" + set -euo pipefail + bin=.pio/build/native-windows/meshtasticd.exe + test -f "$bin" + # `|| true` keeps grep's no-match exit out of `set -e`, so an empty + # import table is caught below rather than passing as "no deps". + deps=$(objdump -p "$bin" | grep -i 'DLL Name' || true) + test -n "$deps" + extra=$(printf '%s\n' "$deps" \ + | grep -viE 'api-ms-win|KERNEL32|WS2_32|ADVAPI32|USER32|msvcrt|bcrypt|IPHLPAPI|SHELL32|ole32|CRYPT32|SETUPAPI|CFGMGR32|WINMM|dbghelp' || true) + if [ -n "$extra" ]; then + printf '%s\n' "$extra" + echo "::error::meshtasticd.exe has non-system DLL dependencies (static link regressed)" + exit 1 + fi + echo "OK: no third-party DLL dependencies" - name: Smoke test the binary - continue-on-error: true run: | .pio/build/native-windows/meshtasticd.exe --version From 517c6c9e16221e607e64706aadc5512a84d2bf2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Fri, 17 Jul 2026 20:15:49 +0200 Subject: [PATCH 3/5] Allowlist ucrtbase.dll in Windows self-contained check UCRT cannot be static-linked, so meshtasticd.exe always imports the Universal C Runtime. Depending on the toolchain it appears as either the api-ms-win-crt-* API sets or a direct ucrtbase.dll import; allowlist the latter so the check does not falsely report a static-link regression. --- .github/workflows/build_windows_bin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_windows_bin.yml b/.github/workflows/build_windows_bin.yml index 0dd1b3c5b12..17606f4f5bf 100644 --- a/.github/workflows/build_windows_bin.yml +++ b/.github/workflows/build_windows_bin.yml @@ -116,7 +116,7 @@ jobs: deps=$(objdump -p "$bin" | grep -i 'DLL Name' || true) test -n "$deps" extra=$(printf '%s\n' "$deps" \ - | grep -viE 'api-ms-win|KERNEL32|WS2_32|ADVAPI32|USER32|msvcrt|bcrypt|IPHLPAPI|SHELL32|ole32|CRYPT32|SETUPAPI|CFGMGR32|WINMM|dbghelp' || true) + | grep -viE 'api-ms-win|KERNEL32|WS2_32|ADVAPI32|USER32|msvcrt|ucrtbase|bcrypt|IPHLPAPI|SHELL32|ole32|CRYPT32|SETUPAPI|CFGMGR32|WINMM|dbghelp' || true) if [ -n "$extra" ]; then printf '%s\n' "$extra" echo "::error::meshtasticd.exe has non-system DLL dependencies (static link regressed)" From 9428899b4b2997f612d675431a96906fab78558d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Fri, 17 Jul 2026 20:20:39 +0200 Subject: [PATCH 4/5] Drop workflow_dispatch from Windows build workflow The manual-dispatch inputs tripped checkov CKV_GHA_7, which requires workflow_dispatch inputs to be empty. The job is reached through main_matrix.yml, so match build_macos_bin.yml and keep workflow_call only. --- .github/workflows/build_windows_bin.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/build_windows_bin.yml b/.github/workflows/build_windows_bin.yml index 17606f4f5bf..4f2ad9b27b6 100644 --- a/.github/workflows/build_windows_bin.yml +++ b/.github/workflows/build_windows_bin.yml @@ -7,12 +7,6 @@ on: required: false default: "2025" type: string - workflow_dispatch: - inputs: - windows_ver: - required: false - default: "2025" - type: string permissions: contents: read From 1452145bae9e52aa7a63ab0fe6e1f7e0f76dbe97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Sun, 19 Jul 2026 23:27:44 +0200 Subject: [PATCH 5/5] Shorten comments in the Windows build changes Trim each comment block to at most two lines of fact. Move the runtime DLL resolution note onto load_dll() and the CH341 pin map onto the pin constant, where each applies. --- .github/workflows/build_windows_bin.yml | 31 ++++-------- extra_scripts/windows_link_flags.py | 5 +- src/mesh/HardwareRNG.cpp | 7 +-- src/platform/portduino/GpsdSerial.cpp | 9 +--- src/platform/portduino/PortduinoGlue.cpp | 10 ++-- src/platform/portduino/WindowsMacAddr.cpp | 23 +++------ .../windows/include/libpinedio-usb.h | 8 +-- .../portduino/windows/libpinedio_ch341dll.c | 50 ++++++------------- src/power/PowerHAL.cpp | 6 +-- 9 files changed, 45 insertions(+), 104 deletions(-) diff --git a/.github/workflows/build_windows_bin.yml b/.github/workflows/build_windows_bin.yml index 4f2ad9b27b6..e755c42e6c5 100644 --- a/.github/workflows/build_windows_bin.yml +++ b/.github/workflows/build_windows_bin.yml @@ -16,9 +16,8 @@ jobs: runs-on: windows-${{ inputs.windows_ver }} defaults: run: - # UCRT64 is the MinGW-w64 environment the native-windows env targets. - # (The `msys` environment would link msys-2.0.dll and produce a - # Cygwin-style binary rather than a native one.) + # UCRT64 is the MinGW-w64 environment native-windows targets; the `msys` + # environment would link msys-2.0.dll and produce a Cygwin-style binary. shell: msys2 {0} steps: - name: Checkout code @@ -34,13 +33,8 @@ jobs: with: msystem: UCRT64 update: true - # argp is deliberately absent here - it is not packaged for MSYS2's - # mingw environments and is built from source below. - # - # Python is deliberately absent too: MSYS2's interpreter reports a - # `mingw` platform tag, so no PyPI wheel matches it and pip builds - # every dependency from source. PlatformIO runs under the runner's - # CPython instead (see below); MSYS2 only supplies the toolchain. + # argp is not packaged for the mingw environments and is built below. + # Python is omitted too: MSYS2's reports a `mingw` platform tag no wheel matches. install: >- mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-pkgconf @@ -58,12 +52,8 @@ jobs: with: python-version: "3.13" - # framework-portduino's Arduino.h includes and its main.cpp calls - # argp_parse(). glibc has argp built in and macOS gets it from - # `brew install argp-standalone`, but MSYS2 packages it only for the - # msys runtime (msys/libargp), which can't be linked into a native - # binary - so build it from source. The project ships no install() rules, - # hence the manual copy. + # framework-portduino calls argp_parse(); MSYS2 packages argp only for the + # msys runtime, which cannot link into a native binary. No install() rules. - name: Build and install argp-standalone run: | git clone --depth 1 https://github.com/tom42/argp-standalone /tmp/argp @@ -97,16 +87,15 @@ jobs: - name: List output files run: ls -lah .pio/build/native-windows/ - # The env links statically, so this should report only Windows system - # DLLs. A third-party DLL appearing here means the static link regressed - # and the artifact would need those DLLs shipped alongside it. + # The env links statically, so only Windows system DLLs should appear here. + # A third-party DLL means the static link regressed. - name: Verify the binary is self-contained run: | set -euo pipefail bin=.pio/build/native-windows/meshtasticd.exe test -f "$bin" - # `|| true` keeps grep's no-match exit out of `set -e`, so an empty - # import table is caught below rather than passing as "no deps". + # `|| true` keeps grep's no-match exit out of `set -e`; an empty import + # table is caught by the test below instead of passing as "no deps". deps=$(objdump -p "$bin" | grep -i 'DLL Name' || true) test -n "$deps" extra=$(printf '%s\n' "$deps" \ diff --git a/extra_scripts/windows_link_flags.py b/extra_scripts/windows_link_flags.py index 915305b6cf1..b86322a0d54 100644 --- a/extra_scripts/windows_link_flags.py +++ b/extra_scripts/windows_link_flags.py @@ -2,9 +2,8 @@ # trunk-ignore-all(ruff/F821) # trunk-ignore-all(flake8/F821): For SConstruct imports # -# PlatformIO routes build_flags to the compile step only, so the static link for -# [env:native-windows] has to be appended to LINKFLAGS here, as -# extra_scripts/wasm_link_flags.py does for [env:native-wasm]. +# PlatformIO routes build_flags to the compile step only, so the static link +# flags are appended here, as wasm_link_flags.py does for [env:native-wasm]. Import("env") if env["PIOENV"].startswith("native-windows"): diff --git a/src/mesh/HardwareRNG.cpp b/src/mesh/HardwareRNG.cpp index 7d9f7e15950..43a3e03854f 100644 --- a/src/mesh/HardwareRNG.cpp +++ b/src/mesh/HardwareRNG.cpp @@ -135,11 +135,8 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy) filled = true; } #elif defined(_WIN32) - // No getrandom/arc4random on Windows. BCryptGenRandom with the - // system-preferred RNG is the documented CSPRNG, preferred over the - // std::random_device fallback below because libstdc++'s Windows backend - // reports entropy() == 0, promising no cryptographic source, and this - // buffer seeds key material. + // No getrandom/arc4random on Windows; BCryptGenRandom is the documented CSPRNG. + // Preferred over std::random_device, whose libstdc++ Windows backend reports entropy() == 0. if (BCryptGenRandom(NULL, buffer, static_cast(length), BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0) { // STATUS_SUCCESS filled = true; } diff --git a/src/platform/portduino/GpsdSerial.cpp b/src/platform/portduino/GpsdSerial.cpp index 7c8c52ff8b4..1b80c7d06d5 100644 --- a/src/platform/portduino/GpsdSerial.cpp +++ b/src/platform/portduino/GpsdSerial.cpp @@ -20,13 +20,8 @@ namespace { -// Winsock needs explicit initialization, closes with closesocket(), sets -// non-blocking via ioctlsocket() rather than fcntl(), and reports errors through -// WSAGetLastError() rather than errno. -// -// GpsdSerial.h stores the descriptor in an `int`. That is safe on Win64 even -// though SOCKET is a UINT_PTR: Windows documents socket handles as fitting in 32 -// bits, and INVALID_SOCKET narrows to -1, so the `_sockfd >= 0` checks hold. +// Winsock needs explicit init, closesocket(), ioctlsocket() and WSAGetLastError(). +// SOCKET fits the header's `int` on Win64, and INVALID_SOCKET narrows to -1. #ifdef _WIN32 // Done lazily to keep the dependency local to the one file that needs it. void initSocketsOnce() diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 6a57033ddbe..c8e27eb8d75 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -204,11 +204,8 @@ void getMacAddr(uint8_t *dmac) freeifaddrs(ifap); } #elif defined(_WIN32) - // No BlueZ on Windows, so fall back to the host's primary adapter MAC, - // the same stable host-derived identifier BlueZ gives on Linux and en0 - // gives on macOS. Otherwise the MAC stays all-zero, device_id is left - // unset, and every user has to pass --hwid. On failure dmac is untouched - // and the caller's "Blank MAC Address not allowed!" check still fires. + // No BlueZ on Windows; the host's primary adapter MAC is the equivalent + // stable identifier. On failure dmac is untouched and the blank-MAC check fires. portduinoWindowsPrimaryMac(dmac); #else // No platform-specific MAC source; leave dmac at its default. Caller @@ -310,8 +307,7 @@ void portduinoSetup() if (ends_with(entry.path().string(), ".yaml")) { std::cout << "Also using " << entry << " as additional config file" << std::endl; // .string() rather than .c_str(): path::value_type is wchar_t on - // Windows, and loadConfig() takes a const char *. The temporary - // outlives the call. + // Windows, and loadConfig() takes a const char *. loadConfig(entry.path().string().c_str()); } } diff --git a/src/platform/portduino/WindowsMacAddr.cpp b/src/platform/portduino/WindowsMacAddr.cpp index 9878136d9f5..8200fdc1b96 100644 --- a/src/platform/portduino/WindowsMacAddr.cpp +++ b/src/platform/portduino/WindowsMacAddr.cpp @@ -1,20 +1,13 @@ #if defined(ARCH_PORTDUINO) && defined(_WIN32) -// Host-MAC lookup for PortduinoGlue's getMacAddr(), standing in for the BlueZ -// HCI path on Linux and the en0/getifaddrs path on macOS. -// -// Its own translation unit on purpose: pulls in the RPC/OLE chain, -// whose `boolean` and MSG types collide with the Arduino API, which is why the -// native-windows env builds with -DNOUSER -DWIN32_LEAN_AND_MEAN -DNOGDI. Those -// trims in turn break itself (oleidl.h needs LPMSG). Keeping this -// file free of Arduino headers lets us undo them locally. +// Host-MAC lookup for getMacAddr(), replacing BlueZ on Linux and en0 on macOS. +// Isolated TU: needs the header trims the env sets, and undoes them here. #undef WIN32_LEAN_AND_MEAN #undef NOUSER #undef NOGDI -// Order is load-bearing, hence the blank lines: winsock2.h must come first, or -// windows.h pulls in the winsock v1 header and the two collide. iphlpapi.h -// needs both. +// Order is load-bearing, hence the blank lines: winsock2.h must precede +// windows.h, which would otherwise pull in the colliding winsock v1 header. #include #include @@ -25,12 +18,8 @@ #include #include -// Fill dmac with the first up, non-loopback adapter's 6-byte physical address. -// Returns false and leaves dmac untouched if none is found. -// -// GetAdaptersAddresses() replaces the deprecated GetAdaptersInfo(). Its ordering -// is driver-determined but stable across reboots, so this picks the same adapter -// each run and the node keeps a stable identity. +// Fill dmac with the first up, non-loopback adapter's physical address, else +// return false. Adapter order is stable across reboots, so the identity persists. bool portduinoWindowsPrimaryMac(uint8_t *dmac) { const ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_SKIP_FRIENDLY_NAME; diff --git a/src/platform/portduino/windows/include/libpinedio-usb.h b/src/platform/portduino/windows/include/libpinedio-usb.h index ee8a1a77bde..3fe0e141a5f 100644 --- a/src/platform/portduino/windows/include/libpinedio-usb.h +++ b/src/platform/portduino/windows/include/libpinedio-usb.h @@ -1,9 +1,5 @@ -// Windows drop-in for libch341-spi-userspace's public header. -// -// Same API surface as the upstream libpinedio-usb.h that Ch341Hal -// (src/platform/portduino/USBHal.h) compiles against, but without libusb: the -// implementation (libpinedio_ch341dll.c) drives WCH's CH341DLL instead. See the -// .c for why. Mirrors the wasm drop-in in ../../wasm/include/. +// Windows drop-in for libch341-spi-userspace's public header, mirroring the wasm +// one in ../../wasm/include/. Same API as upstream, but backed by CH341DLL, not libusb. #ifndef PINEDIO_USB_CH341DLL_H #define PINEDIO_USB_CH341DLL_H #ifdef __cplusplus diff --git a/src/platform/portduino/windows/libpinedio_ch341dll.c b/src/platform/portduino/windows/libpinedio_ch341dll.c index e2b30218cd2..2f0b8e3708b 100644 --- a/src/platform/portduino/windows/libpinedio_ch341dll.c +++ b/src/platform/portduino/windows/libpinedio_ch341dll.c @@ -1,18 +1,5 @@ -// libpinedio API implemented over WCH's CH341DLL, replacing the libusb backend -// on Windows. Mirrors what wasm/libpinedio_webusb.c does for the browser. -// -// WHY NOT LIBUSB: libusb on Windows can only talk to a device bound to -// WinUSB/libusbK/libusb0, so it needs Zadig to replace the driver by hand on -// every machine, and Zadig installs a self-signed CA into the trust store. -// CH341DLL ships with WCH's signed CH341PAR driver and a normal installer. -// -// The DLL is closed-source and not redistributable, so it is resolved at -// runtime: with no CH341PAR installed, pinedio_init() fails and the caller -// (Ch341Hal's constructor) throws, which is the same path a missing device -// takes. Nothing else in the build depends on it. -// -// Pin numbering is the CH341's D0-D7, as upstream. For the PineDio adapter the -// YAML gives CS: 0, IRQ: 6, Reset: 2 against D3=SCK, D5=MOSI, D7=MISO. +// libpinedio API over WCH's CH341DLL, replacing the libusb backend on Windows: +// libusb would need Zadig to rebind the driver, CH341DLL ships with CH341PAR. #if defined(ARCH_PORTDUINO) && defined(_WIN32) @@ -24,8 +11,8 @@ #include #include -// CH341Set_D5_D0 only reaches D0-D5. That covers every output the adapter uses -// (CS/Reset/SCK/MOSI); D6 (IRQ) and D7 (MISO) are inputs, read via CH341GetInput. +// Pins are the CH341's D0-D7: CS 0, Reset 2, SCK 3, MOSI 5, IRQ 6, MISO 7. +// CH341Set_D5_D0 reaches only the outputs; D6/D7 are read via CH341GetInput. #define CH341_MAX_OUTPUT_PIN 5 // iMode bit 7: SPI bit order, 1 = MSB first. The SX1262 is MSB-first, and doing @@ -68,10 +55,8 @@ static pthread_t poll_thread; static volatile bool poll_thread_exit = false; static int int_running_cnt = 0; -// CH341PAR installs the 64-bit library as CH341DLLA64.DLL in System32 and the -// 32-bit one as CH341DLL.DLL in SysWOW64, so a 64-bit process must ask for the -// A64 name; plain CH341DLL.DLL would either not be found or be the wrong -// architecture. Try the matching name first and fall back to the other. +// CH341PAR installs the 64-bit library as CH341DLLA64.DLL and the 32-bit one as +// CH341DLL.DLL, so try the name matching this process first. static const char *const ch341_dll_names[] = { #ifdef _WIN64 "CH341DLLA64.DLL", @@ -82,6 +67,8 @@ static const char *const ch341_dll_names[] = { #endif }; +// The DLL is not redistributable, so it is resolved at runtime: without it +// pinedio_init() fails and Ch341Hal throws, as it does for a missing device. static bool load_dll(void) { if (ch341.dll) @@ -151,10 +138,8 @@ int32_t pinedio_init(struct pinedio_inst *inst, void *driver) pin_dir_out = 0; pin_state = 0; - // CH341DLL exposes no USB serial string. Ch341Hal only uses serial_number to - // pick between multiple adapters and to derive a MAC; on Windows getMacAddr() - // falls back to the host adapter, so leaving it empty is safe. The device name - // is the closest stand-in for the product string. + // CH341DLL exposes no USB serial string. Leaving it empty is safe: on Windows + // getMacAddr() uses the host adapter, and the device name stands in for the product. inst->serial_number[0] = '\0'; inst->product_string[0] = '\0'; if (ch341.get_device_name) { @@ -288,9 +273,8 @@ int32_t pinedio_write_read(struct pinedio_inst *inst, uint8_t *writearr, uint32_ return ret; } -// The CH341's own interrupt (CH341SetIntRoutine) only fires on its INT# pin, but -// the adapter wires the radio's DIO1 to D6, so it can't be used here. Poll D6 -// instead, at upstream's rate and with upstream's edge semantics. +// CH341SetIntRoutine only fires on the INT# pin, but the adapter wires DIO1 to +// D6, so poll D6 instead, at upstream's rate and edge semantics. static void *pin_poll_thread_fn(void *arg) { struct pinedio_inst *inst = (struct pinedio_inst *)arg; @@ -311,9 +295,8 @@ static void *pin_poll_thread_fn(void *arg) pthread_mutex_lock(&usb_mutex); for (uint8_t int_pin = 0; int_pin < PINEDIO_INT_PIN_MAX; int_pin++) { struct pinedio_inst_int *inst_int = &inst->interrupts[int_pin]; - // Copy the pointer while holding the lock: a concurrent - // pinedio_deattach_interrupt() could NULL it once we drop the lock - // to make the call. + // Copy under the lock: pinedio_deattach_interrupt() could NULL it + // once we drop the lock to make the call. void (*cb)(void) = inst_int->callback; if (cb == NULL) continue; @@ -350,9 +333,8 @@ int32_t pinedio_attach_interrupt(struct pinedio_inst *inst, enum pinedio_int_pin inst->interrupts[int_pin].mode = int_mode; inst->interrupts[int_pin].callback = callback; - // Only a new attachment changes the refcount. Re-attaching an already-armed - // pin (RadioLib does this) must not drop the count to 0 and spawn a second - // poll thread alongside the running one. + // Only a new attachment changes the refcount. Re-attaching an armed pin + // (RadioLib does this) must not reach 0 and spawn a second poll thread. if (!was_attached) { if (int_running_cnt == 0) { poll_thread_exit = false; diff --git a/src/power/PowerHAL.cpp b/src/power/PowerHAL.cpp index 3e8d163ff13..bc6af41435e 100644 --- a/src/power/PowerHAL.cpp +++ b/src/power/PowerHAL.cpp @@ -1,10 +1,8 @@ #include "PowerHAL.h" -// PE/COFF has no ELF-style weak definitions: GNU as lowers __attribute__((weak)) -// to a COFF weak external, which the linker treats as an undefined reference -// rather than a fallback, so these defaults would not link on Windows. Only -// nrf52 and nrf54l15 override them, so define them strongly there. +// PE/COFF has no ELF-style weak definitions, so a weak default is an undefined +// reference on Windows. Only nrf52 and nrf54l15 override these; define them strongly. #ifdef _WIN32 #define POWERHAL_WEAK_DEFAULT __attribute__((noinline)) #else