简介:这套 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 风格的 XML,2.x 改成了 JSON 块描述。从网上下载的旧库放到 2.x 里经常不识别,不是文件坏了,是格式换代了。所以下载任何库之前,先确认对方标的是哪个版本。我目前讲的是 1.x 的主流写法,2.x 的迁移问题放到最后一章说。
文件放哪也很关键。Mixly 启动时会扫描 Arduino 用户目录下的libraries文件夹,找到里面带 XML 的子目录,解析后生成积木分类。放错路径的表现不是报错,而是模块区一片空白,很多新手在这卡半天。正确做法是把整个库文件夹(不是 XML 文件本身)丢进libraries,重启 Mixly。
2.2 block 类型与代码模板:占位符是如何把积木变成代码的
自制库的核心机制就一句话:积木上填的参数,通过占位符拼进代码模板,最终生成 Arduino 代码。先看一个最小的例子。假设我要做一个让 LED 闪一下的积木,XML 里这样描述:
<block type="led_blink_once"> <value name="PIN"> <block type="math_number"> <field name="NUM">13</field> </block> </value> </block>对应的代码模板_code.txt里是:
pinMode(%1, OUTPUT); digitalWrite(%1, HIGH); delay(300); digitalWrite(%1, LOW);逻辑说明:type="led_blink_once"是积木的唯一标识,必须有且不能和别的库重复,否则会导致积木覆盖。<value name="PIN">声明了一个参数插槽,嵌套的math_number表示这个插槽只接受数字输入,<field name="NUM">是积木上默认显示的值。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 | 带引号字符串,如"MyWiFi" | SSID、密码、服务器地址 |
| 布尔开关 | logic_boolean | true或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 type="wifi_auto_config"> <value name="TIMEOUT"> <block type="math_number"> <field name="NUM">30000</field> </block> </value> </block> <block type="wifi_save_config"> <value name="SSID"> <block type="text"></block> </value> <value name="PWD"> <block type="text"></block> </value> </block> <block type="wifi_get_ssid"> <value name="RETURN"> <block type="text"></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 type="wifi_save_config">的type属性必须和_code.txt里生成的函数调用保持一致,否则会出现积木拖出来了、编译却报undefined reference。<value name="SSID">的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 类型积木生成的是字符串字面量"9600",Arduino 的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)直接put,ESP8266 的模拟 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 是干净的0xFF,magic 不等于0xA5,读出来就主动清零,避免一堆乱码 SSID 去连 WiFi 造成长时间假死。
4.3 编译报 undefined reference:block 类型和函数名脱节
现象:积木拖出来了,代码也生成了,编译报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、冷启动进配网、配网成功后断电、再上电看是否自动连接。这四步走完,库才算真正能交付。希望帮到你。
本文还有配套的精品资源,点击获取