diff --git a/.github/workflows/build_windows_bin.yml b/.github/workflows/build_windows_bin.yml new file mode 100644 index 00000000000..e755c42e6c5 --- /dev/null +++ b/.github/workflows/build_windows_bin.yml @@ -0,0 +1,120 @@ +name: Build Windows Binary + +on: + workflow_call: + 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 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 + 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 + id: msys2 + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + # 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 + 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 + git + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + + # 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 + 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 + shell: pwsh + run: | + python -m pip install --upgrade pip + pip install platformio + + - name: Get release version string + 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 + 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 }} + + - name: List output files + run: ls -lah .pio/build/native-windows/ + + # 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`; 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" \ + | 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)" + exit 1 + fi + echo "OK: no third-party DLL dependencies" + + - name: Smoke test the binary + 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..b86322a0d54 --- /dev/null +++ b/extra_scripts/windows_link_flags.py @@ -0,0 +1,16 @@ +#!/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 +# flags are appended here, as 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 400bfd1aa4a..94288529e16 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -319,7 +319,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); @@ -341,7 +344,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); @@ -355,7 +361,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 47d51dea34d..ff23565b467 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..43a3e03854f 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,12 @@ 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 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; + } #elif defined(__EMSCRIPTEN__) // Browser/wasm: no getrandom/arc4random - fall through to std::random_device, // which emscripten backs with crypto.getRandomValues(). diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index f8ab7fc07c0..7e3d527ee4a 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..1b80c7d06d5 100644 --- a/src/platform/portduino/GpsdSerial.cpp +++ b/src/platform/portduino/GpsdSerial.cpp @@ -3,13 +3,92 @@ #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 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() +{ + 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 +107,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 +122,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 +138,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 +161,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 +177,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 8da3d085b90..c8e27eb8d75 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,10 @@ void getMacAddr(uint8_t *dmac) } freeifaddrs(ifap); } +#elif defined(_WIN32) + // 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 // can override via the --hwid CLI flag or the YAML config. @@ -292,7 +306,9 @@ 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 *. + 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..8200fdc1b96 --- /dev/null +++ b/src/platform/portduino/WindowsMacAddr.cpp @@ -0,0 +1,54 @@ +#if defined(ARCH_PORTDUINO) && defined(_WIN32) + +// 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 precede +// windows.h, which would otherwise pull in the colliding winsock v1 header. +#include + +#include + +#include + +#include +#include +#include + +// 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; + + 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..3fe0e141a5f --- /dev/null +++ b/src/platform/portduino/windows/include/libpinedio-usb.h @@ -0,0 +1,75 @@ +// 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 +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..2f0b8e3708b --- /dev/null +++ b/src/platform/portduino/windows/libpinedio_ch341dll.c @@ -0,0 +1,400 @@ +// 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) + +#include "libpinedio-usb.h" + +#include +#include +#include +#include +#include + +// 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 +// 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 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", + "CH341DLL.DLL", +#else + "CH341DLL.DLL", + "CH341DLLA64.DLL", +#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) + 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. 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) { + 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; +} + +// 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; + 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 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; + 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 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; + 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..bc6af41435e 100644 --- a/src/power/PowerHAL.cpp +++ b/src/power/PowerHAL.cpp @@ -1,19 +1,27 @@ #include "PowerHAL.h" +// 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 +#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 083b855184d..8ea17653acf 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -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 1c9a9a16174..22ac4ff5f4a 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -251,6 +251,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