news 2026/9/11 9:39:22

MicroPython嵌入式日志模块uLogLite设计与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MicroPython嵌入式日志模块uLogLite设计与实战

1. 为什么在 MicroPython 项目里,日志不能只是 print?

我第一次在 ESP32 上跑一个温湿度+WiFi+OTA 的复合项目时,满屏的print("connecting...")print("temp: 23.4")print("ota done")看起来很“有反馈”,直到某天设备在野外连续运行72小时后突然卡死——串口日志早已被刷屏覆盖,最后几条有效信息根本没留下。重启后一切正常,问题消失得无影无踪。那会儿我才真正意识到:MicroPython 不是开发板玩具,而是嵌入式生产环境里的真实节点;而 print,不是日志,只是调试时的临时烟雾弹。

uLogLite 就是在这种血泪教训里长出来的轻量级日志模块——它不依赖任何第三方库,纯 Python 实现,代码不到 300 行,却完整支持日志级别控制(DEBUG/INFO/WARNING/ERROR/CRITICAL)、按大小轮转(size-based rotation)、关键词过滤(filter by tag or keyword),最关键的是:它把日志写入文件时做了内存友好型缓冲设计,避免频繁 flash 写入导致寿命衰减,也规避了 MicroPython 中os.sync()不稳定带来的丢日志风险。

你不需要用它来替代 Linux 下的 rsyslog 或 Python 的 logging 模块——那太重了。uLogLite 是专为资源受限场景打磨的:它能在 2MB Flash 的 ESP32-S2 上稳定运行,在 16KB RAM 的 RP2040 上只占不到 3KB 运行内存,甚至能在没有文件系统(仅 SPIFFS 或 littlefs)的裸机固件中通过 UART + 外置 SD 卡桥接实现持久化。热搜词里反复出现的 “micropython 支持 usb host 的 micropython 固件”,其实正指向一个新趋势:越来越多开发者开始把 MicroPython 当作边缘计算终端 OS 使用,而 USB Host 意味着能直连 U 盘、打印机、摄像头——这时候,一个可配置、可轮转、可过滤的日志模块,就不再是“锦上添花”,而是“故障归因的生命线”。

如果你正在做以下任一类型项目,uLogLite 就不是“可选”,而是“刚需”:

  • 带 OTA 远程升级的工业传感器节点(需记录每次升级前后的状态与错误)
  • 多任务协程调度的电机控制器(需区分 motor_task、comms_task、sensor_task 的日志流)
  • 长期离线部署的农业监测设备(SD 卡日志轮转 + 关键告警高亮过滤)
  • 教学实验平台上的多学生共享固件(用 tag 过滤快速定位某组实验数据)

它不解决所有问题,但把最痛的三个点——“不知道哪条日志该信”、“关键错误被刷走”、“日志把 flash 写坏”——用最朴素的方式堵死了。下面我们就从零开始,一行行拆解它是怎么做到的。

2. uLogLite 的核心设计逻辑:为什么不用标准 logging?为什么轮转必须自己写?

2.1 MicroPython 的 logging 模块,为什么在实际项目中几乎没人用?

MicroPython 官方确实带了一个logging模块,但它在 v1.19 之后才逐步完善,且存在几个硬伤:

  • 不支持 handler 轮转:标准RotatingFileHandler在 MicroPython 中缺失。官方只提供了StreamHandler(输出到 UART)和极简的FileHandler(直接写文件,无缓冲、无锁、无轮转)。一旦日志文件写满,要么报错,要么覆盖,要么撑爆 flash。
  • 级别控制粒度粗logger.setLevel()只能设全局最低级别,无法对不同模块(如 network、sensor、ui)设置独立级别。你在调试 WiFi 模块时打开 DEBUG,结果 sensor 模块每秒 10 条 DEBUG 日志把 SD 卡写满——这在真实项目里是灾难。
  • 无 tag / context 支持:无法给每条日志打上来源标识(如[wifi] connecting to AP...),导致多模块混写日志时完全无法溯源。
  • 内存开销不可控:标准模块内部使用字符串格式化 + list 缓存,在 RAM 仅 256KB 的 ESP32-WROOM-32 上,一次logger.error("val=%s, code=%d", val, code)可能触发 GC 频繁抖动,甚至 OOM。

提示:你可以用import logging; print(dir(logging))在 REPL 里验证——多数 MicroPython 固件(尤其是非官方 build)压根没编译RotatingFileHandlerFilter类。这不是你配置错了,是固件本身就不带。

所以 uLogLite 的第一设计原则就是:放弃兼容,专注可用。它不试图模拟 CPython 的 logging API,而是用 MicroPython 最擅长的方式做事:用字典管理配置、用生成器做缓冲、用字符串切片代替正则(省 RAM)、用os.stat()替代os.path.getsize()(更可靠)。

2.2 日志轮转为什么必须“自己写”?MicroPython 的文件系统有多脆弱?

轮转(rotation)不是简单地“文件满了就 rename”。在 MicroPython 环境下,它要同时扛住三重压力:

  1. Flash 寿命限制:ESP32 的 Flash 擦写寿命约 10 万次。如果每写 1KB 就open(..., "a") → write() → close()一次,一天写 10MB 日志 = 1 万次 open/close = 10 天擦穿一块扇区。uLogLite 采用双缓冲写入策略:内存缓冲区达 512 字节或 2 秒空闲时才 flush 到文件,大幅降低 I/O 频次。

  2. 文件系统原子性缺失:MicroPython 的os.rename()在 littlefs 上不是原子操作。若轮转时断电,可能出现main.logmain.log.1同时损坏。uLogLite 的轮转流程是:

    • 步骤1:os.stat("main.log")获取当前大小
    • 步骤2:若 > 1MB,执行os.remove("main.log.2")(先删最老备份)
    • 步骤3:os.rename("main.log.1", "main.log.2")
    • 步骤4:os.rename("main.log", "main.log.1")
    • 步骤5:open("main.log", "w")创建新文件
      这个顺序确保即使断电,最多丢失最后 2 秒日志,但绝不会出现两个文件都不可读。
  3. SD 卡热插拔风险:当使用 USB Host 接 U 盘时,用户可能随时拔出设备。uLogLite 在每次写入前检测os.listdir("/usb")是否存在,不存在则自动降级为 UART 输出,并记录"[FATAL] usb storage unavailable, fallback to uart"——这是标准 logging 模块完全做不到的上下文感知。

2.3 过滤机制为什么用“白名单 tag”而不是“黑名单关键词”?

很多初学者想用if "error" not in msg:做过滤,这在 MicroPython 里是典型反模式:

  • 字符串in操作在 MicroPython 中比==慢 3~5 倍(底层用 memcmp 而非哈希)
  • "error"可能出现在正常日志里(如"error_code=0"),误杀率高
  • 无法区分模块层级([network] errorvs[sensor] error

uLogLite 采用两级 tag 过滤

  • 第一级:Logger 实例绑定固定 tag,如log_net = uLogLite("network", level=LOG_WARN)
  • 第二级:全局 filter 函数,接收(tag, level, msg)元组,返回True才写入

这样,你可以轻松实现:

# 只记录 network 模块的 ERROR 及以上,其他模块 INFO 及以上 def my_filter(tag, level, msg): if tag == "network": return level >= LOG_ERROR else: return level >= LOG_INFO uLogLite.set_filter(my_filter)

实测下来,这个函数调用开销 < 12μs(ESP32 @ 240MHz),比字符串匹配快一个数量级,且逻辑清晰、无歧义。

3. 从零手写 uLogLite:核心代码逐行解析与实操配置

3.1 模块结构与最小可行版本(32 行精简版)

我们先抛开轮转和过滤,写出一个能工作的最小内核——它证明 uLogLite 的骨架有多简洁:

# uloglite_min.py import time, os LOG_DEBUG = 10 LOG_INFO = 20 LOG_WARN = 30 LOG_ERROR = 40 LOG_CRIT = 50 class uLogLite: def __init__(self, tag, level=LOG_INFO, file="/log.txt"): self.tag = tag self.level = level self.file = file self._buffer = bytearray() self._buf_size = 0 def _write(self, msg): # 简单追加,不轮转 try: with open(self.file, "a") as f: f.write(msg + "\n") except OSError: # 降级到 UART print("[ULOG] fallback:", msg) def log(self, level, msg): if level < self.level: return t = time.localtime() ts = "{:02}:{:02}:{:02}".format(t[3], t[4], t[5]) full_msg = "[{}] [{}] {}".format(ts, self.tag, msg) self._write(full_msg) # 使用示例 log = uLogLite("main", level=LOG_INFO) log.log(LOG_INFO, "system started") log.log(LOG_WARN, "wifi signal weak")

这段代码只有 32 行,但它已具备 uLogLite 的灵魂:tag 绑定、级别控制、时间戳、fallback 机制。现在我们在此基础上,一层层加上轮转、过滤、缓冲等工业级能力。

3.2 加入智能轮转:如何安全地管理多个日志文件?

轮转的核心是rotate_if_needed()方法。它必须回答三个问题:

  • 当前日志文件多大?
  • 是否达到轮转阈值?
  • 如何重命名而不丢数据?

以下是 uLogLite 中经过 17 次现场测试(含断电模拟)验证的轮转实现:

def rotate_if_needed(self): # 1. 获取当前文件大小(兼容 SPIFFS/littlefs/SD) try: st = os.stat(self.file) size = st[6] # st_size 字段索引,比 os.path.getsize() 更底层、更快 except OSError: size = 0 # 2. 检查是否超限(默认 1MB) if size < self.max_size: return False # 3. 执行轮转:先删最老备份,再依次 rename # 保证:log.txt -> log.txt.1 -> log.txt.2 -> ... -> log.txt.N(N=3) for i in range(self.backup_count, 0, -1): old_name = "{}.{}".format(self.file, i) new_name = "{}.{}".format(self.file, i + 1) try: if i == self.backup_count: os.remove(new_name) # 删除最老的 .N+1 os.rename(old_name, new_name) except OSError: pass # 文件不存在,跳过 # 4. 将当前日志重命名为 .1 try: os.rename(self.file, "{}.1".format(self.file)) except OSError: pass # rename 失败,继续写原文件(不中断业务) return True

关键细节说明:

  • st[6]直接取stat返回元组的第 7 个元素(st_size),比os.path.getsize()少一次路径解析开销,实测快 40%;
  • 轮转时从高序号向低序号 rename(.3→.4,.2→.3,.1→.2),避免中间态冲突(如先.1→.2.2→.3会导致.2被覆盖);
  • backup_count=3是经验值:3 个备份 ≈ 3MB 存储,平衡空间与追溯深度;超过 3 天的日志通常已无分析价值;
  • 所有os.*操作都包裹try/except OSError,因为 MicroPython 的文件系统异常极其常见(SD 卡接触不良、flash wear leveling 失败等)。

注意:不要在rotate_if_needed()中调用gc.collect()!我在某次 OTA 后发现设备重启率飙升 12%,最终定位到是轮转时强制 GC 导致内存碎片加剧。uLogLite 的设计哲学是——日志模块绝不主动触发 GC,把控制权交给主程序。

3.3 实现高效过滤:基于 tag 的轻量级白名单引擎

过滤不是附加功能,而是日志管道的“第一道闸门”。uLogLite 的set_filter()接口设计成函数式风格,便于组合:

# 全局过滤器(可被所有 logger 实例共享) _filter_func = None def set_filter(func): global _filter_func _filter_func = func def _should_log(self, level, msg): if _filter_func is None: return True return _filter_func(self.tag, level, msg) # 在 log() 方法中调用 def log(self, level, msg): if level < self.level: return if not self._should_log(level, msg): return # ... 后续格式化与写入

这个设计带来两个实战优势:

  • 动态开关:调试时set_filter(lambda t,l,m: t=="motor"),上线后set_filter(None)全开;
  • 分层过滤:你可以链式组合,比如先用tag_filter,再用level_filter,最后用keyword_filter,全部在应用层自由组装,不侵入 uLogLite 内部。

我在线上设备中常用的一个生产级过滤器:

# 只记录 ERROR 及以上,且排除特定调试信息 def prod_filter(tag, level, msg): if level < LOG_ERROR: return False # 过滤掉已知的非关键错误(如蓝牙配对失败重试) if "bt pairing failed" in msg and "retry" in msg: return False # 保留所有硬件错误 if "hardware" in msg.lower() or "i2c" in msg.lower(): return True return tag in ["sensor", "comms", "ota"] # 白名单模块 uLogLite.set_filter(prod_filter)

实测表明,这种过滤使日志体积减少 68%,但关键故障捕获率保持 100%——因为真正的硬件异常从来不会出现在retry日志里。

3.4 缓冲与同步:如何让日志既快又不丢?

MicroPython 的file.write()是阻塞的,但file.flush()在某些文件系统上可能卡住(尤其 SD 卡写满时)。uLogLite 采用混合缓冲策略

缓冲类型触发条件作用
行缓冲遇到\n确保每条日志原子写入
大小缓冲缓冲区 ≥ 512 字节减少 flash 擦写次数
时间缓冲最后写入后 ≥ 2 秒防止设备意外断电丢日志

核心缓冲逻辑:

def _flush_buffer(self): if not self._buffer: return try: with open(self.file, "a") as f: f.write(self._buffer.decode()) self._buffer = bytearray() except OSError as e: # 记录错误但不抛出,避免阻塞主逻辑 print("[ULOG ERR] flush fail:", e) def _add_to_buffer(self, msg): self._buffer += msg.encode() + b"\n" self._buf_size += len(msg) + 1 if self._buf_size >= 512: self._flush_buffer() # 在 log() 中调用 def log(self, level, msg): # ... 级别/过滤检查 t = time.time() ts = "{:02}:{:02}:{:02}".format(*time.localtime(t)[3:6]) full_msg = "[{}] [{}] {}".format(ts, self.tag, msg) self._add_to_buffer(full_msg) # 启动定时 flush(用 utime.ticks_ms 实现非阻塞计时) if not hasattr(self, '_last_flush') or time.ticks_ms() - self._last_flush > 2000: self._flush_buffer() self._last_flush = time.ticks_ms()

这里的关键是time.ticks_ms()——它比time.time()更精准(毫秒级)、更轻量(不涉及 RTC 校准),且在 deep sleep 唤醒后依然连续。我曾用逻辑分析仪抓过波形:启用缓冲后,日志写入耗时从平均 18ms 降到 0.3ms(纯内存操作),而flush()每 2 秒集中执行一次,对实时性零影响。

4. 实战部署全流程:从烧录固件到现场排障

4.1 固件选择与 uLogLite 集成步骤(以 ESP32 为例)

uLogLite 对固件唯一要求是:必须启用文件系统支持。不同固件的启用方式差异很大,这是新手最容易卡住的环节。

官方固件(micropython.org 下载)
  • 默认启用littlefs,但未编译uos.dupterm(),导致无法重定向print到日志
  • 解决方案:在boot.py中添加
    import uos uos.dupterm(None, 1) # 关闭 REPL 输出到 UART
  • 然后在main.py中初始化 uLogLite:
    from uloglite import uLogLite log = uLogLite("system", level=LOG_INFO, file="/log/system.log")
支持 USB Host 的定制固件(如 esp32-usb-host-firmware)

这是热搜词 “支持 usb host 的 micropython 固件” 的真实落地场景。这类固件通常:

  • 启用vfs模块,可挂载 U 盘为/usb
  • 但默认不格式化 U 盘,首次使用需手动os.mkfs("/usb")
  • uLogLite 配置需改为:
    # 检测 USB 是否就绪 try: os.listdir("/usb") log_file = "/usb/log/main.log" except OSError: log_file = "/log/main.log" # fallback to internal flash log = uLogLite("main", level=LOG_INFO, file=log_file)

实操心得:USB Host 固件的os.listdir("/usb")在 U 盘刚插入时可能返回空列表(枚举未完成)。我的做法是加一个 3 秒重试循环,而非直接 fallback——因为 U 盘日志对现场调试至关重要。

自定义编译固件(推荐用于量产)

如果你用make BOARD=ESP32_GENERIC编译,务必在mpconfigboard.h中确认:

#define MICROPY_VFS_LITTLEFS (1) // 必须开启 #define MICROPY_PY_OS_DUPTERM (1) // 必须开启,否则无法重定向

然后在frozen目录放入uloglite.py,编译后uLogLite就成了内置模块,无需import

4.2 日志文件系统规划:SPIFFS vs littlefs vs SD 卡

存储介质适用场景uLogLite 配置要点寿命风险
内置 SPIFFS临时调试、无外部存储设备file="/spiffs/log.txt"max_size=256*1024高(SPIFFS 无磨损均衡)
内置 littlefs主力存储、需长期运行file="/littlefs/log.txt"max_size=1*1024*1024低(wear leveling 已启用)
SD 卡(SPI)大容量日志、现场导出file="/sd/log/main.log"backup_count=10中(SD 卡质量参差)
USB U 盘边缘计算、热插拔需求file="/usb/log/{device}.log",需动态检测设备名低(U 盘自带控制器)

关键经验:

  • 永远不要用 SPIFFS 做轮转日志:它的擦写粒度是 4KB,而 uLogLite 的轮转是按文件大小,极易造成 block 碎片化,3 天后os.listdir()就开始报错;
  • littlefs 的max_size建议设为 1MB:这是其内部 block 大小(4KB)的整数倍,避免跨 block 写入导致性能下降;
  • SD 卡日志必须加 CRC 校验:我在uloglite.py末尾加了一行# CRC32: 0x1a2b3c4d,每次启动时校验,损坏则自动清空日志目录——这招救了我 3 次现场故障。

4.3 现场排障:从日志里挖出真凶的 5 个技巧

日志写出来不是目的,读懂它才是价值。以下是我在 12 个工业项目中总结的排障心法:

技巧1:用时间戳差定位卡顿点

MicroPython 没有perf_counter,但time.ticks_ms()是你的朋友:

start = time.ticks_ms() do_something_heavy() log.log(LOG_INFO, "heavy task took {}ms".format(time.ticks_ms() - start))

当看到heavy task took 8500ms,你就知道是 WiFi scan 耗时过长,而非 CPU 占用率高。

技巧2:用 tag 分层隔离干扰

多任务环境下,log_netlog_sensorlog_ota必须分离:

log_net = uLogLite("network", level=LOG_DEBUG) log_sensor = uLogLite("sensor", level=LOG_INFO) # 错误:log_net.log(LOG_DEBUG, "connected") → 会淹没 sensor 日志 # 正确:log_net.log(LOG_INFO, "connected") + log_sensor.log(LOG_DEBUG, "temp=23.4")

上线后只需set_filter(lambda t,l,m: t=="network"),瞬间聚焦网络问题。

技巧3:ERROR 日志必须带上下文堆栈

MicroPython 的sys.print_exception()输出不全,uLogLite 内置精简版:

def log_exception(self, exc): import sys, io buf = io.StringIO() sys.print_exception(exc, buf) self.log(LOG_ERROR, "EXCEPTION:\n" + buf.getvalue().strip())

它能打印出File "main.py", line 42, in on_wifi_connect,比TypeError: can't convert str to int有用 100 倍。

技巧4:用关键词过滤快速筛查

现场工程师没时间翻 10MB 日志。教他们用grep

# 找所有硬件错误 grep "i2c\|spi\|adc" /mnt/usb/log/main.log.1 # 找 OTA 失败的完整上下文(前后 3 行) grep -A3 -B3 "ota fail" /mnt/usb/log/main.log

这就是为什么 uLogLite 的日志格式是[HH:MM:SS] [tag] msg——grep能精准锚定。

技巧5:建立日志健康度看板

在 WebREPL 或串口命令行里加一个log_status()

def log_status(): try: st = os.stat("/log/main.log") size_mb = st[6] / 1024 / 1024 backups = len([f for f in os.listdir("/log") if f.endswith(".log.")]) print("Log status: {:.2f}MB, {} backups, last write: {}".format( size_mb, backups, time.strftime("%H:%M", time.localtime()))) except OSError: print("Log storage unavailable")

运维人员输入log_status()就能立刻判断日志是否健康,比翻文件快 10 倍。

5. 常见问题速查表与避坑指南

以下是我踩过的坑、客户问最多的问题、以及论坛高频报错的终极解决方案。每个问题都附带复现步骤、根本原因和一行修复代码。

问题现象复现步骤根本原因修复方案验证方法
日志文件写到一半就变空连续log.log(LOG_INFO, "test"+str(i))1000 次littlefs 在满块时write()返回 0,uLogLite 未检查返回值_write()中添加if f.write(data) != len(data): raise OSError("short write")模拟满块:os.remove("/log/*"); for i in range(100): log.log(...)
轮转后日志丢失最后 5 秒设备运行中拔掉 SD 卡再插回os.rename()在 SD 卡重插后未重新挂载,/sd路径失效rotate_if_needed()开头加try: os.listdir("/sd")检测挂载状态拔卡后执行os.listdir("/sd")应报OSError: [Errno 19] ENODEV
DEBUG 日志全没了,但 INFO 正常log = uLogLite("test", level=LOG_DEBUG)MicroPython 的time.localtime()在 RTC 未初始化时返回(2000,1,1,...),格式化失败log()中用time.time()替代time.localtime()获取时间戳print(time.localtime())若返回(2000,1,1,0,0,0,...)即 RTC 未校准
U 盘日志写入极慢(>500ms/条)log = uLogLite("usb", file="/usb/log.txt")U 盘未启用 write cache,每次write()都等待物理写入在挂载 U 盘后执行uos.mount(uos.VfsFat(sd), "/usb", readonly=False, cache=True)timeit.timeit(lambda: log.log(...), number=10)对比开启 cache 前后
filter 函数导致设备重启set_filter(lambda t,l,m: m.index("error"))str.index()在子串不存在时抛ValueError,未被捕获所有过滤函数必须try/except包裹,或改用m.find("error") != -1在 REPL 中执行lambda: "abc".index("d")确认会 crash

注意:MicroPython 的ValueError在某些固件版本中会直接触发 hard fault,而非 Python 异常。所以所有用户自定义函数(filter、formatter)必须用 try/except 包裹,这是铁律。

另一个血泪教训:永远不要在 filter 函数里做耗时操作。有客户在 filter 里调用urequests.get()查远程黑名单,结果日志模块阻塞了整个系统。正确做法是——filter 只做内存内判断,复杂逻辑放主循环里预处理。

最后分享一个我私藏的调试技巧:在boot.py里加一行

import machine; machine.freq(240000000) # 强制超频,让日志写入更快

这招在 ESP32 上能把log.log()耗时从 12ms 降到 4ms,代价是功耗增加 8%,但对于调试阶段值得。

6. 进阶扩展:让 uLogLite 成为你项目的日志中枢

uLogLite 的设计留出了三个扩展接口,它们不是“未来计划”,而是我已经在 3 个项目中落地的增强能力。

6.1 通过 UART 实现实时日志转发(免接线调试)

当设备部署在金属柜内,USB 线无法接入时,UART 转发就是生命线。只需 5 行代码:

# 在 uloglite.py 中添加 _uart_forward = None def forward_to_uart(uart_obj): global _uart_forward _uart_forward = uart_obj def _write(self, msg): # ... 原有写入逻辑 if _uart_forward: try: _uart_forward.write(msg.encode() + b"\r\n") except OSError: pass

然后在main.py中:

from machine import UART uart2 = UART(2, tx=17, rx=16, baudrate=115200) uLogLite.forward_to_uart(uart2) # 所有日志实时发到 UART2

手机用 USB-TTL 模块一接,就能看到和电脑串口完全一致的日志流。比 Wi-Fi 日志推送更可靠——毕竟 Wi-Fi 可能连不上,但 UART 总是通的。

6.2 与 OTA 升级联动:自动归档旧日志

OTA 升级前,把当前日志打包为log_20240520_1423.zip并上传到服务器,这是故障回溯的关键证据。uLogLite 提供archive_current()方法:

def archive_current(self, archive_path="/usb/archive/"): import uzlib, uio # 读取当前日志 with open(self.file, "rb") as f: data = f.read() # 压缩 compressed = uzlib.compress(data) # 写入归档文件 archive_name = "{}log_{}.zip".format( archive_path, time.strftime("%Y%m%d_%H%M", time.localtime()) ) with open(archive_name, "wb") as f: f.write(compressed) # 清空当前日志 with open(self.file, "w") as f: f.write("")

调用时机放在 OTAon_start()回调里,确保每次升级都有完整前序日志。

6.3 WebREPL 日志实时查看(零额外依赖)

MicroPython 的 WebREPL 默认不暴露日志文件,但我们可以通过uLogLiteget_last_lines(n=20)实现:

def get_last_lines(self, n=20): try: with open(self.file, "r") as f: lines = f.readlines() return lines[-n:] if len(lines) > n else lines except OSError: return ["[ERROR] log file unreadable"]

然后在 WebREPL 的 HTML 页面里加一个按钮,点击执行:

// WebREPL 前端 JS fetch("/log?lines=20").then(r => r.text()).then(console.log);

后端用uLogLite.get_last_lines()响应——这样客户在现场用手机扫二维码,就能看到最新日志,再也不用带电脑。

这些扩展都不是“炫技”,而是从真实交付场景里长出来的。uLogLite 的价值不在于它多复杂,而在于它用最简的代码,解决了最痛的现场问题。当你在凌晨三点收到客户电话说“设备又死了”,而你打开日志一眼看到[14:22:05] [sensor] i2c timeout on addr 0x40,那一刻你会感谢自己当初没偷懒,认真写了这个日志模块。

我在最后一台设备上部署 uLogLite 时,把它刻在了 PCB 的丝印上:“LOG: ENABLED”。不是为了炫耀,而是提醒自己:在嵌入式世界里,能被看见的错误,永远比静默崩溃更值得信赖

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 9:39:19

Flutter在OpenHarmony上实现高性能表格组件:方案选型与踩坑实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 9:36:18

给Agent会话安个家:用目录归档根治AI工作台的云盘焦虑

把 Agent 会话从 WorkBuddy 工作台里翻出来确认状态&#xff0c;是我最焦虑的日常&#xff1a;几十个云端会话堆在一起&#xff0c;临时目录里躺着跑了一半的脚本&#xff0c;云盘文件夹越同步越乱。我真正崩溃的那次&#xff0c;是连续三周的“AI 重度使用”之后&#xff0c;想…

作者头像 李华
网站建设 2026/9/11 9:35:38

STM32裸机物联网药盒:语音唤醒+WiFi透传+MQTT状态同步

简介&#xff1a;本资源是一套完整的基于STM32的物联网智能药盒毕业设计源码&#xff0c;面向嵌入式开发初学者、电子信息类专业本科生及物联网项目实践者&#xff0c;聚焦医疗健康场景下的智能用药管理问题。方案融合WiFi远程通信与语音交互功能&#xff0c;支持药物提醒、状态…

作者头像 李华
网站建设 2026/9/11 9:34:26

多场景适用:2026高精三维地图定位系统推荐

摘要 2026年&#xff0c;随着空间数字化升级加速&#xff0c;公共建筑、工业园区及交通枢纽对室内外一体化导航需求激增。针对采购方在选型中易忽视技术适配与运维兼容的痛点&#xff0c;本文梳理了高精三维地图定位系统的核心筛选维度&#xff0c;并重点介绍北京大希科技有限公…

作者头像 李华
网站建设 2026/9/11 9:34:18

12G显存跑16B MoE模型:分层调度实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华