新鲜出炉!LuatOS墨水屏+ESP32C3开发板,自制在线电纸书

描述

 

 

上周合宙发布了一款1.54寸墨水屏开发板,售价仅为16.8元,又掀起一阵抢购热旋风

由于墨水屏的特性,无需背光,在光线照射下观感和纸张印刷效果类似,很适合用来做电纸书。再配合可以使用Wi-Fi的合宙ESP32C3开发板,我们就可以用LuatOS驱动这块墨水屏来做一个在线电纸书了。
 

开发板

- LuatOS在线电纸书 -

接下来,让我们一起看看制作LuatOS在线电纸书的要点吧!

 

1

基础准备工作


 

本文电纸书示例主要硬件采用合宙LuatOS墨水屏开发板+ESP32C3开发板,软件建议使用合宙Luatools进行操作。
 

1.1 合宙LuatOS墨水屏开发板一块:

1.54寸黑白双色墨水屏,分辨率200*200,板载升压电路,仅需正常3.3V供电即可驱动。使⽤LuatOS固件中的eink库,可以⽅便快捷地驱动屏幕。

点击图片链接了解更多:

开发板

 

1.2 合宙ESP32C3开发板一块:

目前合宙ESP32C3开发板分为两版:经典款和简约款。须注意的是简约款无串口芯片,从Type-C口连接的话没法用LuatIDE调试,需使用Luatools进行烧录。

点击图片链接了解更多:

开发板

 

1.3 接线示意:

合宙LuatOS墨水屏开发板接口兼容合宙LuatOS全系列MCU开发板,对应接口对插即可。

开发板

 

 

 

2

编写在线电纸书


 

开发板

2.1 解锁GPIO11

由于墨水屏的BUSY引脚连接到了ESP32C3的GPIO11,但是ESP32C3的GPIO11(VDD_SPI)默认功能是给Flash供电,默认情况下无法当作GPIO使用,我们可以使用外部3.3V给Flash供电,把GPIO11释放出来使用。

具体步骤详见【ESP32C3解锁使用IO11】:https://gitee.com/dreamcmi/LuatOS-ESP32/blob/master/doc/VDD_SPI_AS_GPIO.md

2.2 界面及交互设计

使用开发板上的BOOT键(GPIO9)作为功能按键:

单击:下一个
双击:上一个
长按:进入/退出

选择12号中文字体作为显示字体;开机后显示图书列表界面,图书列表一页显示11项,选中的图书高亮显示,长按功能键进入图书阅读界面,阅读界面显示内容为11行*12列,最下面一行显示当前的阅读进度,在阅读界面长按功能键退回到图书列表界面。

2.3 搭建在线电纸书后台服务

后台服务主要提供以下两个HTTP接口供客户端调用:

开发板

当前墨水屏使用12号中文字体的情况下,一页显示11行*12列,后台会根据需求分页返回给客户端。

后台源代码详见【电纸书后台源码】:

https://github.com/JeremyHash/EinkBook-LuatOS/blob/master/Server/main.go

2.4 封装必要模块

在线电纸书需要Wi-Fi连接和发起HTTP请求两个基本需求,这部分代码比较多,封装成两个函数:

 

创建Wi-Fi连接
 

 

 


 

 

手机横屏/上下滑动查看完整代码:

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

 

 

 

 

发起HTTP请求

 

 


 

 

手机横屏/上下滑动查看完整代码:

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

        for k, v in pairs(head) do

            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、初始化墨水屏、文字函数、图书列表及内容函数、网络及按键功能等几个部分:

 

初始化FDB

 

 


 

assert(fdb.kvdb_init("env", "onchip_flash") == true, tag .. ".kvdb_init ERROR")

 

 

 

初始化墨水屏

 

 


 

由于我们使用的是ESP32C3,连接的SPI通道id为2,初始化成功之后需要设置墨水屏的大小,然后将墨水屏完整的黑白刷新一次来清除之前显示内容的残留,再设置我们要使用的12号中文字体。

代码如下:

-- 局刷模式

eink.setup(1, 2, 11, 10, 6, 7)

-- 设置分辨率200*200

eink.setWin(200, 200, 0)

-- 全刷一次屏幕防止之前的显示内容残留

eink.clear(0, true)

eink.show(0, 0)

eink.clear(1, true)

eink.show(0, 0)

eink.setFont(eink.font_opposansm12_chinese)

 

 

 

封装墨水屏显示文字函数

 

 


 

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

 

 

 

封装渲染图书列表和图书内容函数

 

 


 

 

手机横屏/上下滑动查看完整代码:

function showBookList(index)

    local firstIndex

    for k, v in pairs(onlineBooksShowTableTmp[1]) do

        firstIndex = v["index"]

    end

    -- 当要高亮的图书索引超过了当前的列表,扩充当前列表

    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, "暂无在线图书", 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", "获取图书内容失败 ", 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

                -- 最后一行渲染读书进度

                einkShowStr(60, 16 * 12 + 2, page .. "/" .. onlineBooksTable[bookName]["pages"], 0, false, true)

                break

            end

        end

        waitHttpTask = false

    end)

end

 

 

 

 

连接网络并获取图书列表

 

 


 

 

需要提前创建几个函数,用来处理下面获取到的图书列表数据。

手机横屏/上下滑动查看完整代码:

-- 获取table的切片并作为一个新的table返回

function getTableSlice(intable, startIndex, endIndex)

    local outTable = {}

    for i = startIndex, endIndex do

        table.insert(outTable, intable[i])

    end

    return outTable

end

 

-- 通过#获取table长度不靠谱,所以需要这个函数

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

```

 

调用之前封装的WiFi连接函数,将获取到的图书列表解析并显示

 

代码如下:

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, "连接图书服务器失败 正在重启", 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

 

 

 

 

初始化功能键并编写对应的按键处理
 

 

 


 

 

初始化BOOT/GPIO9为功能键,在对应的按键事件处理函数中,调用之前封装好的渲染图书列表或渲染图书内容的函数。

手机横屏/上下滑动查看完整代码:

-- 短按处理函数

function btnShortHandle()

    -- 如果当前http请求未完成则直接返回

    if waitHttpTask == true then

        waitDoubleClick = false

        return

    end

    -- 如果当前页面在图书列表,短按会高亮下一本书

    if PAGE == "LIST" then

        if einkBooksIndex == onlineBooksTableLen then

            einkBooksIndex = 1

        else

            einkBooksIndex = einkBooksIndex + 1

        end

        showBookList(einkBooksIndex)

    -- 如果当前页面在图书阅读页面,短按进入下一页

    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"])

        -- 如果当前页已是最后一页,直接返回

        if thisBookPages == gpage then

            waitDoubleClick = false

            return

        end

        gpage = gpage + 1

        -- 渲染下一页

        showBook(bookName, serverAdress .. string.urlEncode(bookName), gpage)

        log.info(bookName, gpage)

        -- 把新的阅读记录存入fdb

        fdb.kv_set(bookName, gpage)

    end

    waitDoubleClick = false

end

 

-- 长按处理函数

function btnLongHandle()

    if waitHttpTask == true then

        return

    end

    -- 如果当前在列表页进入内容阅读页

    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

    -- 如果当前在内容页进入列表页

    elseif PAGE == "BOOK" then

        PAGE = "LIST"

        showBookList(einkBooksIndex)

    end

end

 

-- 双击处理函数

function btnDoublehandle()

    if waitHttpTask == true then

        return

    end

    -- 若果当前在列表页,高亮上一项

    if PAGE == "LIST" then

        if einkBooksIndex == 1 then

            einkBooksIndex = onlineBooksTableLen

        else

            einkBooksIndex = einkBooksIndex - 1

        end

        showBookList(einkBooksIndex)

    -- 如果当前在内容页,读取上一页内容

    else

        -- 如果已是第一页直接返回

        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

 

-- 按键中断处理函数

function btnHandle(val)

    if val == 0 then

        -- 按下时在等待双击状态时,停止短按事件函数的定时器,并触发双击

        if waitDoubleClick == true then

            sys.timerStop(gShortCb)

            gDoubleCb()

            waitDoubleClick = false

            return

        end

        -- 启动一个定时器触发长按处理函数

        sys.timerStart(longTimerCb, gPressTime)

        gBtnStatus = "PRESSED"

    else

        -- 停止长按处理函数的定时器

        sys.timerStop(longTimerCb)

        -- 如果当前状态为短按下,开启一个定时器来触发短按处理函数

        if gBtnStatus == "PRESSED" then

            sys.timerStart(gShortCb, 500)

            waitDoubleClick = true

            gBtnStatus = "IDLE"

        -- 如果当前状态以为长按处理函数执行完成,改变状态为IDLE并返回

        elseif gBtnStatus == "LONGPRESSED" then

            gBtnStatus = "IDLE"

        end

    end

end

 

-- 初始化按键函数

function btnSetup(gpioNumber, pressTime, shortCb, longCb, doubleCb)

    gpio.setup(gpioNumber, btnHandle, gpio.PULLUP)

    gPressTime = pressTime

    gShortCb = shortCb

    gLongCb = longCb

    gDoubleCb = doubleCb

end

 

-- 初始化GPIO9为上拉中断模式,并注册短按/长按/双击的处理函数

btnSetup(9, 1000, btnShortHandle, btnLongHandle, btnDoublehandle)

 

 

 

2.6 相关资料链接

完整项目代码:

https://github.com/JeremyHash/EinkBook-LuatOS

烧录教程:

https://wiki.luatos.com/boardGuide/flash.html

墨水屏开发板资料:
https://wiki.luatos.com/peripherals/eink_1.54/index.html

ESP32C3开发板资料:
https://wiki.luatos.com/chips/esp32c3/index.html

 

 

 

好了,今天就分享到这里
以上内容你学会了吗
自己制作一款电纸书愉快玩耍吧

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分