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
parent 4726375282
commit a808e992a1
16 changed files with 910 additions and 21 deletions
@@ -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