Add native Windows build of meshtasticd (#11031)

This commit is contained in:
Thomas Göttgens
2026-07-19 23:31:19 +02:00
committed by GitHub
co-authored by GitHub
parent 4726375282
commit a808e992a1
16 changed files with 910 additions and 21 deletions
+120
View File
@@ -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
+12
View File
@@ -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
+16
View File
@@ -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++",
]
)
+12 -3
View File
@@ -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);
+4
View File
@@ -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);
+12
View File
@@ -22,6 +22,12 @@ extern Adafruit_nRFCrypto nRFCrypto;
#include <unistd.h>
#ifdef __linux__
#include <sys/random.h> // 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 <windows.h>
#include <bcrypt.h> // BCryptGenRandom()
#else
#include <stdlib.h> // arc4random_buf() on Darwin/BSD
#endif
@@ -128,6 +134,12 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
if (generated == static_cast<ssize_t>(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<ULONG>(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().
+4
View File
@@ -33,7 +33,11 @@
#include "IPAddress.h"
#if defined(ARCH_PORTDUINO)
#if defined(_WIN32)
#include <winsock2.h> // ntohl()
#else
#include <netinet/in.h>
#endif
#elif !defined(ntohl)
#include <machine/endian.h>
#define ntohl __ntohl
+92 -11
View File
@@ -3,13 +3,92 @@
#include "GpsdSerial.h"
#include "configuration.h"
#include <arpa/inet.h>
#include <cerrno>
#ifdef _WIN32
#include <mutex>
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#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<SOCKET>(fd));
}
void setNonBlocking(int fd)
{
u_long mode = 1;
::ioctlsocket(static_cast<SOCKET>(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<SOCKET>(fd), static_cast<char *>(buf), static_cast<int>(len), 0);
}
int sendAll(int fd, const void *buf, size_t len)
{
return ::send(static_cast<SOCKET>(fd), static_cast<const char *>(buf), static_cast<int>(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<int>(::recv(fd, buf, len, MSG_DONTWAIT));
}
int sendAll(int fd, const void *buf, size_t len)
{
return static_cast<int>(::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<int>(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<int>(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<size_t>(n) < space) ? static_cast<size_t>(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();
}
+18 -2
View File
@@ -21,8 +21,12 @@
#include <memory>
#include <set>
#include <stdexcept>
#include <sys/ioctl.h>
#include <unistd.h>
#ifndef _WIN32
// Only the PORTDUINO_LINUX_HARDWARE block below calls ioctl() (HCIGETDEVINFO,
// for the BlueZ-derived MAC address); Windows has no <sys/ioctl.h>.
#include <sys/ioctl.h>
#endif
#ifdef PORTDUINO_LINUX_HARDWARE
#include "linux/gpio/LinuxGPIOPin.h"
@@ -34,6 +38,12 @@
#include <cxxabi.h>
#endif
#ifdef _WIN32
// Defined in WindowsMacAddr.cpp, which keeps <iphlpapi.h> 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; `<net/if_dl.h>` 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());
}
}
}
+1
View File
@@ -8,6 +8,7 @@
#include <cstring>
#include <iostream>
#include <libpinedio-usb.h>
#include <sys/time.h> // gettimeofday(), previously pulled in via libusb.h
#include <unistd.h>
extern uint32_t rebootAtMsec;
+54
View File
@@ -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: <iphlpapi.h> 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 <winsock2.h>
#include <windows.h>
#include <iphlpapi.h>
#include <cstring>
#include <memory>
#include <stdint.h>
// 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<char[]> buf(new char[bufLen]);
auto *addrs = reinterpret_cast<IP_ADAPTER_ADDRESSES *>(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<IP_ADAPTER_ADDRESSES *>(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
@@ -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 <stdbool.h>
#include <stdint.h>
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
@@ -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 <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
// 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
+11 -3
View File
@@ -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;
}
+2 -2
View File
@@ -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
+77
View File
@@ -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 <argp.h> 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 <windows.h>, 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}
-<input/LinuxInput.cpp>
-<input/LinuxInputImpl.cpp>
-<graphics/Panel_sdl.cpp>
-<graphics/TFTDisplay.cpp>
; LovyanGFX includes <malloc.h> 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