Funpack3-5 Beaglebone Black 网络控制LED及简易天气显示项目
该项目使用了BeagleBone Black开发板,实现了网络智能硬件系统的设计,它的主要功能为:网络控制LED及简易天气显示。
标签
嵌入式系统
Funpack活动
Python
IoT
Beaglebone
贪吃蛇
更新2025-01-20
160

一、项目介绍

基于BeagleBone BlackLinux系统,使用python编写程序,从网络源获取并解析天气信息。而后将信息通过串口转发给屏幕,实现简易的天气显示功能。同时提供一个网页服务器,对User LED2的状态进行控制,可设置为常亮、常灭和按指定频率闪烁。

二、设计思路

本次项目目标主要分为两个部分,即通过网络控制LED和显示天气信息。前者不涉及远程数据的获取,而两者共有的部分是均需要网络参与。在确定以Python+Linux作为基础环境后,可以做如下的具体划分:

    1. 显示简单的网页,用户通过HTML表单向HTTP服务器发送数据(LED的状态)。
    2. 通过sysfs接口,以文件读写方式控制LED的状态。
    3. 通过网络API获取天气数据,通常是REST请求。此操作还需要定时完成。
    4. 读写串口,与显示屏通信,显示数据。

三、硬件介绍

BeagleBone Black是一款由Texas Instruments设计的开发板。其SoC芯片选用基于ARM Cortex-A8处理器的TI AM3358,具有丰富的外设接口和强大的计算能力。板上配备512MBDDR3 SDRAM800MHz),对嵌入式应用而言已经非常丰富,可以运行LinuxRTOS裸机系统。TI AM3358还内置了3D图形处理器、2×32-bit 200-MHz PRU IO协处理器和CAN总线控制器。

BeagleBone Black开发板使用microSD卡作为存储介质,并额外板载4GBeMMC。可通过板载轻触开关选择从二者之一启动。另外,其配备了一个USB Type-A主机接口和一个USB mini设备接口。两接口均支持USB2.0协议,并向前兼容USB1.0/1.1。主机接口支持符合USB2.0标准的最高500mA外供电。

网络方面,BeagleBone Black开发板带有一个RJ45网络接口,支持百兆以太网。BeagleBone Black开发板并没有配备任何无线通信模块,例如Wifi或蓝牙等。它的同系列产品BeagleBone Black Wireless带有Wifi和蓝牙,但可能由于开发板空间不足,取消了RJ45。查阅规格书可知,AM3358提供了千兆以太网的MAC控制器,但开发板并没有配备对应的PHY,所以板上只提供百兆以太网。

BeagleBone Black开发板引出了SoCJTAG接口用于调试,并提供了单独的UART0排母供基础调试和控制台访问使用。TI提供的Debian镜像默认会在UART0上开启tty,对于首次配置开发板非常重要。

BeagleBone Black开发板提供了HDMI接口以输出视频信号。立体声音频信号同样从HDMI接口引出,并未提供单独的音频接口。也提供了LCD接口以驱动无控制器的外接屏幕。

作为嵌入式开发板,BeagleBone Black提供了常规的嵌入式外设,例如UARTADC, I2C, SPI, PWM等。其中,UART接口也支持IrDA的信号处理。SoC的大部分引脚通过分布在开发板两侧的两组双排2.54mm排母引出,可以作GPIO扩展之用。

HMI屏幕选用尚视界HF024。这款屏幕通过串口与控制器通信,具有240x320QVGA分辨率。通过使用上位机软件制作和烧录工程,可以预置文本、图像等控件,方便显示丰富的信息。板载Flash存储,最高支持20张图片和16个控件页面。

四、软件架构

1. 使用python编写网页服务器和LED控制器。

2. 使用python编写天气API请求工具,以及对应的解析和串口通信组件。

3. 配置系统启用串口UART4

 

五、硬件连接

使用自制连接线连接BeagleBone BlackUART4HMI屏幕,如下图所示。

六、软件代码

分别为主Python代码与HMI屏幕工程文件(XML格式)。另有config.py中配置城市与API Token,故未上传。

程序流程图如下:

import http.server
import urllib.parse
import urllib.request
import threading
import config
import json
import time
import datetime
import serial


HTML_BODY = """<html><head>
<link rel="shortcut icon" href="#" />
<meta charset="UTF-8">
<title>Simple LED Controller</title>
</head><body>
<div><form action="" method="post">
    <div><input type=submit name="act" value="常亮"/></div>
    <div><input type=submit name="act" value="常灭"/></div>
    <div><input type=text name="freq" value="{}"/>Hz&nbsp;<input type=submit name="act" value="闪烁"/></div>
</div>        
</body></html>"""


interval = "on"
freq = "1.0"



class Worker(threading.Thread):
    def run(self):
        global interval
        last_state, last_interval = True, None
        last_report, blinked = -1, 0
        with open("/sys/class/leds/beaglebone:green:usr1/trigger", "w") as f:
            f.write("none")


        while True:
            with open("/sys/class/leds/beaglebone:green:usr1/brightness", "w") as f:
                if interval == "on":
                    f.write("255")
                    time.sleep(0.1)
                    if last_interval != interval:
                        print("LED switched on")
                elif interval == "off":
                    f.write("0")
                    time.sleep(0.1)
                    if last_interval != interval:
                        print("LED switched off")
                else:
                    if last_state:
                        f.write("1")
                    else:
                        f.write("0")
                    last_state = not last_state
                    blinked += 1
                    if time.time() - last_report > 5:
                        print(
                            f"Blinked {blinked} times in last {time.time()-last_report:.0f} seconds"
                        )
                        blinked = 0
                        last_report = time.time()
                    time.sleep(interval)
                last_interval = interval



class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/":
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(HTML_BODY.format(freq).encode("utf8"))
        else:
            self.send_response(404)


    def do_POST(self):
        if self.headers.get("content-type", "") != "application/x-www-form-urlencoded":
            self.send_response(500)
        else:
            content_length = int(self.headers.get("content-length", 0))
            body = urllib.parse.parse_qs(
                self.rfile.read(content_length).decode("utf8"), keep_blank_values=True
            )
            global interval, freq
            if body.get("act", None) == ["常亮"]:
                interval = "on"
            elif body.get("act", None) == ["常灭"]:
                interval = "off"
            elif body.get("act", None) == ["闪烁"]:
                try:
                    interval = 1 / float(body.get("freq", ["1"])[0])
                    freq = body.get("freq", ["1"])[0]
                except:
                    interval = 1.0


            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(HTML_BODY.format(freq).encode("utf8"))



def run_http(
    server_class=http.server.HTTPServer,
    handler_class=Handler,
):
    server_address = ("", 8013)
    httpd = server_class(server_address, handler_class)
    httpd.serve_forever()



class WeatherWorker(threading.Thread):
    def update(self):
        try:
            url = f"https://restapi.amap.com/v3/weather/weatherInfo?key={config.TOKEN}&city={config.CITY}&extensions=all&output=json"
            req = urllib.request.Request(url)
            req.method = "GET"
            res = urllib.request.urlopen(req)
            data = json.load(res)
            with open("data.json", "w", encoding="utf8") as f:
                json.dump(data, f)
            if data["status"] == "1" and data["info"] == "OK":
                data2 = data["forecasts"][0]
                return data2["city"], data2["casts"], data2["reporttime"]
            return None, None, None
        except:
            return None, None, None


    def run(self):
        # UTC+8
        timezone = datetime.timezone(datetime.timedelta(hours=8))
        last_update, last_ind = -1, 0
        comm = serial.Serial("/dev/ttyS4", 115200)
        time.sleep(2)
        comm.write(b"RESET();\r\n")
        time.sleep(2)
        mapper = {
            "晴": 1,
            "多云": 2,
            "阴": 3,
            "小雨": 4,
            "中雨": 5,
            "大雨": 6,
            "小雪": 7,
            "中雪": 8,
            "大雪": 9,
            "冰雹": 10,
        }
        while True:
            if time.time() - last_update > 3600:
                print("Updating...")
                city, casts, rtime = self.update()
                last_update = time.time()
                print(f"Fetched data of {city}")
            if city is None or casts is None:
                last_update = time.time() - 3300
                # comm.write(b'JUMP(0);\r\n')
                pass
            else:
                last_ind += 1
                if last_ind >= len(casts):
                    last_ind = 0
                d = casts[last_ind]
                date, dw, nw, tempe, dwnd, nwnd = (
                    d["date"],
                    d["dayweather"],
                    d["nightweather"],
                    f"{d['nighttemp']}~{d['daytemp']}℃",
                    f"{d['daywind']} {d['daypower']}",
                    f"{d['nightwind']} {d['nightpower']}",
                )
                daytime = 6 <= datetime.datetime.now(timezone).hour < 18
                print(f"Updating page {last_ind}")
                comm.write(
                    "".join(
                        [
                            "JUMP(1);"
                            f"SET_BTN_IMG(0,0,{mapper[dw if daytime else nw]});",
                            f"SET_TXT(1,'{city}');",
                            f"SET_TXT(2,'{dw}');",
                            f"SET_TXT(3,'{dwnd}');",
                            f"SET_TXT(4,'{nw}');",
                            f"SET_TXT(5,'{nwnd}');",
                            f"SET_TXT(6,'{tempe}');",
                            f"SET_TXT(7,'{date}')",
                            "\r\n",
                        ]
                    ).encode("gbk", "ignore")
                )
            time.sleep(5)



if __name__ == "__main__":
    worker = Worker()
    worker.start()
    ww = WeatherWorker()
    ww.start()
    run_http()
<?xml version="1.0" encoding="UTF-8"?>
<ui>
    <frame frame="frame">
        <frameW>320</frameW>
        <frameH>240</frameH>
        <parentImageNum>-1</parentImageNum>
        <parentBackgroundNum>15</parentBackgroundNum>
        <screenType>1</screenType>
        <dpi>240*320(QVGA-1)</dpi>
        <baud>115200</baud>
        <blValue>187</blValue>
        <logo1>0</logo1>
        <logo2>0</logo2>
        <logox>0</logox>
        <logoy>0</logoy>
        <logow>0</logow>
        <logoh>0</logoh>
        <logotime>0</logotime>
        <currentPage>1</currentPage>
    </frame>
    <page id="1">
        <pageImageNum pageImageNum="0"/>
        <pageBackgroundNum pageBackgroundNum="15"/>
        <pageHelpLineNum pageHelpLineNum="0"/>
    </page>
    <page id="2">
        <pageImageNum pageImageNum="-1"/>
        <pageBackgroundNum pageBackgroundNum="15"/>
        <pageHelpLineNum pageHelpLineNum="0"/>
        <ClassName ClassName="QPushButton">
            <id>0</id>
            <x>8</x>
            <y>20</y>
            <w>200</w>
            <h>197</h>
            <text></text>
            <fontSize>16</fontSize>
            <hAlign>0</hAlign>
            <vAlign>0</vAlign>
            <fontColorNum>0</fontColorNum>
            <backgroundColorNum>7</backgroundColorNum>
            <pressColorNum>0</pressColorNum>
            <imageNum>1</imageNum>
            <cutImageNum>-1</cutImageNum>
            <backgroundImageNum>-1</backgroundImageNum>
            <backgroundCutImageNum>-1</backgroundCutImageNum>
            <pressImageNum>1</pressImageNum>
            <pressCutImageNum>-1</pressCutImageNum>
            <progType>0</progType>
            <style>1</style>
            <styleColorNum>0</styleColorNum>
            <pointerX>0</pointerX>
            <pointerY>0</pointerY>
            <pointerW>0</pointerW>
            <pointerL>0</pointerL>
            <pointerAngle>0</pointerAngle>
        </ClassName>
        <ClassName ClassName="QLabel">
            <id>1</id>
            <x>212</x>
            <y>8</y>
            <w>100</w>
            <h>30</h>
            <text>位置</text>
            <fontSize>24</fontSize>
            <hAlign>2</hAlign>
            <vAlign>0</vAlign>
            <fontColorNum>0</fontColorNum>
            <backgroundColorNum>7</backgroundColorNum>
            <pressColorNum>0</pressColorNum>
            <imageNum>-1</imageNum>
            <cutImageNum>-1</cutImageNum>
            <backgroundImageNum>-1</backgroundImageNum>
            <backgroundCutImageNum>-1</backgroundCutImageNum>
            <pressImageNum>-1</pressImageNum>
            <pressCutImageNum>-1</pressCutImageNum>
            <progType>0</progType>
            <style>0</style>
            <styleColorNum>0</styleColorNum>
            <pointerX>0</pointerX>
            <pointerY>0</pointerY>
            <pointerW>0</pointerW>
            <pointerL>0</pointerL>
            <pointerAngle>0</pointerAngle>
        </ClassName>
        <ClassName ClassName="QLabel">
            <id>2</id>
            <x>212</x>
            <y>40</y>
            <w>100</w>
            <h>30</h>
            <text>日间天气</text>
            <fontSize>16</fontSize>
            <hAlign>2</hAlign>
            <vAlign>0</vAlign>
            <fontColorNum>0</fontColorNum>
            <backgroundColorNum>7</backgroundColorNum>
            <pressColorNum>0</pressColorNum>
            <imageNum>-1</imageNum>
            <cutImageNum>-1</cutImageNum>
            <backgroundImageNum>-1</backgroundImageNum>
            <backgroundCutImageNum>-1</backgroundCutImageNum>
            <pressImageNum>-1</pressImageNum>
            <pressCutImageNum>-1</pressCutImageNum>
            <progType>0</progType>
            <style>0</style>
            <styleColorNum>0</styleColorNum>
            <pointerX>0</pointerX>
            <pointerY>0</pointerY>
            <pointerW>0</pointerW>
            <pointerL>0</pointerL>
            <pointerAngle>0</pointerAngle>
        </ClassName>
        <ClassName ClassName="QLabel">
            <id>3</id>
            <x>212</x>
            <y>72</y>
            <w>100</w>
            <h>30</h>
            <text>日间风向</text>
            <fontSize>16</fontSize>
            <hAlign>2</hAlign>
            <vAlign>0</vAlign>
            <fontColorNum>0</fontColorNum>
            <backgroundColorNum>7</backgroundColorNum>
            <pressColorNum>0</pressColorNum>
            <imageNum>-1</imageNum>
            <cutImageNum>-1</cutImageNum>
            <backgroundImageNum>-1</backgroundImageNum>
            <backgroundCutImageNum>-1</backgroundCutImageNum>
            <pressImageNum>-1</pressImageNum>
            <pressCutImageNum>-1</pressCutImageNum>
            <progType>0</progType>
            <style>0</style>
            <styleColorNum>0</styleColorNum>
            <pointerX>0</pointerX>
            <pointerY>0</pointerY>
            <pointerW>0</pointerW>
            <pointerL>0</pointerL>
            <pointerAngle>0</pointerAngle>
        </ClassName>
        <ClassName ClassName="QLabel">
            <id>4</id>
            <x>212</x>
            <y>104</y>
            <w>100</w>
            <h>30</h>
            <text>夜间天气</text>
            <fontSize>16</fontSize>
            <hAlign>2</hAlign>
            <vAlign>0</vAlign>
            <fontColorNum>0</fontColorNum>
            <backgroundColorNum>7</backgroundColorNum>
            <pressColorNum>0</pressColorNum>
            <imageNum>-1</imageNum>
            <cutImageNum>-1</cutImageNum>
            <backgroundImageNum>-1</backgroundImageNum>
            <backgroundCutImageNum>-1</backgroundCutImageNum>
            <pressImageNum>-1</pressImageNum>
            <pressCutImageNum>-1</pressCutImageNum>
            <progType>0</progType>
            <style>0</style>
            <styleColorNum>0</styleColorNum>
            <pointerX>0</pointerX>
            <pointerY>0</pointerY>
            <pointerW>0</pointerW>
            <pointerL>0</pointerL>
            <pointerAngle>0</pointerAngle>
        </ClassName>
        <ClassName ClassName="QLabel">
            <id>5</id>
            <x>212</x>
            <y>136</y>
            <w>100</w>
            <h>30</h>
            <text>夜间风向</text>
            <fontSize>16</fontSize>
            <hAlign>2</hAlign>
            <vAlign>0</vAlign>
            <fontColorNum>0</fontColorNum>
            <backgroundColorNum>7</backgroundColorNum>
            <pressColorNum>0</pressColorNum>
            <imageNum>-1</imageNum>
            <cutImageNum>-1</cutImageNum>
            <backgroundImageNum>-1</backgroundImageNum>
            <backgroundCutImageNum>-1</backgroundCutImageNum>
            <pressImageNum>-1</pressImageNum>
            <pressCutImageNum>-1</pressCutImageNum>
            <progType>0</progType>
            <style>0</style>
            <styleColorNum>0</styleColorNum>
            <pointerX>0</pointerX>
            <pointerY>0</pointerY>
            <pointerW>0</pointerW>
            <pointerL>0</pointerL>
            <pointerAngle>0</pointerAngle>
        </ClassName>
        <ClassName ClassName="QLabel">
            <id>6</id>
            <x>212</x>
            <y>168</y>
            <w>100</w>
            <h>30</h>
            <text>气温</text>
            <fontSize>16</fontSize>
            <hAlign>2</hAlign>
            <vAlign>0</vAlign>
            <fontColorNum>0</fontColorNum>
            <backgroundColorNum>7</backgroundColorNum>
            <pressColorNum>0</pressColorNum>
            <imageNum>-1</imageNum>
            <cutImageNum>-1</cutImageNum>
            <backgroundImageNum>-1</backgroundImageNum>
            <backgroundCutImageNum>-1</backgroundCutImageNum>
            <pressImageNum>-1</pressImageNum>
            <pressCutImageNum>-1</pressCutImageNum>
            <progType>0</progType>
            <style>0</style>
            <styleColorNum>0</styleColorNum>
            <pointerX>0</pointerX>
            <pointerY>0</pointerY>
            <pointerW>0</pointerW>
            <pointerL>0</pointerL>
            <pointerAngle>0</pointerAngle>
        </ClassName>
        <ClassName ClassName="QLabel">
            <id>7</id>
            <x>212</x>
            <y>200</y>
            <w>100</w>
            <h>30</h>
            <text>日期</text>
            <fontSize>16</fontSize>
            <hAlign>0</hAlign>
            <vAlign>0</vAlign>
            <fontColorNum>0</fontColorNum>
            <backgroundColorNum>7</backgroundColorNum>
            <pressColorNum>0</pressColorNum>
            <imageNum>-1</imageNum>
            <cutImageNum>-1</cutImageNum>
            <backgroundImageNum>-1</backgroundImageNum>
            <backgroundCutImageNum>-1</backgroundCutImageNum>
            <pressImageNum>-1</pressImageNum>
            <pressCutImageNum>-1</pressCutImageNum>
            <progType>0</progType>
            <style>0</style>
            <styleColorNum>0</styleColorNum>
            <pointerX>0</pointerX>
            <pointerY>0</pointerY>
            <pointerW>0</pointerW>
            <pointerL>0</pointerL>
            <pointerAngle>0</pointerAngle>
        </ClassName>
    </page>
</ui>

七、实际演示

启动LOGO:

天气页:

网页LED控制台(比较简陋)

八、总结与展望

本次项目通过引入外部组件(HMI屏),降低了开发复杂度,也得益于python语言的高表达能力,使得在BeagleBone Black上运行单一python文件就可以实现两个主要功能。总体而言,这块屏幕分担了一些图形处理的工作,但经过实际测试,其性能并不够强大,刷新时无缓冲,从而使得多个刷新元素依次出现,对用户体验有所影响。未来可以考虑改为直接使用BeagleBone Black集成的LCD控制器驱动裸屏,以充分利用开发板的性能,并更好地整合软硬件。



附件下载
daemon.py
weather.sGUI
logo.png
团队介绍
个人业余独立开发
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号