Add native Windows build of meshtasticd (#11031)
This commit is contained in:
co-authored by
GitHub
parent
4726375282
commit
a808e992a1
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user