
简介这套 Mixly 自制库专为 ESP8266 开发设计聚焦物联网原型制作中最常用的几个环节EEPROM 断电保存与字符复制、WiFi 自动配网、数据类型转换以及基于 U8G2 的液晶屏驱动。压缩包共 68 个文件核心代码是 6 个 JavaScript 模块配以 60 张 PNG 图解另有 TXT 说明和 XML 配置整体 446KB轻量且容易迁移到不同项目。已有 5786 人学习使用尤其适合刚接触 ESP8266 的爱好者以及想在课堂上演示图形化编程的教师。借助说明文档和模块源码可以弄清如何在 Mixly 中写入/读取 EEPROM 数据、实现设备自动联网并掌握整数与字符串的相互转换U8G2 部分则展示了在 OLED/LCD 上绘制文本和简单图形的方法。自制库对模块边界的划分和组织方式也为读者扩展自己的图形化积木提供了清晰范本。1. 自制 Mixly 库从只会拖积木到能造积木的那道坎第一次从前辈手里拿到一个 mixly库 文件夹时我还在老老实实拖官方积木。把文件丢进米思齐的库目录重启模块区凭空多出一排 WiFi 配网、EEPROM 存储的积木那一刻就意识到能用积木的人和能造积木的人中间隔着的不过是一份 XML 描述文件加一堆代码模板。自制库这件事说白了就是把你已经写好的 Arduino 函数包装成 Blockly 认识的积木块让不懂代码的人也能用。这篇笔记会把库结构、配网库实战、数据类型转换和 EEPROM 的坑一次讲完。2. 自制库的内部结构XML、代码模板与参数映射的三层关系2.1 一个库文件夹的解剖先搞清每个文件是干嘛的市面上能下载到的 mixly 自制库包拆开看基本都是同一个骨架。先别急着写代码把文件认全。以 Mixly 1.x 生态为例一个完整库文件夹通常长这样文件/目录作用是否必须MyWifi.xml定义积木的外观、参数类型、返回形状必须_code.txt代码模板决定积木生成什么 Arduino 语句必须MyWifi.h/MyWifi.cpp函数实现真正干活的部分强烈建议zh-cn.json积木名的中英文映射部分版本识别可选images/积木分类的图标可选注意一个容易踩的坑Mixly 1.x 和 2.x 对库的定义方式不一样。1.x 用 Blockly 风格的 XML2.x 改成了 JSON 块描述。从网上下载的旧库放到 2.x 里经常不识别不是文件坏了是格式换代了。所以下载任何库之前先确认对方标的是哪个版本。我目前讲的是 1.x 的主流写法2.x 的迁移问题放到最后一章说。文件放哪也很关键。Mixly 启动时会扫描 Arduino 用户目录下的libraries文件夹找到里面带 XML 的子目录解析后生成积木分类。放错路径的表现不是报错而是模块区一片空白很多新手在这卡半天。正确做法是把整个库文件夹不是 XML 文件本身丢进libraries重启 Mixly。2.2 block 类型与代码模板占位符是如何把积木变成代码的自制库的核心机制就一句话积木上填的参数通过占位符拼进代码模板最终生成 Arduino 代码。先看一个最小的例子。假设我要做一个让 LED 闪一下的积木XML 里这样描述block typeled_blink_once value namePIN block typemath_number field nameNUM13/field /block /value /block对应的代码模板_code.txt里是pinMode(%1, OUTPUT); digitalWrite(%1, HIGH); delay(300); digitalWrite(%1, LOW);逻辑说明typeled_blink_once是积木的唯一标识必须有且不能和别的库重复否则会导致积木覆盖。value namePIN声明了一个参数插槽嵌套的math_number表示这个插槽只接受数字输入field nameNUM是积木上默认显示的值。MNemonic%1就是第一个参数槽位Mixly 生成代码时会把用户填的数字替换进去。如果积木有两个参数模板里用%1、%2依次对应顺序按 XML 里value的排列。参数说明%1替换的是用户填的字面值不是变量名。如果用户填13生成的就是pinMode(13, OUTPUT)如果用户在积木上改了数字重新生成代码时%1也会跟着变。这里有一个血泪经验%1替换后是裸值字符串类型积木会带引号数字不会。所以写模板时不要手抖加引号否则会出现pinMode(13, OUTPUT)变成pinMode(13, OUTPUT)这类编译报错或运行时诡异行为。2.3 参数类型声明数字、字符串、布尔与下拉的选用原则写模板前先想清楚每个参数允许填什么。Mixly 的 XML 通过嵌套 block 类型来约束参数类型常见对应关系如下积木控件XML 嵌套 block 类型生成到模板的内容适用场景数字输入框math_number裸数字如115200波特率、引脚号、延时文本输入框text带引号字符串如MyWiFiSSID、密码、服务器地址布尔开关logic_booleantrue或false功能开关、高低电平选择下拉选择自定义field或映射成数字选中项对应值模式枚举、通道选择这里重点说下拉选择。Mixly 1.x 不同小版本对下拉的实现写法差异很大有的用dropdown类型有的要求你自定义一个带options属性的 block 类型。我踩过一轮坑之后形成了一套保守方案不用花哨的下拉直接把枚举映射成math_number在.h文件里用switch解析。比如配网模式积木上是一个数字框填 0 表示 SmartConfig填 1 表示 SoftAP代码模板里把这个数字传给函数函数内部判断。虽然积木不够「傻瓜化」但换版本时最不容易翻车。2.4 加载机制为什么库放对了目录还是看不到积木Mixly 对自定义库的扫描是有缓存的。第一次启动时解析 XML之后如果不重启你改了 XML 它也不会重新读取。更隐蔽的是如果 XML 文件有语法错误比如标签未闭合、字段名拼错Mixly 会静默跳过整个库模块区不出现任何新积木。我的排查习惯是三步走先重启 Mixly再看 XML 有没有被编辑器标红最后把 XML 里非 ASCII 的中文标签临时改掉因为 1.x 某些版本对编码敏感。这三步解决了我 90% 的「库不显示」问题。参数类型的选用直接决定后面类型转换的坑多坑少所以这块值得花时间想清楚。能用math_number就绝不用text能限定枚举就绝不放任用户输入这是自制库参数设计的黄金法则。3. ESP8266 WiFi 自动配网库EEPROM 存储与三个积木块的完整实现3.1 为什么必须做自动配网从烧死 SSID 到存进 EEPROM开发 ESP8266 项目时最烦的就是每次换 WiFi 环境都要改代码重烧固件。固定 SSID 的写法适合演示不适合产品。自动配网的标准做法是设备上电先尝试连接上次保存的 WiFi连不上就进入配网模式收到手机下发的 WiFi 信息后存入 EEPROM下次开机直接连。这样就实现了「一次配网永久记忆」。方案优点缺点适用场景固定 SSID 烧死实现最简单换网络必须重烧实验室固定环境SmartConfig 一键配网用户操作快需要手机 App 支持阻塞等待有超时坑现场配网、批量部署SoftAP 配网不依赖 App浏览器可配开发量大交互复杂产品化设备我选择 SmartConfig EEPROM 组合原因很实际ESP8266 官方 SDK 直接支持beginSmartConfig()代码量最小配合 EEPROM 缓存后体验接近成品路由器。这次的库设计成三个积木块积木块类型生成代码作用WiFi 自动配网语句块wifi_auto_config(30000)读 EEPROM 尝试连接失败则进入配网保存 WiFi 信息语句块wifi_save_config(%1, %2)将 SSID 和密码写入 EEPROM读取已保存 SSID表达式块wifi_get_ssid()返回字符串方便 OLED 等显示3.2 底层实现EEPROM 读写和 SmartConfig 配网逻辑先写库的核心头文件MyWifi.h#ifndef MYWIFI_H #define MYWIFI_H #include ESP8266WiFi.h #include EEPROM.h #define WIFI_MAGIC 0xA5 struct WifiConfig { char ssid[32]; char pwd[32]; uint8_t magic; }; extern WifiConfig g_cfg; void wifi_load_config(); void wifi_save_config(const String ssid, const String pwd); bool wifi_auto_config(uint32_t timeout_ms); String wifi_get_ssid(); #endif逻辑说明结构体WifiConfig用定长char[]而不是String存 SSID 和密码这是 EEPROM 读写的基本素养——String内部是堆指针断电重启后指针指向的内存早就不在了只有char[]才能稳定序列化。magic字段是校验位写固定值0xA5读出来如果不是这个值就说明 EEPROM 里没有有效配置从而避免用垃圾数据去连 WiFi。所有函数声明为extern方便不同代码模板文件引用#ifndef防止多个积木重复 include 时重定义。接下来是MyWifi.cpp的实现#include MyWifi.h WifiConfig g_cfg; void wifi_load_config() { EEPROM.begin(sizeof(WifiConfig)); EEPROM.get(0, g_cfg); if (g_cfg.magic ! WIFI_MAGIC) { memset(g_cfg, 0, sizeof(g_cfg)); } } void wifi_save_config(const String ssid, const String pwd) { memset(g_cfg, 0, sizeof(g_cfg)); ssid.toCharArray(g_cfg.ssid, sizeof(g_cfg.ssid)); pwd.toCharArray(g_cfg.pwd, sizeof(g_cfg.pwd)); g_cfg.magic WIFI_MAGIC; EEPROM.begin(sizeof(WifiConfig)); EEPROM.put(0, g_cfg); EEPROM.commit(); } bool wifi_auto_config(uint32_t timeout_ms) { wifi_load_config(); if (g_cfg.magic WIFI_MAGIC strlen(g_cfg.ssid) 0) { WiFi.mode(WIFI_STA); WiFi.begin(g_cfg.ssid, g_cfg.pwd); uint32_t start millis(); while (WiFi.status() ! WL_CONNECTED) { if (millis() - start timeout_ms) return false; delay(100); } return true; } WiFi.mode(WIFI_STA); WiFi.beginSmartConfig(); uint32_t start millis(); while (!WiFi.smartConfigDone()) { if (millis() - start timeout_ms) return false; delay(100); } while (WiFi.status() ! WL_CONNECTED) { delay(100); } wifi_save_config(WiFi.SSID(), WiFi.psk()); return true; } String wifi_get_ssid() { wifi_load_config(); return String(g_cfg.ssid); }逻辑说明wifi_load_config()每次调用都执行EEPROM.begin(sizeof(WifiConfig))这是 ESP8266 的规矩。ESP8266 的 EEPROM 库是拿 Flash 模拟的不调用begin(n)直接get会得到全0xFF。beginSmartConfig()启动配网后smartConfigDone()返回 true 只代表手机 app 已经把 WiFi 消息发过来了此时WiFi.SSID()和WiFi.psk()才能取到有效值所以要再等WL_CONNECTED。参数说明timeout_ms默认传 30000 毫秒这是配网等待的上限。很多库卡死就是因为没这个超时后面避坑章节专门讲。wifi_save_config接收两个const String引用避免拷贝开销toCharArray时第三个参数传sizeof是复制上限防止超长密码把结构体后面的字段冲掉造成内存踩踏。3.3 XML 与代码模板把三个函数包装成积木块底层函数写好了剩下的工作是让这些函数从积木区长出来。新建MyWifi.xml:block typewifi_auto_config value nameTIMEOUT block typemath_number field nameNUM30000/field /block /value /block block typewifi_save_config value nameSSID block typetext/block /value value namePWD block typetext/block /value /block block typewifi_get_ssid value nameRETURN block typetext/block /value /block对应_code.txt代码模板wifi_auto_config(%1); wifi_save_config(%1, %2); wifi_get_ssid();逻辑说明注意第三个积木wifi_get_ssid的类型是表达式块它嵌套了一个text类型的 value表示这个积木块最后要返回一个字符串值可以塞进 OLED 显示或串口打印等其他积木的参数槽里。_code.txt里的三行分别对应三个积木块的生成代码wifi_get_ssid()没有参数所以生成的就是一条函数调用。头文件的 include 建议加在第一个积木的模板第一行#include MyWifi.h因为头文件带了#ifndef防重入多个积木重复 include 不会报错只是多一次预处理检查。参数说明XML 中block typewifi_save_config的type属性必须和_code.txt里生成的函数调用保持一致否则会出现积木拖出来了、编译却报undefined reference。value nameSSID的name属性是积木内部识别槽位用的改不改不影响生成代码但建议写成和参数含义相关的名字方便你三个月后回来维护时看懂。3.4 在 Mixly 里加载与验证从模块区到串口输出把MyWifi.xml、_code.txt、MyWifi.h、MyWifi.cpp放在同一个文件夹文件夹命名MyWifi整体丢进 Mixly 的libraries目录重启 Mixly。验证步骤模块区出现「MyWifi」分类拖出三个积木到编程区。烧录前先打开串口监视器波特率设 115200。上电后观察串口先打印[WiFi] try saved config...接着尝试连保存的 WiFi。如果之前没保存过配置设备会进入 SmartConfig 配网状态此时用支持 ESP-Touch 协议的手机 App 发送家庭 WiFi 信息。配网成功后串口打印[WiFi] connected, save config断电重启后应能看到设备直接连接 WiFi不再进入配网。这里有一个很多人忽略的验证点断电前要确认已经走到wifi_save_config。SmartConfig 收到信息到执行保存之间有个时间差如果在WiFi.status() ! WL_CONNECTED等待循环里就断电设备会拿不到完整配置下次开机又回到配网流程。这是硬件调试里典型的「看起来配网成功实际没存住」问题。4. 自制库避坑排查数据类型转换、EEPROM 与编译失败的四个现场4.1 字符串转数字看似能编译一跑就出错现象积木上填的波特率9600生成的代码是Serial.begin(9600)编译不报错串口输出全乱码。或者把文本参数转成int后判断出错比如空字符串转出来是0导致逻辑误判成「合法」。原因text 类型积木生成的是字符串字面量9600Arduino 的Serial.begin有long重载字符串字面量能隐式转换但转换结果是内存地址而不是数值。更隐蔽的是String(abc).toInt()在解析失败时返回0这会和真实数值0混淆没法区分「用户填了 0」和「用户填了非法字符」。解决模板里显式转换// 错误写法依赖隐式转换 wifi_send_command(%1); // 正确写法String 包一层再转明确意图 wifi_send_command(String(%1).toInt());逻辑说明String(%1)先构造临时 String 对象再调用toInt()把字符串解析成int。这样无论用户填的是不是纯数字语义都明确。参数说明如果这个参数本身就是数字框math_number不用担心这个问题只有 text 框才会产生字符串字面量。4.2 EEPROM 写入静默失败重启后配置全丢现象第一次配网成功断电重启后又进入配网模式或者串口打印读出来的 SSID 是\xFF\xFF...一串乱码。原因三个高频根因。第一没调EEPROM.begin(n)直接putESP8266 的模拟 EEPROM 不初始化就是全0xFF第二begin(n)的 n 小于实际写入的结构体大小比如结构体 65 字节却begin(64)最后一个字节写越界commit 时被丢弃第三写入后忘记commit()数据留在缓存没刷进 Flash。结构体里直接放String类型的同学字符串内容存在堆上序列化到 Flash 的只是指针的碎片断电后必丢。解决统一用下面的写法// 结构体定长、带 magic 校验 struct WifiConfig { char ssid[32]; char pwd[32]; uint8_t magic; }; // 写入前计算真实大小 EEPROM.begin(sizeof(WifiConfig)); EEPROM.put(0, cfg); EEPROM.commit();逻辑说明sizeof(WifiConfig)在这里是 65 字节begin必须传 65 或更大。参数说明ESP8266 的 EEPROM 库上限是 4096 字节Flash 扇区大小超过 4096 的写入会被截断别想着拿它当大容量存储用存几百字节的配置信息才是它的本分。依靠 magic 字段判断有效性的好处是第一次上电 Flash 是干净的0xFFmagic 不等于0xA5读出来就主动清零避免一堆乱码 SSID 去连 WiFi 造成长时间假死。4.3 编译报 undefined referenceblock 类型和函数名脱节现象积木拖出来了代码也生成了编译报undefined reference to wifi_auto_config(unsigned int)。最气人的是函数明明写在.cpp里了就是链接不上。原因函数声明和定义的签名不一致或者 block type 生成的调用函数名和库文件里的定义不是一个名字。我见过最离谱的一次是 XML 里 block type 写的wifi_auto_config代码模板写的也是wifi_auto_config但.h里声明的是bool wifi_auto_config(int timeout)传参类型不同C 符号修饰后就不是同一个函数了。解决先看生成代码再核对签名# 把 Mixly 生成的临时 sketch 目录打开搜函数名 grep -rn wifi_auto_config generated_sketch/逻辑说明这行命令把生成目录里所有引用wifi_auto_config的地方标出来。重点看声明、调用、定义的参数类型是否一致。参数说明const String和String是两个不同的函数签名C 的符号表会对参数类型做名称修饰一个字符不匹配都会导致链接失败。建议库内部统一用const String传参模板生成调用时就不用关心副本开销。4.4 SmartConfig 卡死拿不到 WiFi 信息时程序原地等待现象设备上电后不执行任何后续逻辑卡在配网等待或者配网成功后重启依然进配网模式。原因beginSmartConfig()之后库进入一个等待循环如果一直没收到手机下发信息这个循环就是死循环。另外有些版本的 ESP8266 在smartConfigDone()返回 true 后立刻读WiFi.psk()会读到空值如果不等待WL_CONNECTED就保存存入 EEPROM 的是空密码。解决所有等待循环都加超时并且保存前确认连接状态uint32_t start millis(); while (!WiFi.smartConfigDone()) { if (millis() - start timeout_ms) { Serial.println([WiFi] smartconfig timeout); return false; } delay(100); }逻辑说明timeout_ms由积木参数控制默认 30000 毫秒用户自己可以在积木上改。超时后返回false程序就有机会走其他分支比如进入 SoftAP 模式或者重启重试而不是死等。参数说明delay(100)不能省没有这个延时millis()读到的值变化太快循环空转消耗 CPU 且可能触发看门狗复位。配网完成后的while (WiFi.status() ! WL_CONNECTED)同样要带超时把这一层思考基本就避开大多数配网卡死现场了。5. 让自制库更耐用的三个进阶习惯5.1 给库加调试开关把串口输出留在现场调试阶段没有串口打印出了问题只能盲猜。但正式使用时满天飞的Serial.printf又影响性能。我的习惯是在头文件里加一个编译开关#define WIFI_DEBUG 1 #if WIFI_DEBUG #define DBG(...) Serial.printf(__VA_ARGS__) #else #define DBG(...) #endif逻辑说明WIFI_DEBUG设 1 时 DBG 宏映射到Serial.printf设 0 时映射为空操作连参数求值都被优化掉。发布库之前把开关改成 0 即可不用删代码。参数说明所有调试输出统一走DBG不要在代码里裸写Serial.printf否则后期清理就是一场恶梦。从那以后我每次写库都会先铺好这层宏省掉无数「这里怎么还有个 print」的尴尬。5.2 留一个清空配置的隐藏积木EEPROM 存了错误配置后调试时想回到配网流程总不能每次拔 Flash 或者改代码。我在库里额外加了一个wifi_clear_config()函数专门把 magic 字段改掉void wifi_clear_config() { g_cfg.magic 0; EEPROM.begin(sizeof(WifiConfig)); EEPROM.put(0, g_cfg); EEPROM.commit(); }逻辑说明把 magic 写成0下次wifi_load_config()读出来校验不通过自动清空结构体设备重启后就会重新进入配网。这个函数不在正式积木区展示测试时临时拖出来用也可以绑一个物理按键「长按 5 秒恢复出厂」比重新烧固件快一个量级。5.3 兼容 Mixly 2.x底层逻辑复用积木描述重写迁移到 2.x 时MyWifi.h和MyWifi.cpp这两个底层文件可以原封不动复用真正要重写的是 XML 块描述——2.x 改成了 JSON 格式描述积木的槽位、颜色、类型。一次迁移的经验是把 XML 里的每个block type对照着翻译成 JSON 块定义参数类型、默认值和代码模板的对应关系不变两个小时就能完成。不要试图用兼容层去骗 2.x 读 1.x 的 XML,这条路我没走通直接重写反而快。写库这几年最大的教训来自一次室外验收设备配网成功、重启后总掉线折腾一下午最后发现是EEPROM.begin长度比结构体小 1 字节magic 校验位被丢在 Flash 扇区外。从那以后我每次写完库都强制走一遍完整验证流程清空 EEPROM、冷启动进配网、配网成功后断电、再上电看是否自动连接。这四步走完库才算真正能交付。希望帮到你。本文还有配套的精品资源点击获取