:從拖積木到造積木的完整實戰(zhàn))
簡介這套 Mixly 自制庫專為 ESP8266 開發(fā)設計聚焦物聯(lián)網原型制作中最常用的幾個環(huán)節(jié)EEPROM 斷電保存與字符復制、WiFi 自動配網、數(shù)據類型轉換以及基于 U8G2 的液晶屏驅動。壓縮包共 68 個文件核心代碼是 6 個 JavaScript 模塊配以 60 張 PNG 圖解另有 TXT 說明和 XML 配置整體 446KB輕量且容易遷移到不同項目。已有 5786 人學習使用尤其適合剛接觸 ESP8266 的愛好者以及想在課堂上演示圖形化編程的教師。借助說明文檔和模塊源碼可以弄清如何在 Mixly 中寫入/讀取 EEPROM 數(shù)據、實現(xiàn)設備自動聯(lián)網并掌握整數(shù)與字符串的相互轉換U8G2 部分則展示了在 OLED/LCD 上繪制文本和簡單圖形的方法。自制庫對模塊邊界的劃分和組織方式也為讀者擴展自己的圖形化積木提供了清晰范本。1. 自制 Mixly 庫從只會拖積木到能造積木的那道坎第一次從前輩手里拿到一個 mixly庫 文件夾時我還在老老實實拖官方積木。把文件丟進米思齊的庫目錄重啟模塊區(qū)憑空多出一排 WiFi 配網、EEPROM 存儲的積木那一刻就意識到能用積木的人和能造積木的人中間隔著的不過是一份 XML 描述文件加一堆代碼模板。自制庫這件事說白了就是把你已經寫好的 Arduino 函數(shù)包裝成 Blockly 認識的積木塊讓不懂代碼的人也能用。這篇筆記會把庫結構、配網庫實戰(zhàn)、數(shù)據類型轉換和 EEPROM 的坑一次講完。2. 自制庫的內部結構XML、代碼模板與參數(shù)映射的三層關系2.1 一個庫文件夾的解剖先搞清每個文件是干嘛的市面上能下載到的 mixly 自制庫包拆開看基本都是同一個骨架。先別急著寫代碼把文件認全。以 Mixly 1.x 生態(tài)為例一個完整庫文件夾通常長這樣文件/目錄作用是否必須MyWifi.xml定義積木的外觀、參數(shù)類型、返回形狀必須_code.txt代碼模板決定積木生成什么 Arduino 語句必須MyWifi.h/MyWifi.cpp函數(shù)實現(xiàn)真正干活的部分強烈建議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 的子目錄解析后生成積木分類。放錯路徑的表現(xiàn)不是報錯而是模塊區(qū)一片空白很多新手在這卡半天。正確做法是把整個庫文件夾不是 XML 文件本身丟進libraries重啟 Mixly。2.2 block 類型與代碼模板占位符是如何把積木變成代碼的自制庫的核心機制就一句話積木上填的參數(shù)通過占位符拼進代碼模板最終生成 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聲明了一個參數(shù)插槽嵌套的math_number表示這個插槽只接受數(shù)字輸入field nameNUM是積木上默認顯示的值。MNemonic%1就是第一個參數(shù)槽位Mixly 生成代碼時會把用戶填的數(shù)字替換進去。如果積木有兩個參數(shù)模板里用%1、%2依次對應順序按 XML 里value的排列。參數(shù)說明%1替換的是用戶填的字面值不是變量名。如果用戶填13生成的就是pinMode(13, OUTPUT)如果用戶在積木上改了數(shù)字重新生成代碼時%1也會跟著變。這里有一個血淚經驗%1替換后是裸值字符串類型積木會帶引號數(shù)字不會。所以寫模板時不要手抖加引號否則會出現(xiàn)pinMode(13, OUTPUT)變成pinMode(13, OUTPUT)這類編譯報錯或運行時詭異行為。2.3 參數(shù)類型聲明數(shù)字、字符串、布爾與下拉的選用原則寫模板前先想清楚每個參數(shù)允許填什么。Mixly 的 XML 通過嵌套 block 類型來約束參數(shù)類型常見對應關系如下積木控件XML 嵌套 block 類型生成到模板的內容適用場景數(shù)字輸入框math_number裸數(shù)字如115200波特率、引腳號、延時文本輸入框text帶引號字符串如MyWiFiSSID、密碼、服務器地址布爾開關logic_booleantrue或false功能開關、高低電平選擇下拉選擇自定義field或映射成數(shù)字選中項對應值模式枚舉、通道選擇這里重點說下拉選擇。Mixly 1.x 不同小版本對下拉的實現(xiàn)寫法差異很大有的用dropdown類型有的要求你自定義一個帶options屬性的 block 類型。我踩過一輪坑之后形成了一套保守方案不用花哨的下拉直接把枚舉映射成math_number在.h文件里用switch解析。比如配網模式積木上是一個數(shù)字框填 0 表示 SmartConfig填 1 表示 SoftAP代碼模板里把這個數(shù)字傳給函數(shù)函數(shù)內部判斷。雖然積木不夠「傻瓜化」但換版本時最不容易翻車。2.4 加載機制為什么庫放對了目錄還是看不到積木Mixly 對自定義庫的掃描是有緩存的。第一次啟動時解析 XML之后如果不重啟你改了 XML 它也不會重新讀取。更隱蔽的是如果 XML 文件有語法錯誤比如標簽未閉合、字段名拼錯Mixly 會靜默跳過整個庫模塊區(qū)不出現(xiàn)任何新積木。我的排查習慣是三步走先重啟 Mixly再看 XML 有沒有被編輯器標紅最后把 XML 里非 ASCII 的中文標簽臨時改掉因為 1.x 某些版本對編碼敏感。這三步解決了我 90% 的「庫不顯示」問題。參數(shù)類型的選用直接決定后面類型轉換的坑多坑少所以這塊值得花時間想清楚。能用math_number就絕不用text能限定枚舉就絕不放任用戶輸入這是自制庫參數(shù)設計的黃金法則。3. ESP8266 WiFi 自動配網庫EEPROM 存儲與三個積木塊的完整實現(xiàn)3.1 為什么必須做自動配網從燒死 SSID 到存進 EEPROM開發(fā) ESP8266 項目時最煩的就是每次換 WiFi 環(huán)境都要改代碼重燒固件。固定 SSID 的寫法適合演示不適合產品。自動配網的標準做法是設備上電先嘗試連接上次保存的 WiFi連不上就進入配網模式收到手機下發(fā)的 WiFi 信息后存入 EEPROM下次開機直接連。這樣就實現(xiàn)了「一次配網永久記憶」。方案優(yōu)點缺點適用場景固定 SSID 燒死實現(xiàn)最簡單換網絡必須重燒實驗室固定環(huán)境SmartConfig 一鍵配網用戶操作快需要手機 App 支持阻塞等待有超時坑現(xiàn)場配網、批量部署SoftAP 配網不依賴 App瀏覽器可配開發(fā)量大交互復雜產品化設備我選擇 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 底層實現(xiàn)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 讀寫的基本素養(yǎng)——String內部是堆指針斷電重啟后指針指向的內存早就不在了只有char[]才能穩(wěn)定序列化。magic字段是校驗位寫固定值0xA5讀出來如果不是這個值就說明 EEPROM 里沒有有效配置從而避免用垃圾數(shù)據去連 WiFi。所有函數(shù)聲明為extern方便不同代碼模板文件引用#ifndef防止多個積木重復 include 時重定義。接下來是MyWifi.cpp的實現(xiàn)#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()每次調用都執(zhí)行EEPROM.begin(sizeof(WifiConfig))這是 ESP8266 的規(guī)矩。ESP8266 的 EEPROM 庫是拿 Flash 模擬的不調用begin(n)直接get會得到全0xFF。beginSmartConfig()啟動配網后smartConfigDone()返回 true 只代表手機 app 已經把 WiFi 消息發(fā)過來了此時WiFi.SSID()和WiFi.psk()才能取到有效值所以要再等WL_CONNECTED。參數(shù)說明timeout_ms默認傳 30000 毫秒這是配網等待的上限。很多庫卡死就是因為沒這個超時后面避坑章節(jié)專門講。wifi_save_config接收兩個const String引用避免拷貝開銷toCharArray時第三個參數(shù)傳sizeof是復制上限防止超長密碼把結構體后面的字段沖掉造成內存踩踏。3.3 XML 與代碼模板把三個函數(shù)包裝成積木塊底層函數(shù)寫好了剩下的工作是讓這些函數(shù)從積木區(qū)長出來。新建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 顯示或串口打印等其他積木的參數(shù)槽里。_code.txt里的三行分別對應三個積木塊的生成代碼wifi_get_ssid()沒有參數(shù)所以生成的就是一條函數(shù)調用。頭文件的 include 建議加在第一個積木的模板第一行#include MyWifi.h因為頭文件帶了#ifndef防重入多個積木重復 include 不會報錯只是多一次預處理檢查。參數(shù)說明XML 中block typewifi_save_config的type屬性必須和_code.txt里生成的函數(shù)調用保持一致否則會出現(xiàn)積木拖出來了、編譯卻報undefined reference。value nameSSID的name屬性是積木內部識別槽位用的改不改不影響生成代碼但建議寫成和參數(shù)含義相關的名字方便你三個月后回來維護時看懂。3.4 在 Mixly 里加載與驗證從模塊區(qū)到串口輸出把MyWifi.xml、_code.txt、MyWifi.h、MyWifi.cpp放在同一個文件夾文件夾命名MyWifi整體丟進 Mixly 的libraries目錄重啟 Mixly。驗證步驟模塊區(qū)出現(xiàn)「MyWifi」分類拖出三個積木到編程區(qū)。燒錄前先打開串口監(jiān)視器波特率設 115200。上電后觀察串口先打印[WiFi] try saved config...接著嘗試連保存的 WiFi。如果之前沒保存過配置設備會進入 SmartConfig 配網狀態(tài)此時用支持 ESP-Touch 協(xié)議的手機 App 發(fā)送家庭 WiFi 信息。配網成功后串口打印[WiFi] connected, save config斷電重啟后應能看到設備直接連接 WiFi不再進入配網。這里有一個很多人忽略的驗證點斷電前要確認已經走到wifi_save_config。SmartConfig 收到信息到執(zhí)行保存之間有個時間差如果在WiFi.status() ! WL_CONNECTED等待循環(huán)里就斷電設備會拿不到完整配置下次開機又回到配網流程。這是硬件調試里典型的「看起來配網成功實際沒存住」問題。4. 自制庫避坑排查數(shù)據類型轉換、EEPROM 與編譯失敗的四個現(xiàn)場4.1 字符串轉數(shù)字看似能編譯一跑就出錯現(xiàn)象積木上填的波特率9600生成的代碼是Serial.begin(9600)編譯不報錯串口輸出全亂碼?;蛘甙盐谋緟?shù)轉成int后判斷出錯比如空字符串轉出來是0導致邏輯誤判成「合法」。原因text 類型積木生成的是字符串字面量9600Arduino 的Serial.begin有l(wèi)ong重載字符串字面量能隱式轉換但轉換結果是內存地址而不是數(shù)值。更隱蔽的是String(abc).toInt()在解析失敗時返回0這會和真實數(shù)值0混淆沒法區(qū)分「用戶填了 0」和「用戶填了非法字符」。解決模板里顯式轉換// 錯誤寫法依賴隱式轉換 wifi_send_command(%1); // 正確寫法String 包一層再轉明確意圖 wifi_send_command(String(%1).toInt());邏輯說明String(%1)先構造臨時 String 對象再調用toInt()把字符串解析成int。這樣無論用戶填的是不是純數(shù)字語義都明確。參數(shù)說明如果這個參數(shù)本身就是數(shù)字框math_number不用擔心這個問題只有 text 框才會產生字符串字面量。4.2 EEPROM 寫入靜默失敗重啟后配置全丟現(xiàn)象第一次配網成功斷電重啟后又進入配網模式或者串口打印讀出來的 SSID 是\xFF\xFF...一串亂碼。原因三個高頻根因。第一沒調EEPROM.begin(n)直接putESP8266 的模擬 EEPROM 不初始化就是全0xFF第二begin(n)的 n 小于實際寫入的結構體大小比如結構體 65 字節(jié)卻begin(64)最后一個字節(jié)寫越界commit 時被丟棄第三寫入后忘記commit()數(shù)據留在緩存沒刷進 Flash。結構體里直接放String類型的同學字符串內容存在堆上序列化到 Flash 的只是指針的碎片斷電后必丟。解決統(tǒng)一用下面的寫法// 結構體定長、帶 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 字節(jié)begin必須傳 65 或更大。參數(shù)說明ESP8266 的 EEPROM 庫上限是 4096 字節(jié)Flash 扇區(qū)大小超過 4096 的寫入會被截斷別想著拿它當大容量存儲用存幾百字節(jié)的配置信息才是它的本分。依靠 magic 字段判斷有效性的好處是第一次上電 Flash 是干凈的0xFFmagic 不等于0xA5讀出來就主動清零避免一堆亂碼 SSID 去連 WiFi 造成長時間假死。4.3 編譯報 undefined referenceblock 類型和函數(shù)名脫節(jié)現(xiàn)象積木拖出來了代碼也生成了編譯報undefined reference to wifi_auto_config(unsigned int)。最氣人的是函數(shù)明明寫在.cpp里了就是鏈接不上。原因函數(shù)聲明和定義的簽名不一致或者 block type 生成的調用函數(shù)名和庫文件里的定義不是一個名字。我見過最離譜的一次是 XML 里 block type 寫的wifi_auto_config代碼模板寫的也是wifi_auto_config但.h里聲明的是bool wifi_auto_config(int timeout)傳參類型不同C 符號修飾后就不是同一個函數(shù)了。解決先看生成代碼再核對簽名# 把 Mixly 生成的臨時 sketch 目錄打開搜函數(shù)名 grep -rn wifi_auto_config generated_sketch/邏輯說明這行命令把生成目錄里所有引用wifi_auto_config的地方標出來。重點看聲明、調用、定義的參數(shù)類型是否一致。參數(shù)說明const String和String是兩個不同的函數(shù)簽名C 的符號表會對參數(shù)類型做名稱修飾一個字符不匹配都會導致鏈接失敗。建議庫內部統(tǒng)一用const String傳參模板生成調用時就不用關心副本開銷。4.4 SmartConfig 卡死拿不到 WiFi 信息時程序原地等待現(xiàn)象設備上電后不執(zhí)行任何后續(xù)邏輯卡在配網等待或者配網成功后重啟依然進配網模式。原因beginSmartConfig()之后庫進入一個等待循環(huán)如果一直沒收到手機下發(fā)信息這個循環(huán)就是死循環(huán)。另外有些版本的 ESP8266 在smartConfigDone()返回 true 后立刻讀WiFi.psk()會讀到空值如果不等待WL_CONNECTED就保存存入 EEPROM 的是空密碼。解決所有等待循環(huán)都加超時并且保存前確認連接狀態(tài)uint32_t start millis(); while (!WiFi.smartConfigDone()) { if (millis() - start timeout_ms) { Serial.println([WiFi] smartconfig timeout); return false; } delay(100); }邏輯說明timeout_ms由積木參數(shù)控制默認 30000 毫秒用戶自己可以在積木上改。超時后返回false程序就有機會走其他分支比如進入 SoftAP 模式或者重啟重試而不是死等。參數(shù)說明delay(100)不能省沒有這個延時millis()讀到的值變化太快循環(huán)空轉消耗 CPU 且可能觸發(fā)看門狗復位。配網完成后的while (WiFi.status() ! WL_CONNECTED)同樣要帶超時把這一層思考基本就避開大多數(shù)配網卡死現(xiàn)場了。5. 讓自制庫更耐用的三個進階習慣5.1 給庫加調試開關把串口輸出留在現(xiàn)場調試階段沒有串口打印出了問題只能盲猜。但正式使用時滿天飛的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 時映射為空操作連參數(shù)求值都被優(yōu)化掉。發(fā)布庫之前把開關改成 0 即可不用刪代碼。參數(shù)說明所有調試輸出統(tǒng)一走DBG不要在代碼里裸寫Serial.printf否則后期清理就是一場惡夢。從那以后我每次寫庫都會先鋪好這層宏省掉無數(shù)「這里怎么還有個 print」的尷尬。5.2 留一個清空配置的隱藏積木EEPROM 存了錯誤配置后調試時想回到配網流程總不能每次拔 Flash 或者改代碼。我在庫里額外加了一個wifi_clear_config()函數(shù)專門把 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()讀出來校驗不通過自動清空結構體設備重啟后就會重新進入配網。這個函數(shù)不在正式積木區(qū)展示測試時臨時拖出來用也可以綁一個物理按鍵「長按 5 秒恢復出廠」比重新燒固件快一個量級。5.3 兼容 Mixly 2.x底層邏輯復用積木描述重寫遷移到 2.x 時MyWifi.h和MyWifi.cpp這兩個底層文件可以原封不動復用真正要重寫的是 XML 塊描述——2.x 改成了 JSON 格式描述積木的槽位、顏色、類型。一次遷移的經驗是把 XML 里的每個block type對照著翻譯成 JSON 塊定義參數(shù)類型、默認值和代碼模板的對應關系不變兩個小時就能完成。不要試圖用兼容層去騙 2.x 讀 1.x 的 XML,這條路我沒走通直接重寫反而快。寫庫這幾年最大的教訓來自一次室外驗收設備配網成功、重啟后總掉線折騰一下午最后發(fā)現(xiàn)是EEPROM.begin長度比結構體小 1 字節(jié)magic 校驗位被丟在 Flash 扇區(qū)外。從那以后我每次寫完庫都強制走一遍完整驗證流程清空 EEPROM、冷啟動進配網、配網成功后斷電、再上電看是否自動連接。這四步走完庫才算真正能交付。希望幫到你。本文還有配套的精品資源點擊獲取