上周合宙發(fā)布了一款1.54寸墨水屏開發(fā)板,售價(jià)僅為16.8元,又掀起一陣搶購(gòu)熱旋風(fēng)。
由于墨水屏的特性,無(wú)需背光,在光線照射下觀感和紙張印刷效果類似,很適合用來(lái)做電紙書。再配合可以使用Wi-Fi的合宙ESP32C3開發(fā)板,我們就可以用LuatOS驅(qū)動(dòng)這塊墨水屏來(lái)做一個(gè)在線電紙書了。
- LuatOS在線電紙書 -
接下來(lái),讓我們一起看看制作LuatOS在線電紙書的要點(diǎn)吧!
1
基礎(chǔ)準(zhǔn)備工作
本文電紙書示例主要硬件采用合宙LuatOS墨水屏開發(fā)板+ESP32C3開發(fā)板,軟件建議使用合宙Luatools進(jìn)行操作。
1.1 合宙LuatOS墨水屏開發(fā)板一塊:
1.54寸黑白雙色墨水屏,分辨率200*200,板載升壓電路,僅需正常3.3V供電即可驅(qū)動(dòng)。使?LuatOS固件中的eink庫(kù),可以?便快捷地驅(qū)動(dòng)屏幕。
點(diǎn)擊圖片鏈接了解更多:
1.2 合宙ESP32C3開發(fā)板一塊:
目前合宙ESP32C3開發(fā)板分為兩版:經(jīng)典款和簡(jiǎn)約款。須注意的是簡(jiǎn)約款無(wú)串口芯片,從Type-C口連接的話沒(méi)法用LuatIDE調(diào)試,需使用Luatools進(jìn)行燒錄。
點(diǎn)擊圖片鏈接了解更多:
1.3 接線示意:
合宙LuatOS墨水屏開發(fā)板接口兼容合宙LuatOS全系列MCU開發(fā)板,對(duì)應(yīng)接口對(duì)插即可。
2
編寫在線電紙書
2.1 解鎖GPIO11
由于墨水屏的BUSY引腳連接到了ESP32C3的GPIO11,但是ESP32C3的GPIO11(VDD_SPI)默認(rèn)功能是給Flash供電,默認(rèn)情況下無(wú)法當(dāng)作GPIO使用,我們可以使用外部3.3V給Flash供電,把GPIO11釋放出來(lái)使用。
具體步驟詳見【ESP32C3解鎖使用IO11】:https://gitee.com/dreamcmi/LuatOS-ESP32/blob/master/doc/VDD_SPI_AS_GPIO.md
2.2 界面及交互設(shè)計(jì)
使用開發(fā)板上的BOOT鍵(GPIO9)作為功能按鍵:
單擊:下一個(gè)
雙擊:上一個(gè)
長(zhǎng)按:進(jìn)入/退出
選擇12號(hào)中文字體作為顯示字體;開機(jī)后顯示圖書列表界面,圖書列表一頁(yè)顯示11項(xiàng),選中的圖書高亮顯示,長(zhǎng)按功能鍵進(jìn)入圖書閱讀界面,閱讀界面顯示內(nèi)容為11行*12列,最下面一行顯示當(dāng)前的閱讀進(jìn)度,在閱讀界面長(zhǎng)按功能鍵退回到圖書列表界面。
2.3 搭建在線電紙書后臺(tái)服務(wù)
后臺(tái)服務(wù)主要提供以下兩個(gè)HTTP接口供客戶端調(diào)用:
當(dāng)前墨水屏使用12號(hào)中文字體的情況下,一頁(yè)顯示11行*12列,后臺(tái)會(huì)根據(jù)需求分頁(yè)返回給客戶端。
后臺(tái)源代碼詳見【電紙書后臺(tái)源碼】:
https://github.com/JeremyHash/EinkBook-LuatOS/blob/master/Server/main.go
2.4 封裝必要模塊
在線電紙書需要Wi-Fi連接和發(fā)起HTTP請(qǐng)求兩個(gè)基本需求,這部分代碼比較多,封裝成兩個(gè)函數(shù):
創(chuàng)建Wi-Fi連接
手機(jī)橫屏/上下滑動(dòng)查看完整代碼:
function wifiConnect.connect(ssid, passwd)
local waitRes, data
if wlan.init() ~= 0 then
log.error(tag .. ".init", "ERROR")
return false
end
if wlan.setMode(wlan.STATION) ~= 0 then
log.error(tag .. ".setMode", "ERROR")
return false
end
if USE_SMARTCONFIG == true then
if wlan.smartconfig() ~= 0 then
log.error(tag .. ".connect", "ERROR")
return false
end
waitRes, data = sys.waitUntil("WLAN_STA_CONNECTED", 180 * 10000)
log.info("WLAN_STA_CONNECTED", waitRes, data)
if waitRes ~= true then
log.error(tag .. ".wlan ERROR")
return false
end
waitRes, data = sys.waitUntil("IP_READY", 10000)
if waitRes ~= true then
log.error(tag .. ".wlan ERROR")
return false
end
log.info("IP_READY", waitRes, data)
return true
end
if wlan.connect(ssid, passwd) ~= 0 then
log.error(tag .. ".connect", "ERROR")
return false
end
waitRes, data = sys.waitUntil("WLAN_STA_CONNECTED", 10000)
if waitRes ~= true then
log.error(tag .. ".wlan ERROR")
return false
end
log.info("WLAN_STA_CONNECTED", waitRes, data)
waitRes, data = sys.waitUntil("IP_READY", 10000)
if waitRes ~= true then
log.error(tag .. ".wlan ERROR")
return false
end
log.info("IP_READY", waitRes, data)
return true
end
發(fā)起HTTP請(qǐng)求
手機(jī)橫屏/上下滑動(dòng)查看完整代碼:
local methodTable = {
GET = esphttp.GET,
POST = esphttp.POST,
PUT = esphttp.PUT,
DELETE = esphttp.DELETE
}
function httpLib.request(method, url, head)
local responseCode = 0
local httpc = esphttp.init(methodTable[method], url)
if httpc == nil then
esphttp.cleanup(httpc)
return false, responseCode, "create httpClient error"
end
if head ~= nil then
esphttp.set_header(httpc, k, v)
end
end
local ok, err = esphttp.perform(httpc, true)
if ok then
local response = ""
while 1 do
local result, c, ret, data = sys.waitUntil("ESPHTTP_EVT", 20000)
-- log.info("ESPHTTP_EVT", result, c, ret, data)
if result == false then
esphttp.cleanup(httpc)
return false, responseCode, "wait for http response timeout"
end
if c == httpc then
if esphttp.is_done(httpc, ret) then
esphttp.cleanup(httpc)
return true, esphttp.status_code(httpc), response
end
if ret == esphttp.EVENT_ON_DATA then
response = response .. data
end
end
end
else
esphttp.cleanup(httpc)
return false, responseCode, "perform httpClient error " .. err
end
end
2.5 封裝必要模塊編寫電紙書代碼
下面開始電紙書部分的邏輯代碼,主要分為初始化FDB、初始化墨水屏、文字函數(shù)、圖書列表及內(nèi)容函數(shù)、網(wǎng)絡(luò)及按鍵功能等幾個(gè)部分:
初始化FDB
assert(fdb.kvdb_init("env", "onchip_flash") == true, tag .. ".kvdb_init ERROR")
初始化墨水屏
由于我們使用的是ESP32C3,連接的SPI通道id為2,初始化成功之后需要設(shè)置墨水屏的大小,然后將墨水屏完整的黑白刷新一次來(lái)清除之前顯示內(nèi)容的殘留,再設(shè)置我們要使用的12號(hào)中文字體。
代碼如下:
-- 局刷模式
eink.setup(1, 2, 11, 10, 6, 7)
-- 設(shè)置分辨率200*200
eink.setWin(200, 200, 0)
-- 全刷一次屏幕防止之前的顯示內(nèi)容殘留
eink.clear(0, true)
eink.show(0, 0)
eink.clear(1, true)
eink.show(0, 0)
eink.setFont(eink.font_opposansm12_chinese)
封裝墨水屏顯示文字函數(shù)
function einkShowStr(x, y, str, colored, clear, show)
-- 每20次刷屏就全刷一次防止顯示殘留
if einkPrintTime > 20 then
einkPrintTime = 0
eink.rect(0, 0, 200, 200, 0, 1)
eink.show(0, 0, true)
eink.rect(0, 0, 200, 200, 1, 1)
eink.show(0, 0, true)
end
if clear == true then
eink.clear()
end
eink.print(x, y, str, colored)
if show == true then
einkPrintTime = einkPrintTime + 1
eink.show(0, 0, true)
end
end
封裝渲染圖書列表和圖書內(nèi)容函數(shù)
手機(jī)橫屏/上下滑動(dòng)查看完整代碼:
function showBookList(index)
local firstIndex
for k, v in pairs(onlineBooksShowTableTmp[1]) do
firstIndex = v["index"]
end
-- 當(dāng)要高亮的圖書索引超過(guò)了當(dāng)前的列表,擴(kuò)充當(dāng)前列表
if index > firstIndex + 10 then
onlineBooksShowTableTmp = getTableSlice(onlineBooksShowTable, index - 10, index)
end
if index < firstIndex then
onlineBooksShowTableTmp = getTableSlice(onlineBooksShowTable, index, index + 10)
end
einkShowStr(0, 16, "圖書列表", 0, true)
local ifShow = false
local len = getTableLen(onlineBooksTable)
local showLen = getTableLen(onlineBooksShowTableTmp)
if len == 0 then
einkShowStr(0, 32, "暫無(wú)在線圖書", 0, false, true)
return
end
local i = 1
for k, v in pairs(onlineBooksShowTableTmp) do
for name, info in pairs(v) do
local bookName = string.split(name, ".")[1]
local bookSize = tonumber(info["size"]) / 1024 / 1024
if i == showLen then
ifShow = true
end
if info["index"] == index then
eink.rect(0, 16 * i, 200, 16 * (i + 1), 0, 1, nil, ifShow)
einkShowStr(0, 16 * (i + 1), bookName .. " " .. string.format("%.2f", bookSize) .. "MB", 1,
nil, ifShow)
else
einkShowStr(0, 16 * (i + 1), bookName .. " " .. string.format("%.2f", bookSize) .. "MB", 0,
nil, ifShow)
end
i = i + 1
end
end
end
function showBook(bookName, bookUrl, page)
sys.taskInit(function()
waitHttpTask = true
for i = 1, 3 do
local result, code, data = httpLib.request("GET", bookUrl .. "/" .. page)
log.info("SHOWBOOK", result, code)
if result == false or code == -1 or code == 0 then
log.error("SHOWBOOK", "獲取圖書內(nèi)容失敗 ", data)
else
local bookLines = json.decode(data)
for k, v in pairs(bookLines) do
if k == 1 then
einkShowStr(0, 16 * k, v, 0, true, false)
elseif k == #bookLines then
einkShowStr(0, 16 * k, v, 0, false, false)
else
einkShowStr(0, 16 * k, v, 0, false, false)
end
end
-- 最后一行渲染讀書進(jìn)度
einkShowStr(60, 16 * 12 + 2, page .. "/" .. onlineBooksTable[bookName]["pages"], 0, false, true)
break
end
end
waitHttpTask = false
end)
end
連接網(wǎng)絡(luò)并獲取圖書列表
需要提前創(chuàng)建幾個(gè)函數(shù),用來(lái)處理下面獲取到的圖書列表數(shù)據(jù)。
手機(jī)橫屏/上下滑動(dòng)查看完整代碼:
-- 獲取table的切片并作為一個(gè)新的table返回
function getTableSlice(intable, startIndex, endIndex)
local outTable = {}
for i = startIndex, endIndex do
table.insert(outTable, intable[i])
end
return outTable
end
-- 通過(guò)#獲取table長(zhǎng)度不靠譜,所以需要這個(gè)函數(shù)
function getTableLen(t)
local count = 0
for _, _ in pairs(t) do
count = count + 1
end
return count
end
-- 需要將獲取到的圖書列表格式化符合我們下面的需求
function formatOnlineBooksTable(inTable)
local outTable = {}
local i = 1
for k, v in pairs(inTable) do
v["index"] = i
table.insert(outTable, {
[k] = v
})
i = i + 1
end
return outTable
end
```
調(diào)用之前封裝的WiFi連接函數(shù),將獲取到的圖書列表解析并顯示
代碼如下:
for i = 1, 5 do
local result, code, data = httpLib.request("GET",serverAdress .. "getBooks")
if result == false or code == -1 or code == 0 then
log.error(tag, "獲取圖書列表失敗 ", data)
if i == 5 then
einkShowStr(0, 16, "連接圖書服務(wù)器失敗 正在重啟", 0,true, true)
rtos.reboot()
end
else
onlineBooksTable = json.decode(data)
onlineBooksTableLen = getTableLen(onlineBooksTable)
onlineBooksShowTable = formatOnlineBooksTable(onlineBooksTable)
onlineBooksShowTableTmp = getTableSlic(onlineBooksShowTable, 1, 11)
-- 渲染圖書列表,索引從1開始
showBookList(1)
btnSetup(9, 1000, btnShortHandle, btnLongHandle, btnDoublehandle)
break
end
sys.wait(1000)
end
初始化功能鍵并編寫對(duì)應(yīng)的按鍵處理
初始化BOOT/GPIO9為功能鍵,在對(duì)應(yīng)的按鍵事件處理函數(shù)中,調(diào)用之前封裝好的渲染圖書列表或渲染圖書內(nèi)容的函數(shù)。
手機(jī)橫屏/上下滑動(dòng)查看完整代碼:
-- 短按處理函數(shù)
function btnShortHandle()
-- 如果當(dāng)前http請(qǐng)求未完成則直接返回
if waitHttpTask == true then
waitDoubleClick = false
return
end
-- 如果當(dāng)前頁(yè)面在圖書列表,短按會(huì)高亮下一本書
if PAGE == "LIST" then
if einkBooksIndex == onlineBooksTableLen then
einkBooksIndex = 1
else
einkBooksIndex = einkBooksIndex + 1
end
showBookList(einkBooksIndex)
-- 如果當(dāng)前頁(yè)面在圖書閱讀頁(yè)面,短按進(jìn)入下一頁(yè)
else
local i = 1
local bookName = nil
for k, v in pairs(onlineBooksTable) do
if i == einkBooksIndex then
bookName = k
end
i = i + 1
end
local thisBookPages = tonumber(onlineBooksTable[bookName]["pages"])
-- 如果當(dāng)前頁(yè)已是最后一頁(yè),直接返回
if thisBookPages == gpage then
waitDoubleClick = false
return
end
gpage = gpage + 1
-- 渲染下一頁(yè)
showBook(bookName, serverAdress .. string.urlEncode(bookName), gpage)
log.info(bookName, gpage)
-- 把新的閱讀記錄存入fdb
fdb.kv_set(bookName, gpage)
end
waitDoubleClick = false
end
-- 長(zhǎng)按處理函數(shù)
function btnLongHandle()
if waitHttpTask == true then
return
end
-- 如果當(dāng)前在列表頁(yè)進(jìn)入內(nèi)容閱讀頁(yè)
if PAGE == "LIST" then
PAGE = "BOOK"
local i = 1
local bookName = nil
for k, v in pairs(onlineBooksTable) do
if i == einkBooksIndex then
bookName = k
end
i = i + 1
end
local pageCache = fdb.kv_get(bookName)
log.info(bookName, pageCache)
if pageCache == nil then
gpage = 1
showBook(bookName, serverAdress .. string.urlEncode(bookName), gpage)
else
gpage = pageCache
showBook(bookName, serverAdress .. string.urlEncode(bookName), pageCache)
end
-- 如果當(dāng)前在內(nèi)容頁(yè)進(jìn)入列表頁(yè)
elseif PAGE == "BOOK" then
PAGE = "LIST"
showBookList(einkBooksIndex)
end
end
-- 雙擊處理函數(shù)
function btnDoublehandle()
if waitHttpTask == true then
return
end
-- 若果當(dāng)前在列表頁(yè),高亮上一項(xiàng)
if PAGE == "LIST" then
if einkBooksIndex == 1 then
einkBooksIndex = onlineBooksTableLen
else
einkBooksIndex = einkBooksIndex - 1
end
showBookList(einkBooksIndex)
-- 如果當(dāng)前在內(nèi)容頁(yè),讀取上一頁(yè)內(nèi)容
else
-- 如果已是第一頁(yè)直接返回
if gpage == 1 then
return
end
gpage = gpage - 1
local i = 1
local bookName = nil
for k, v in pairs(onlineBooksTable) do
if i == einkBooksIndex then
bookName = k
end
i = i + 1
end
log.info(bookName, gpage)
fdb.kv_set(bookName, gpage)
showBook(bookName, serverAdress .. string.urlEncode(bookName), gpage)
end
end
-- 按鍵中斷處理函數(shù)
function btnHandle(val)
if val == 0 then
-- 按下時(shí)在等待雙擊狀態(tài)時(shí),停止短按事件函數(shù)的定時(shí)器,并觸發(fā)雙擊
if waitDoubleClick == true then
sys.timerStop(gShortCb)
gDoubleCb()
waitDoubleClick = false
return
end
-- 啟動(dòng)一個(gè)定時(shí)器觸發(fā)長(zhǎng)按處理函數(shù)
sys.timerStart(longTimerCb, gPressTime)
gBtnStatus = "PRESSED"
else
-- 停止長(zhǎng)按處理函數(shù)的定時(shí)器
sys.timerStop(longTimerCb)
-- 如果當(dāng)前狀態(tài)為短按下,開啟一個(gè)定時(shí)器來(lái)觸發(fā)短按處理函數(shù)
if gBtnStatus == "PRESSED" then
sys.timerStart(gShortCb, 500)
waitDoubleClick = true
gBtnStatus = "IDLE"
-- 如果當(dāng)前狀態(tài)以為長(zhǎng)按處理函數(shù)執(zhí)行完成,改變狀態(tài)為IDLE并返回
elseif gBtnStatus == "LONGPRESSED" then
gBtnStatus = "IDLE"
end
end
end
-- 初始化按鍵函數(shù)
function btnSetup(gpioNumber, pressTime, shortCb, longCb, doubleCb)
gpio.setup(gpioNumber, btnHandle, gpio.PULLUP)
gPressTime = pressTime
gShortCb = shortCb
gLongCb = longCb
gDoubleCb = doubleCb
end
-- 初始化GPIO9為上拉中斷模式,并注冊(cè)短按/長(zhǎng)按/雙擊的處理函數(shù)
btnSetup(9, 1000, btnShortHandle, btnLongHandle, btnDoublehandle)
2.6相關(guān)資料鏈接
完整項(xiàng)目代碼:
https://github.com/JeremyHash/EinkBook-LuatOS
燒錄教程:
https://wiki.luatos.com/boardGuide/flash.html
墨水屏開發(fā)板資料:
https://wiki.luatos.com/peripherals/eink_1.54/index.html
ESP32C3開發(fā)板資料:
https://wiki.luatos.com/chips/esp32c3/index.html
好了,今天就分享到這里
以上內(nèi)容你學(xué)會(huì)了嗎
自己制作一款電紙書愉快玩耍吧
-
電紙書
+關(guān)注
關(guān)注
0文章
26瀏覽量
9602 -
開發(fā)板
+關(guān)注
關(guān)注
25文章
4896瀏覽量
97059
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論