- 物联网
- 嵌入式
- 通信
- 智能硬件
【免费下载链接】firmware
The official firmware for Meshtastic, an open-source, off-grid mesh communication system.
本指南围绕 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.cpp | LoRa 配置(MeshToad 默认值 +wasm_set_lora_*setter,无 YAML)、VFS 挂载、region/MAC 辅助函数,以及wasm_api_*PhoneAPI 桥接 |
| portduino_main_wasm.cpp | wasm_setup()/wasm_loop_once()—— 由 JS 驱动协作式循环 |
| libpinedio_webusb.c | WebUSB 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_sx1262lora_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() 按优先级解析:
MESH_MAC环境变量(headless 确定性 / 对等测试,如DEAD00C0FFEE;浏览器中忽略process.env);/meshdata/oem/mac中持久化的值(跨刷新/重启存活);- 新生成的本地管理随机 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.cpp | include<emscripten.h>;排除 raspi HTTP 服务器(L108-L110,ulfius/zlib/openssl 不适用于浏览器);主循环空闲睡眠改用emscripten_sleep且封顶 50ms(L1580-L1589) |
| src/mesh/SX126xInterface.cpp | startReceive()改用连续接收(RADIOLIB_SX126X_RX_TIMEOUT_INF)而非 duty-cycle 自动接收——注释说明:浏览器里 duty-cycle 睡眠会让 BUSY 在 RX 窗口间停在高位,拖慢本就较慢的 WebUSB SPI 链路,且这里没有电池要省 |
| src/mesh/InterfacesTemplates.cpp | 排除 TCP socket API 服务器 |
| src/mesh/LR11x0Interface.cpp | getCurrentRSSI()使用 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.cpp | portduinoSetup()中调用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.
相关推荐
wa-sqlite:在浏览器中运行SQLite的WebAssembly构建
wa sqlite:在浏览器中运行SQLite的WebAssembly构建 项目介绍 wa sqlite 是一个基于WebAssembly的SQLite构建版本
如何让老旧Mac焕发新生:OCLP-Mod终极升级指南
如何让老旧Mac焕发新生:OCLP Mod终极升级指南 你的Mac设备是否被苹果官方抛弃,无法升级到最新的macOS系统?是否因为硬件限制而无法享受新系统的流畅
桌面应用CLI系统编程SSH-Chat消息环形缓冲区:高性能实时通信的内存优化架构
SSH Chat消息环形缓冲区:高性能实时通信的内存优化架构 SSH Chat作为基于SSH协议的实时聊天系统,其核心创新在于采用 环形缓冲区内存管理机制 实现
即时通讯后端网络
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考