Funpack2-5任务三,使用ESP32-S3-BOX-LITE的屏幕实现在线电子书
Funpack2-5任务三,使用ESP32-S3的联网功能以及BOX-LITE的屏幕实现在线电子书阅读器,通过板载ADC按键选择书目、章节并进行翻页
标签
嵌入式系统
Funpack活动
显示
开发板
StreakingJerry
更新2023-08-02
682

项目介绍

Funpack2-5任务三,使用ESP32-S3的联网功能以及BOX-LITE的屏幕实现在线电子书阅读器,通过板载ADC按键选择书目、章节并进行翻页。

 


硬件介绍

ESP-BOX 是乐鑫信息科技发布的新一代 AIoT 应用开发平台。ESP32-S3-BOX 和 ESP32-S3-BOX-Lite 是目前对应的 AIoT 应用开发板,搭载支持 AI 加速的 ESP32-S3 Wi-Fi + Bluetooth 5 (LE) SoC。他们为用户提供了一个基于语音助手 + 触摸屏控制、传感器、红外控制器和智能 Wi-Fi 网关等功能,开发和控制智能家居设备的平台。开发板出厂支持离线语音交互功能,用户通过乐鑫丰富的 SDK 和解决方案,能够轻松构建在线和离线语音助手、智能语音设备、HMI 人机交互设备、控制面板、多协议网关等多样的应用。

 


设计思路

电子书内容来自于网页端信息的爬取,而爬取HTML资源需要大量的字符串处理,会比较耗费内存,因此不是特别适合直接在开发板上进行。因此在该项目中我们使用pc服务器来爬取网页资源,获取到需要的信息后,再开放API接口给开发板,由开发板进行调用。

为了尽可能实现解耦,PC服务器获得的列表与内容都是网页上完整的内容,比如完整的章节列表,或是完整的整张内容;而具体的分页显示,与换行处理全部都在ESP32-S3上完成。因此这也对ESP32-S3提出了更高的要求,因为这需要足够大的内存来装完整的字符串与列表,并且还需要有足够的空间来进行换行与分屏处理。由于Circuitpython在内存管理上和PC上的python并不一样,因此代码中要特别注意内存的使用,将临时变量尽量晚创建,集中创建,并在使用完成后及时清理内存,避免出现碎片化的内存。

项目框图如下:

Fo7wGOBHHujKaP4D01gb6XfuTZC4

 

该项目使用Circuitpython作为开发平台进行开发,分为以下几个模块:

1,抓取小说服务器搭建

2,Wifi连接

3,分页显示

4,从服务器获取小说数据

5,按键控制

 

 

代码讲解

首先先是第一部分:

import requests
from bs4 import BeautifulSoup
import pandas as pd

class qidian:
    def __init__(self) -> None:
        pass

    def getbook(self):
        _url = "https://www.qidian.com/free/all/chanId0-action1/"
        _response = requests.get(_url)
        _soup = BeautifulSoup(_response.text, "html.parser")
        _content = _soup.find("div",class_="all-book-list").find_all("li")
        _df = pd.DataFrame(columns=['title', 'url'])
        _i = 0
        for _book in _content:
            _df.loc[_i] = [_book.h2.a.text,"https:"+_book.h2.a.attrs["href"]]
            _i += 1
        _data = _df.to_json(orient="records")
        return _data
    
    def getchapter(self,url):
        _url = url# + "#Catalog"
        _response = requests.get(_url)
        _soup = BeautifulSoup(_response.text, "html.parser")
        _content = _soup.find("div",class_="catalog-all").find_all("li")
        _df = pd.DataFrame(columns=['title', 'url'])
        _i = 0
        for _book in _content:
            _df.loc[_i] = [_book.a.text,"https:"+_book.a.attrs["href"]]
            _i += 1
        _data = _df.to_json(orient="records")
        return _data

    def gettext(self,url):
        _url = url
        _response = requests.get(_url)
        _soup = BeautifulSoup(_response.text, "html.parser")
        _content = _soup.main.get_text().split('\u3000\u3000')[1:]
        _content[0] = "\u3000\u3000" + _content[0]
        _content = "\n\u3000\u3000".join(_content)
        return _content
    
if __name__ == "__main__":
    from json import loads
    qd=qidian()
    a = qd.getbook()
    pass


这一部分代码是负责从网站上抓取小说。一共有三个方法,第一是获取免费小说列表,第二是根据提供的书目url获取章节列表,第三是根据提供的章节url获取章节内容。

 


接下来只需要一个简单的http服务器,打开以上代码的AI接口即可:

from flask import Flask, request
import json

from qidian import qidian
def func_qidian(data):
    _func = data["func"]
    try:
        _url = data["url"]
    except:
        _url = None
    print(_url)
    _qd = qidian()
    if _func == "getbook":
        return _qd.getbook()
    elif _func == "getchapter":
        return _qd.getchapter(_url)
    elif _func == "gettext":
        return _qd.gettext(_url)
    else:
        return None

app = Flask(__name__)

@app.route("/qidian/", methods=["POST"])
def handle_qidian():
    _data = request.get_json()
    return func_qidian(_data)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8888, debug=True)

 


至此服务端的代码全部结束。接下来我们开始编写esp32-s3-box-lite的代码。

 


首先是wifi连接。circuitpython的wifi连接非常简单,不需要在主程序中添加任何代码,只需将SSID和PASWORD填入settings.toml就行。以下是settings.toml文件示例:

# To auto-connect to Wi-Fi
CIRCUITPY_WIFI_SSID="xxx"
CIRCUITPY_WIFI_PASSWORD="xxxxxxxx"

# To enable modifying files from the web. Change this too!
# Leave the User field blank when you type the password into the browser.
CIRCUITPY_WEB_API_PASSWORD=""
CIRCUITPY_WEB_API_PORT=80

API_URL="http://192.168.1.101:8888/qidian/"


在settings.toml文件中的内容其实就是全局环境变量的定义,最上面两个环境变量就是WIFI设置。开发板在重置后会首先尝试用以上两个环境变量去连接WIFI。这个文件也可以作为secrets文件使用,我将服务端地址也放在了这里,这样大家下载代码后,只需要修改这个文件就可以运行。

 


接下来是分页显示。circuitpython原生自带屏幕驱动支持,所以驱动屏幕也非常简单。但这个项目有所特殊的是需要显示大量的无法提前预支的中文字符,因此需要一个完整的中文字体。好在circuitpython支持bdf与pcf字体,因此我们可以下载现成的wenquanyi_12pt.pcf。初始化字体组件的代码如下:

color = 0x000000
font = bitmap_font.load_font("/wenquanyi_12pt.pcf")
text_area = label.Label(font, color=color)
text_area.y = 8
text_area.x = 2
text_area.background_color = None
text_area.line_spacing = 1.1
text_area.scale = 1
group.append(text_area)

 


这里我们按照功能一共有三种屏幕页面需要显示,一个是书目列表,一个是章节列表,还有最后一个是章节内容。我们使用一个index来确定当前需要显示哪个屏幕,并在屏幕中显示对应内容。内容获取就依靠request方法向我们刚刚搭建的http服务器发送请求。还有一些细节比如自动换行拉,超过一页的内容分页显示等等,就不再赘述。三个屏幕的显示代码如下:

def book_screen():
    global screen_index
    global book_page
    global book_cursor
    global chapter_page
    global chapter_cursor
    global url_chapter
    global _url
    arr.hide()
    chapter_page = 0
    chapter_cursor = 0
    text_area.text = "加载中。。。"
    _data = {"func": "getbook"}
    _raw = requests.post(_url, json=_data).json()
    _len = len(_raw)
    _text = ""
    _turn_page = True
    _move_arr = 0
    while True:
        gc.collect()
        if _turn_page:
            arr.hide()
            _text = ""
            _range = range(book_page, book_page + line)
            for i in _range:
                _text = _text + "\u3000\u3000" + _raw[i]["title"] + "\n"
            text_area.text = ""
            text_area.text = _text
            _turn_page = False
            arr.pos(book_cursor)
            arr.show()

        if _move_arr == 1:
            _move_arr = 0
            arr.pos(book_cursor)
        elif _move_arr == 2:
            _move_arr = 0
            book_page -= line
            if book_page < 0:
                book_cursor = line + book_page
                book_page = 0
                if book_cursor != 0:
                    _turn_page = True
            else:
                book_cursor = line - 1
                _turn_page = True
        elif _move_arr == 3:
            _move_arr = 0
            book_page += line
            if book_page > (_len - line):
                book_cursor = book_page - (_len - line) - 1
                book_page = _len - line
                if book_cursor != (line - 1):
                    _turn_page = True
            else:
                book_cursor = 0
                _turn_page = True
        else:
            pass

        _move_arr, book_cursor = updown(line, book_cursor, 1)
        _sw = choose()
        if _sw == 1:
            _index = book_page + book_cursor
            url_chapter = _raw[_index]["url"]
            screen_index = 1
            return
        else:
            pass


def chapter_screen(qidian_url):
    global screen_index
    global book_page
    global book_cursor
    global chapter_page
    global chapter_cursor
    global url_text
    global _url
    arr.hide()
    text_area.text = "加载中。。。"
    _data = {"func": "getchapter", "url":qidian_url}
    _raw = requests.post(_url, json=_data).json()
    _len = len(_raw)
    _text = ""
    _turn_page = True
    _move_arr = 0
    while True:
        gc.collect()
        if _turn_page:
            arr.hide()
            _text = ""
            for i in range(chapter_page, chapter_page + line):
                _text = _text + "\u3000\u3000" + _raw[i]["title"] + "\n"
            text_area.text = ""
            text_area.text = _text
            _turn_page = False
            arr.pos(chapter_cursor)
            arr.show()

        if _move_arr == 1:
            _move_arr = 0
            arr.pos(chapter_cursor)
        elif _move_arr == 2:
            _move_arr = 0
            chapter_page -= line
            if chapter_page < 0:
                chapter_cursor = line + chapter_page
                chapter_page = 0
                if chapter_cursor != 0:
                    _turn_page = True
            else:
                chapter_cursor = line - 1
                _turn_page = True
        elif _move_arr == 3:
            _move_arr = 0
            chapter_page += line
            if chapter_page > (_len - line):
                chapter_cursor = chapter_page - (_len - line) - 1
                chapter_page = _len - line
                if chapter_cursor != (line - 1):
                    _turn_page = True
            else:
                chapter_cursor = 0
                _turn_page = True
        else:
            pass

        _move_arr, chapter_cursor = updown(line, chapter_cursor, 1)
        _sw = choose()
        if _sw == 1:
            _index = chapter_page + chapter_cursor
            url_text = _raw[_index]["url"]
            screen_index = 2
            return
        elif _sw == 2:
            screen_index = 0
            return
        else:
            pass


def text_screen(qidian_url):
    global screen_index
    global _url
    text_area.text="加载中。。。"
    arr.hide()
    _data = {"func": "gettext", "url":qidian_url}
    _raw = wrap_text_to_pixels(requests.post(_url, json=_data).text,320,font=font)
    _cursor = 0
    _turn = True
    while True:
        gc.collect()
        if _turn:
            _text = ""
            for i in range(_cursor,_cursor+line):
                _text = _text + _raw[i].replace("-","") + "\n"
            text_area.text=""
            text_area.text=_text
            _turn = False
        _turn, _cursor = updown(len(_raw),_cursor,line)
        _sw = choose()
        if _sw == 2:
            screen_index = 1
            return


以上代码中还是用了板载的ADC按钮来进行选择与翻页,因此还需要对应的指针位置与状态控制,及确认与返回的控制函数。具体代码如下:

class arrow:
    def __init__(self, group) -> None:
        self.arrow_area = label.Label(terminalio.FONT, color=color, text="->")
        self.arrow_area.y = 6
        self.arrow_area.x = 8
        self.group = group

    def show(self):
        try:
            self.group.append(self.arrow_area)
        except:
            pass

    def hide(self):
        try:
            self.group.remove(self.arrow_area)
        except:
            pass

    def pos(self, index):
        self.arrow_area.y = 6 + (index * 20)


arr = arrow(group)


def updown(length, cursor, step):
    _left = btn.left()
    _right = btn.right()
    if _left == "press":
        cursor -= step
        if cursor < 0:
            cursor = 0
            return 2, cursor
        return 1, cursor
    if _right == "press":
        cursor += step
        if cursor > (length - step):
            cursor = (length - step)
            return 3, cursor
        return 1, cursor
    return 0, cursor

def choose():
    global timer
    global diff
    global middle_state
    _middle = btn.middle()
    if middle_state:
        if _middle == "press":
            if not timer:
                timer = time.monotonic()
            else:
                diff = time.monotonic() - timer
                if diff > 1:
                    timer = 0
                    middle_state = False
                    return 2    
            return 0
        else:
            if timer > 0:
                timer = 0
                return 1
            else:
                return 0   
    else:
        if _middle == "release":
            middle_state = True
        else:
            pass
        return 0

 


由于ESP32-S3-BOX-LITE使用的是ADC按键,相比普通按键略微复杂点,还需要区分长按短按,因此我写了一个单独的类用来驱动这个ADC按键,文件是/lib/litebtn.py,具体代码如下:

from analogio import AnalogIn


class LiteButton:
    def __init__(self, pin):
        self.pot = AnalogIn(pin)
        self.left_base = 48000
        self.right_base = 16000
        self.middle_base = 40000
        self.range = 4000
        self.debounce = 1
        self.left_press = 0
        self.right_press = 0
        self.middle_press = 0

    def read(self, base_val):
        _adc = self.pot.value
        if (_adc > (base_val - self.range)) and (_adc < (base_val + self.range)):
            return True
        else:
            return False

    def state(self):
        if self.read(self.left_base):
            return "left"
        elif self.read(self.right_base):
            return "right"
        elif self.read(self.middle_base):
            return "middle"
        else:
            return

    def left(self):
        if self.left_press < self.debounce:
            _state = self.read(self.left_base)
            if _state:
                self.left_press += 1
                if self.left_press < self.debounce:
                    return
                else:
                    return "press"
            else:
                return
        else:
            _state = self.read(self.left_base)
            if _state:
                return "press"
            else:
                self.left_press = 0
                return "release"

    def right(self):
        if self.right_press < self.debounce:
            _state = self.read(self.right_base)
            if _state:
                self.right_press += 1
                if self.right_press < self.debounce:
                    return
                else:
                    return "press"
            else:
                return
        else:
            _state = self.read(self.right_base)
            if _state:
                return "press"
            else:
                self.right_press = 0
                return "release"
            
    def middle(self):
        if self.middle_press < self.debounce:
            _state = self.read(self.middle_base)
            if _state:
                self.middle_press += 1
                if self.middle_press < self.debounce:
                    return
                else:
                    return "press"
            else:
                return
        else:
            _state = self.read(self.middle_base)
            if _state:
                return "press"
            else:
                self.middle_press = 0
                return "release"


至此代码讲解全部结束。

 


功能展示

具体功能展示请参考上方视频,视频里已做详细的展示。本项目可以直接作为一个完成度相对较高的在线电子书浏览器使用。

列表选择与显示:

FnJRQbxc0G8C4IVBGqf8eaJMmppN

段落显示:

Fmo0ia7eD6cCkqTalehzvCIlPkER


心得体会

自带了屏幕模块的开发板非常好用,省去了面包板接线的麻烦以及可能出现的接触不良或接错线带来的各种坑。Circuitpython也非常好用,在硬件驱动上已经替开发者完成了近乎全部工作,开发者只需要专心设计app就好。美中不足的是目前bug还比较多,我做这个项目的过程中死机了无数次。而circuitpython官网上也写目前对ESP32-S3的支持还在BETA阶段。相信未来CIRCUITPYTHON稳定后会成为主流的创客开发工具。

 

附件下载
ebook.7z
团队介绍
StreakingJerry
团队成员
StreakingJerry
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号