☰
在浏览器中运行真实 Meshtastic 节点:ARCH_PORTDUINO_WASM 的 WebAssembly + WebUSB 架构解析
2026/10/2 13:44:27 网站建设 项目流程
  • 物联网
  • 嵌入式
  • 通信
  • 智能硬件

【免费下载链接】firmware

The official firmware for Meshtastic, an open-source, off-grid mesh communication system.

项目地址:https://gitcode.com/GitHub_Trending/fi/firmware
点击查看免费下载

本指南围绕 src/platform/portduino/wasm/README.md 展开,系统讲解 Meshtastic 官方固件如何通过 Emscripten 将完整的 portduino 固件(setup()/loop())编译为 WebAssembly,让一个真实的 Meshtastic 节点在浏览器标签页(或 headless Node)中运行,并通过WebUSB驱动 CH341 USB-to-SPI 桥接器连接 LoRa 无线电。读完本文,你将掌握该构建目标的环境配置、编译命令、宿主端 JS 引导流程、WebUSB 桥接层原理、LoRa 适配器参数配置、PhoneAPI 对接方式以及其单线程协作式调度与重启语义。

一、方案定位:同一套固件,三个后端

传统上,meshtasticd桌面守护进程(portduino 原生构建)通过 libusb 直接访问 CH341 USB-to-SPI 芯片,进而驱动 SX1262 等 LoRa 射频芯片。ARCH_PORTDUINO_WASM的目标是把这条链路整体搬进浏览器:

  • 固件本体不变:仍然是那套完整的setup()/loop()portduino 固件,只是以 wasm 形态存在;
  • 射频 HAL 不变:仍然走Ch341Hal(桌面meshtasticd同一条路径),只不过底层的libusb 后端被替换为 WebUSB 后端;
  • 原生构建不受影响:wasm/目录在原生 PlatformIO 的build_src_filter中被显式排除(见 variants/native/portduino.ini 的-<platform/portduino/wasm/>注释),所有 wasm 相关改动都以#ifdef ARCH_PORTDUINO_WASM守卫,不会污染其他构建。

从源码结构看,可以这样理解整条调用链:

固件 RadioLib/Ch341Hal -> libpinedio API(include/libpinedio-usb.h) -> libpinedio_webusb.c(EM_ASYNC_JS,Asyncify 挂起) -> js/bridge.js(实现 C 后端的 webusb_* imports) -> js/ch341.js(CH341 WebUSB 传输层) -> js/protocol.js(CH341 线协议 framing) -> navigator.usb(Chromium WebUSB)

其中Ch341Hal定义于 src/platform/portduino/USBHal.h,其构造函数默认 VID 0x1A86、PID 0x5512,与 WebUSB 侧协议常量一致。

二、目录结构与各文件职责

wasm/目录在原生构建中被排除,专供[env:native-wasm]使用,各文件角色如下(摘自 README 布局表,并结合源码核实):

文件角色
portduino_glue_wasm.cppLoRa 配置(MeshToad 默认值 +wasm_set_lora_*setter,无 YAML)、VFS 挂载、region/MAC 辅助函数,以及wasm_api_*PhoneAPI 桥接
portduino_main_wasm.cppwasm_setup()/wasm_loop_once()—— 由 JS 驱动协作式循环
libpinedio_webusb.cWebUSB libpinedio 后端:通过 Asyncify 的EM_ASYNC_JS让同步 C 调用 await 异步 WebUSB Promise
include/libpinedio-usb.h后端实现的 12 个函数的 libpinedio API(与固件Ch341Hal编译所依赖的上游 libpinedio-usb.h 同一套接口面,但不含 libusb/pthread 依赖)
stubs/argp.hshim + jsoncpp 序列化器 stub(仅 MQTT 使用,wasm 构建已排除 MQTT)
js/WebUSB 运行时:bridge.js(实现 C 后端的 imports)、ch341.js(CH341 传输)、protocol.js(framing)

stubs 的存在价值:framework-portduino的 Arduino.h 无条件#include <argp.h>,但 wasm 构建里没有任何代码调用argp_parse(原来的 framework main.cpp 已被替换),所以 argp.h 只提供编译所需的类型与宏;而 serializer_stub.cpp 为空实现MeshPacketSerializer::JsonSerialize*,用于满足Router.cpp的链接引用而不引入 jsoncpp(MQTT JSON 输出在 wasm 构建中被排除)。

三、构建:普通的 PlatformIO 环境

[env:native-wasm]定义在 variants/native/portduino/platformio.ini,是一个标准 PlatformIO 环境,工具链来自 meshtastic/platform-wasm 平台(emcc/em++),与编译其他板卡目标的方式完全一致。

3.1 前置条件

需要Emscripten SDK 位于 PATH上,以便平台构建器定位emcc:

source <emsdk>/emsdk_env.sh # 或 export EMSDK=<path>

3.2 编译命令

pio run -e native-wasm # emcc 编译 + Asyncify 链接 pio run -e native-wasm -t clean # 清空构建目录

输出产物:.pio/build/native-wasm/meshnode.mjs+meshnode.wasm。meshnode.mjs是 ES 模块,工厂函数名为createMeshNode,导出_wasm_setup、_wasm_loop_once、_wasm_fs_sync、_wasm_set_region、_wasm_api_to_radio、_wasm_api_from_radio、_wasm_api_available、_wasm_api_is_connected以及_wasm_set_lora_*系列 setter。

3.3 链接期参数:wasm_link_flags.py

PlatformIO 只把build_flags送到编译步骤,不会送到链接步骤,因此 wasm 节点的链接期 Emscripten 设置由 extra_scripts/wasm_link_flags.py 追加到LINKFLAGS(该脚本带env["PIOENV"] == "native-wasm"守卫,不会被其他环境误用)。关键设置(见该脚本第 27-40 行):

  • -sEXPORT_NAME=createMeshNode:ES 模块工厂名,消费者 import 的就是它;
  • -sEXPORTED_RUNTIME_METHODS=...:导出ccall/cwrap/callMain/FS/IDBFS/NODEFS/PATH/HEAPU8/UTF8ToString/stringToUTF8,供 JS 宿主与桥接层驱动;
  • -sEXPORTED_FUNCTIONS=_main,_wasm_setup,_wasm_loop_once,_wasm_fs_sync,...:显式导出全部 C 入口;_malloc/_free供宿主编组 protobuf 缓冲区;
  • -sASYNCIFY_IMPORTS=webusb_open,webusb_transceive,webusb_digital_write,webusb_digital_read,webusb_close:声明 WebUSB 缝(seam)——这 5 个被导入的 C 函数会在 WebUSB 传输 await 期间挂起(Asyncify)调用栈。

而通用的、与应用无关的 Emscripten 设置(Asyncify、MODULARIZE、ALLOW_MEMORY_GROWTH、ES6 模块形态)由 platform-wasm 平台构建器提供。同时,env:native-wasm 的编译期宏大量排除了浏览器里无法运行的能力:HAS_SCREEN=0、MESHTASTIC_EXCLUDE_GPS/I2C/AUDIO/MQTT/SERIAL/STOREFORWARD/CANNEDMESSAGES等,并强制-D ARCH_PORTDUINO_WASM,确保构建正确性不依赖于仓库外的 board 文件。

3.4 CI 保障

该构建目标并非一次性脚本,而是常驻 CI:.github/workflows/build_portduino_wasm.yml会执行pio run -e native-wasm并校验产物存在、node --check校验 JS 语法;主 CI matrix(.github/workflows/main_matrix.yml)也把 wasm 构建纳入常规流程,防止该目标静默腐化。

四、运行:JS 驱动协作式循环

4.1 最小宿主流程

C 后端导入webusb_*函数,js/bridge.js 在ch341.js之上实现它们。README 给出的最小宿主流程:

import createMeshNode from "./meshnode.mjs"; import { CH341 } from "./js/ch341.js"; import { createCH341Bridge } from "./js/bridge.js"; const dev = (await CH341.request()).device; // WebUSB 设备选择器(Chromium) const Module = await createMeshNode({ noInitialRun: true }); Module.ch341 = createCH341Bridge(Module, dev); // 启动前先接好 WebUSB await Module.ccall("wasm_setup", null, [], [], { async: true }); const pump = async () => { await Module.ccall("wasm_loop_once", "number", [], [], { async: true }); setTimeout(pump, 5); }; pump();

关键顺序:先通过用户手势授权 WebUSB 设备(CH341.request()),再把桥接对象挂到Module.ch341上,最后才调用wasm_setup()。wasm_setup()内部依次执行wasm_fs_mount()(挂载/meshdata)→portduinoSetup()(应用 wasm 配置并构造 WebUSB 版Ch341Hal)→setup()(完整固件初始化),见 portduino_main_wasm.cpp。

4.2 协作式调度而非while(1)泵

framework-portduino 原生 main.cpp 使用while(1){ loop(); usleep(loopDelay); }泵,这在浏览器里不可行(会卡死标签页)。替换后的 portduino_main_wasm.cpp 让main()直接返回,由 JS 以定时器反复调用wasm_loop_once():

extern "C" EMSCRIPTEN_KEEPALIVE int wasm_loop_once() { g_wasm_in_firmware = true; loop(); g_wasm_in_firmware = false; return 5; // ms;JS 侧以此封顶调度节奏 }

固件自身的loop()已经做了service->loop(); mainController.runOrDelay(); RadioLibInterface::instance->pollMissedIrqs();(src/main.cpp 附近),所以每个 tick 只需调用一次。这种设计让 Asyncify 调用栈保持很浅——只会在 WebUSB 传输内部挂起,而不是跨空闲期挂起——同时 JS 可以在 tick 之间泵送 WebUSB IRQ 轮询。

配合这一协作式模型,src/main.cpp 中的ARCH_PORTDUINO_WASM守卫把原生mainDelay.delay(delayMsec)(基于 pthread cond/mutex,单线程 wasm 下无人能give()且会忙转)替换为emscripten_sleep,并把单次空闲睡眠封顶在 50ms,以保持每 tick 的 IRQ 轮询延迟有界。

五、WebUSB 桥接层:从同步 C 到异步 Promise

这是整个 wasm 方案最核心的工程点:固件里的 RadioLib 是完全同步的 SPI 代码,而 WebUSB 是 Promise-only 的异步 API。解决之道是Asyncify + EM_ASYNC_JS。

5.1 libpinedio_webusb.c:C 侧后端

libpinedio_webusb.c 实现了 include/libpinedio-usb.h 声明的 12 个函数(pinedio_init、pinedio_set_option、pinedio_set_pin_mode、pinedio_digital_write、pinedio_set_cs、pinedio_write_read、pinedio_transceive、pinedio_digital_read、pinedio_get_irq_state、pinedio_attach_interrupt、pinedio_deattach_interrupt、pinedio_deinit)。其中异步操作通过EM_ASYNC_JS声明(第 26-41 行),同步操作通过EM_JS声明(第 45-48 行):

EM_ASYNC_JS(int, webusb_transceive, (int writePtr, int readPtr, int count), { return await Module.ch341.transceive(writePtr, readPtr, count); });

设计要点(源码注释中明确说明):

  • 每次 SPI 传输一个异步挂起,而不是每个包一个,以控制 Asyncify 开销;
  • "单次在途 USB 操作"不变式自动成立:每个 C 调用都会等待完整的 JS 操作返回,且节点是单线程协作式的,不会有传输重叠;
  • attachInterrupt不启动线程(上游 lib 通过 pthread 轮询 USB),只记录回调,真正的 RX/TX 检测依赖固件主循环里的pollMissedIrqs()/IRQ 标志轮询;
  • await之后 wasm 堆可能增长(ALLOW_MEMORY_GROWTH),所以 JS 桥在写回结果时必须重新读取Module.HEAPU8。

5.2 bridge.js:实现 C 后端的 imports

js/bridge.js 的createCH341Bridge(Module, device)返回一个与 C 后端方法一一对应的对象:open、transceive、digitalWrite、digitalRead、setPinMode、setAutoCS、getSerial、getProduct、close。

几个工程细节值得注意:

  • open带重试退避:首次连接在授权后 WebUSB 接口可能瞬时不可声明(或仍被前一会话持有,表现为瞬时的 "Could not open SPI: -1"),因此最多尝试 4 次,每次之间dev.close()重置设备并以 200ms×(i+1) 退避(第 44-57 行);
  • 堆增长安全:transceive在 await 前先slice复制写缓冲,await 后重新读取Module.HEAPU8再写回读缓冲(第 65-70 行)——注释明确警告:绝不能在挂起点之间缓存 HEAPU8 引用。

5.3 ch341.js + protocol.js:传输与线协议

js/ch341.js 是浏览器侧 CH341 传输层,暴露CH341.request()(必须由用户手势触发,内部调用navigator.usb.requestDevice,过滤条件为 VID 0x1a86 / PID 0x5512)与CH341.tryReconnect()(免提示重连已授权设备)。open()中会声明接口 0 的 bulk EP 0x02/0x82,并把 D3=SCK、D5=MOSI 配置为输出(D7=MISO 保持输入)——这与Ch341Hal构造函数的pinedio_set_pin_mode(&pinedio, 3, true)完全镜像。

js/protocol.js 是无副作用的 framing 纯函数(因此可以在 node 下无硬件单测,并被浏览器驱动与 wasm C 后端复用),忠实移植了 libch341-spi-userspace 的组帧:

  • SPI 流:命令0xA8,每个 USB 包 ≤32 字节(1 命令字节 + 至多 31 数据字节);CH341 在线上位序反转,所以每个 SPI 字节(TX 与 RX)都要reverseByte;
  • GPIO(D0..D7):UIO 流命令0xAB;D0 接到 CS;
  • 输入读取:状态命令0xA0。

transceive是完整双工传输,且对 WebUSB 的短读做了严格处理:transferIn可能返回比请求少的字节,必须循环累积到全部 MISO 字节,一旦设备返回 0 字节则显式抛错而不是把缺失字节当 0 填充——否则会把射频芯片置于错误状态(初始化期间偶发挂死)。

六、LoRa 适配器配置:wasm_set_lora_*setter 与 MeshToad 默认值

浏览器节点没有 YAML 配置文件(PortduinoGlue.cpp中ARCH_PORTDUINO_WASM分支下的loadConfig()是空实现,见 PortduinoGlue.cpp)。配置通过 portduino_glue_wasm.cpp 中导出的 6 个 setter 从 JS 侧注入:

setter作用
wasm_set_lora_module(int enum)选择 LoRa 模块(lora_module_enum),是触发开关:不设置时wasm_config_apply()回退到 MeshToad 默认值
wasm_set_lora_usb_ids(vid, pid)覆盖 USB VID/PID
wasm_set_lora_usb_serial(serial)按序列号锁定特定设备(空则取首个匹配)
wasm_set_lora_dio_config(dio2_as_rf_switch, dio3_tcxo_mv)DIO2 是否作为 TX/RX 射频开关;DIO3 TCXO 电压(mV)
wasm_set_lora_spi_speed(hz)SPI 时钟频率
wasm_set_lora_pin(name, pin)按引脚名(CS/IRQ/BUSY/RESET/RXEN/TXEN/ANT_SW)设置 D 线号,pin为RADIOLIB_NC时禁用

这些 setter 必须在wasm_setup()之前调用。若 JS 未做任何预置,wasm_config_apply()(第 201-230 行)回退到MeshToad(E22/SX1262 over CH341)默认值:

  • lora_module = use_sx1262
  • lora_usb_vid = 0x1A86、lora_usb_pid = 0x5512、serial 留空(取首个匹配设备)
  • dio2_as_rf_switch = true(E22 用 DIO2 做 TX/RX 开关)
  • dio3_tcxo_voltage = 1800(1.8 V TCXO)
  • spiSpeed = 2000000(2 MHz)
  • MeshToad CH341 D 线引脚映射:CS=D0、IRQ=D6、BUSY=D4、RESET=D2、RXen=D1(对应桌面端bin/config.d/lora-usb-meshtoad-e22.yaml)

此外,浏览器/headless 不变式始终生效:lora_spi_dev = "ch341"(这里所有适配器都是 WebUSB/CH341)、displayPanel = no_screen、has_gps = false、MaxNodes = 80(浏览器里的小型节点数据库)。

七、PhoneAPI 桥接:把固件的客户端 API 直接暴露给 JS

固件的PhoneAPI是与传输无关的请求/响应状态机——TCP、串口、BLE、HTTP 服务都包装它。wasm 构建排除了这些服务,因此 portduino_glue_wasm.cpp 直接把PhoneAPI暴露给 JS:

class WasmPhoneAPI : public PhoneAPI { public: virtual bool checkIsConnected() override { return true; } // 进程内永远在线 };

暴露的 4 个导出函数:

  • wasm_api_to_radio(ptr, len):喂入一条未加帧的ToRadioprotobuf;返回 1 接受 / 0 拒绝(如端口限流,而非传输错误);
  • wasm_api_from_radio(out, max):写出一条FromRadio;返回字节数,无可读数据时返回 0,out必须 ≥ 512 字节;
  • wasm_api_available():无缓冲的廉价可读检查;
  • wasm_api_is_connected():配置握手是否在进行/已完成(state != NOTHING)。

这正是设备 HTTP API 使用的未加帧契约。官方@meshtastic/coreSDK 通过一个约 40 行的进程内传输驱动它(对应的meshtasticd-wasm-node仓库托管了 dev server、SDK-UI 页面、headless node-usb 运行器,以及供 Python CLI 使用的 TCP :4403 桥接)。WebUSB 仅 Chromium 支持。

WasmPhoneAPI采用懒构造(首次 API 调用时service必然已就绪),并刻意用available()轮询而非onNowHasData()推送,把所有 API 调用挡在wasm_loop_once()tick 之外(见下方重入规则)。

八、运行时 region 切换与重入保护

wasm_set_region(region)(第 249-281 行)允许 UI(浏览器<select>)或 headless(MESH_REGIONenv)在运行时切换 LoRa region,完整镜像AdminModule的 set_config(lora) region 路径:校验 →(首次设置 region 时)PKI 密钥生成 + 开启 TX →initRegion()→service->reloadConfig()(重算载波频率并落盘)→wasm_fs_sync()。region 变更只需重配置、无需重启。返回 0 成功、-1 校验失败、-2 表示忙。

重入保护是本方案的一个隐蔽但重要的安全网:节点是单线程 + Asyncify 的,当setup()/loop()挂起在一次 WebUSB 传输内时 JS 事件循环是空闲的,一个游离的 DOM 或定时器回调可能重入wasm_*入口,从而启动第二次 Asyncify 展开("async operation already in flight" abort)或破坏共享的 PhoneAPI 状态。因此 portduino_glue_wasm.cpp 定义全局标志g_wasm_in_firmware,在 portduino_main_wasm.cpp 的wasm_setup/wasm_loop_once内置位,让wasm_api_*与wasm_set_region在 tick 中途被调用时拒绝而非损坏状态。真正可靠的修复是 JS 侧排队,此标志只是兜底安全网。

九、节点身份与持久化:MAC 解析与 IDBFS/NODEFS

9.1 每实例唯一 MAC

每个浏览器节点必须有互不相同的 MAC,否则低 4 字节派生的 32 位 NodeNum(pickNewNodeNum: mac[2..5])会碰撞,导致同一 mesh 上的身份冲突。wasm_resolve_mac() 按优先级解析:

  1. MESH_MAC环境变量(headless 确定性 / 对等测试,如DEAD00C0FFEE;浏览器中忽略process.env);
  2. /meshdata/oem/mac中持久化的值(跨刷新/重启存活);
  3. 新生成的本地管理随机 MAC并持久化(置 bit1=1 本地管理、bit0=0 单播,见第 132 行m[0] = (m[0] | 0x02) & 0xFE)。

该函数在wasm_config_apply()内运行(即portduinoSetup()期间、setup()的pickNewNodeNum之前),此时/meshdata已由 JS 挂载。它同时短路了getMacAddr()的 popen(浏览器里没有 shell)。

9.2 VFS 挂载与同步

wasm_fs_mount() 创建/meshdata、/meshdata/prefs、/meshdata/oem子树并把portduinoVFS挂载点设为/meshdata,否则 NodeDB/config 每次保存都会报 "File system is not mounted"。JS 宿主预先在/meshdata上FS.mount了 IDBFS(浏览器)或 NODEFS(headless)。

wasm_fs_sync() 负责把 emscripten FS 刷到后端存储:浏览器中把 MEMFS 刷入 IndexedDB(异步,fire-and-forget);headless 下 NODEFS 写操作本就是同步的,syncfs 是无害 no-op。它带合并逻辑:IDBFS syncfs 是异步的,重叠的 sync 会警告 "2 FS.syncfs operations in flight",因此若已有 sync 在途则标记 pending,由运行中的那个完成后链式补跑。源码注释还提醒了一个细节:EM_ASM内嵌 JS 不能用!==和/regex/字面量(clang-format 会把它拆成非法 token),只能用松散比较与双引号字符串。

十、重启语义:把重启交给宿主

wasm 内固件无法自我重启,因此 reboot()(由 admin/phone 命令、工厂重置或 60 秒卡 TX 看门狗触发)把重启交给宿主:

  • 浏览器:调用location.reload();由于 NodeDB 状态经 IDBFS 存活,节点以相同身份回归;
  • headless:若提供了Module.onReboot回调则调用之(重新实例化模块、process.exit()交由 supervisor 重启等);
  • 两者皆无:仅记日志并继续运行。
const Module = await createMeshNode({ noInitialRun: true }); Module.onReboot = () => process.exit(0); // 可选;headless 重启策略

十一、固件源码中的 wasm 守卫:六个文件的小改动

README 明确指出六个固件源码携带#ifdef ARCH_PORTDUINO_WASM守卫,逐个核实如下:

文件守卫内容
src/main.cppinclude<emscripten.h>;排除 raspi HTTP 服务器(L108-L110,ulfius/zlib/openssl 不适用于浏览器);主循环空闲睡眠改用emscripten_sleep且封顶 50ms(L1580-L1589)
src/mesh/SX126xInterface.cppstartReceive()改用连续接收(RADIOLIB_SX126X_RX_TIMEOUT_INF)而非 duty-cycle 自动接收——注释说明:浏览器里 duty-cycle 睡眠会让 BUSY 在 RX 窗口间停在高位,拖慢本就较慢的 WebUSB SPI 链路,且这里没有电池要省
src/mesh/InterfacesTemplates.cpp排除 TCP socket API 服务器
src/mesh/LR11x0Interface.cppgetCurrentRSSI()使用 RadioLib 的 0 参数版本(无 true/false 参数)
src/mesh/NodeDB.cpp(README 列出的守卫文件之一)配合wasm_resolve_mac的身份路径
src/mesh/HardwareRNG.cpp__EMSCRIPTEN__分支下跳过 getrandom/arc4random,回退std::random_device——emscripten 以crypto.getRandomValues()支撑之
src/platform/portduino/PortduinoGlue.cppportduinoSetup()中调用wasm_config_apply()并构造 WebUSB 版Ch341Hal后提前返回,跳过 YAML 配置路径;exec()短路为空字符串(浏览器无 popen/shell,L1465-L1470);loadConfig()空实现(L945-L953);readGPIOFromYaml排除

此外 portduino_glue_wasm.cpp 还补上了原本来自 frameworklinux/LinuxCommon.cpp(wasm 构建已排除)的框架符号:delay()/yield()通过 Asyncify 的emscripten_sleep让初始化期的阻塞让步给浏览器事件循环;random()/randomSeed();tone()/noTone()空实现(浏览器无蜂鸣器);delayMicroseconds()(<1ms 直接让步、≥1ms 向上取整睡眠);shouldWakeOnReceivedMessage()返回 false(无屏幕,永不唤醒);initApiServer()/deInitApiServer()空 stub 以满足main.cpp链接。

十二、适用前提与限制

  • WebUSB 仅 Chromium(且通常要求 https 或 localhost 环境);设备选择必须由用户手势触发;
  • 单线程协作式模型:所有 wasm 入口只能在wasm_loop_once()tick 之间调用,否则被g_wasm_in_firmware拒绝;RX/TX 完成依赖每 tick 的pollMissedIrqs()轮询,故 JS 泵送间隔决定了接收延迟下界(示例用 5ms);
  • 无屏幕、GPS、I2C、音频、MQTT、串口、Store&Forward等能力被编译期宏排除,这是一个专注 mesh 射频的 headless 节点;
  • Asyncify 有成本:每次 WebUSB 传输一个挂起,堆可增长,桥接层必须遵守 HEAPU8 重读约定;
  • 桌面/原生 portduino 构建不受任何影响(wasm 目录被排除 + 全量守卫)。

若要在浏览器标签页或 headless Node 中体验一个真实参与 mesh 网络的 Meshtastic 节点,[env:native-wasm]正是现成的构建目标:source <emsdk>/emsdk_env.sh && pio run -e native-wasm产出meshnode.{mjs,wasm}后,按本文第四节的最小宿主流程即可引导一个通过 WebUSB 驱动 CH341/SX1262 的浏览器内节点,并用@meshtastic/coreSDK 通过wasm_api_to_radio/wasm_api_from_radio完成配置与通信。

  • 物联网
  • 嵌入式
  • 通信
  • 智能硬件

【免费下载链接】firmware

The official firmware for Meshtastic, an open-source, off-grid mesh communication system.

项目地址:https://gitcode.com/GitHub_Trending/fi/firmware
点击查看免费下载
上一篇:GTA5线上小助手怎么用?一文看懂免费开源线上工具的五大功能
下一篇:ComfyUI-VideoHelperSuite 实战教程:10 分钟把 128 帧拼成可播放的 24fps MP4

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询