一、项目介绍
基于BeagleBone Black的Linux系统,使用python编写程序,从网络源获取并解析天气信息。而后将信息通过串口转发给屏幕,实现简易的天气显示功能。同时提供一个网页服务器,对User LED2的状态进行控制,可设置为常亮、常灭和按指定频率闪烁。
二、设计思路
本次项目目标主要分为两个部分,即通过网络控制LED和显示天气信息。前者不涉及远程数据的获取,而两者共有的部分是均需要网络参与。在确定以Python+Linux作为基础环境后,可以做如下的具体划分:
- 显示简单的网页,用户通过HTML表单向HTTP服务器发送数据(LED的状态)。
- 通过sysfs接口,以文件读写方式控制LED的状态。
- 通过网络API获取天气数据,通常是REST请求。此操作还需要定时完成。
- 读写串口,与显示屏通信,显示数据。
三、硬件介绍
BeagleBone Black是一款由Texas Instruments设计的开发板。其SoC芯片选用基于ARM Cortex-A8处理器的TI AM3358,具有丰富的外设接口和强大的计算能力。板上配备512MB的DDR3 SDRAM(800MHz),对嵌入式应用而言已经非常丰富,可以运行Linux或RTOS裸机系统。TI AM3358还内置了3D图形处理器、2×32-bit 200-MHz PRU IO协处理器和CAN总线控制器。
BeagleBone Black开发板使用microSD卡作为存储介质,并额外板载4GB的eMMC。可通过板载轻触开关选择从二者之一启动。另外,其配备了一个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开发板引出了SoC的JTAG接口用于调试,并提供了单独的UART0排母供基础调试和控制台访问使用。TI提供的Debian镜像默认会在UART0上开启tty,对于首次配置开发板非常重要。
BeagleBone Black开发板提供了HDMI接口以输出视频信号。立体声音频信号同样从HDMI接口引出,并未提供单独的音频接口。也提供了LCD接口以驱动无控制器的外接屏幕。
作为嵌入式开发板,BeagleBone Black提供了常规的嵌入式外设,例如UART,ADC, I2C, SPI, PWM等。其中,UART接口也支持IrDA的信号处理。SoC的大部分引脚通过分布在开发板两侧的两组双排2.54mm排母引出,可以作GPIO扩展之用。
HMI屏幕选用尚视界HF024。这款屏幕通过串口与控制器通信,具有240x320的QVGA分辨率。通过使用上位机软件制作和烧录工程,可以预置文本、图像等控件,方便显示丰富的信息。板载Flash存储,最高支持20张图片和16个控件页面。
四、软件架构
1. 使用python编写网页服务器和LED控制器。
2. 使用python编写天气API请求工具,以及对应的解析和串口通信组件。
3. 配置系统启用串口UART4。
五、硬件连接
使用自制连接线连接BeagleBone Black的UART4与HMI屏幕,如下图所示。
六、软件代码
分别为主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 <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控制器驱动裸屏,以充分利用开发板的性能,并更好地整合软硬件。