一、项目简介:
这个项目,基于行空板K10核micropython环境,从天气网站提取信息,并仿照天气网站的布局,制作了一个天气牌,并提供了完整的源码(往后看),具体界面和效果如下:
二、硬件清单:
材料清单
- 行空板 K10 MicroPython编程教学/学习主控板(AI教学) X1 链接
三、天气网站:
这个天气牌,参考的天气网站为:https://zte.weatherol.com/
当然,也有很多提供天气信息API的站点,可以直接通过API接口获取天气信息。
这个项目中,直接从原始页面中获取天气信息,所以也展示了一些python语言的用法。
如果查看该页面的源代码,则可以在源代码中,看到基本天气信息的数据:
因此,只需要在程序中,获取backstageData对应的数据,就可以得到天气数据了。
另外,还需要分析一些辅助信息,以便获取或者计算最终需要的数据。
空气质量信息:zte.weatherol.com/getAqiforecast24h?id=101010100
降雨信息:zte.weatherol.com/getprecipitationByid?type=forecast&id=101010100
生活指数:zte.weatherol.com/getCurrentIndexDate?stationid=101010100
上述4项信息,第一项需要从页面源码中提取,而后三项,直接请求对应的地址即可获得需要的JSON格式的数据。
其对应的地址为:
天气信息网址:https://zte.weatherol.com/home.html?id=城市编号
空气质量网址:https://zte.weatherol.com/getAqiforecast24h?id=城市编号
降雨信息网址:https://zte.weatherol.com/getprecipitationByid?type=forecast&id=城市编号
生活指数网址:https://zte.weatherol.com/getCurrentIndexDate?stationid=城市编号
城市编号,可以在 zte.weatherol.com/city.html 页面,选取对应的城市后,从网址中获取即可:
四、天气信息处理代码
这个天气牌的程序,核心代码有两个部分,一个是天气信息的提取,一个是界面的显示。
天气信息提取部分,涉及到网络请求和文本内容分析提取。
首先,需要联网,对应的调用代码如下:
import network
# 联网处理
def do_connect(ssid, passwd):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, passwd)
while not wlan.isconnected():
print("try connect...")
time.sleep(1)
# 联网成功
print("connect ok")
do_connect("WiFi热点名称", ""WiFi连接密码)
然后,是网络请求,使用urequests库来实现。
对应的代码如下:
import urequests as requests
city_id = '101010100'
home_url = 'https://zte.weatherol.com/home.html?id='
aqi_url = 'https://zte.weatherol.com/getAqiforecast24h?id='
rain_url = 'https://zte.weatherol.com/getprecipitationByid?type=forecast&id='
life_url = 'https://zte.weatherol.com/getCurrentIndexDate?stationid='
# header字符串
header_string = '''
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: identity
Accept-Language: zh,en-US
Cache-Control: no-cache
Connection: keep-alive
Host: zte.weatherol.com
Pragma: no-cache
Referer: https://zte.weatherol.com/
User-Agent: Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36 Edg/127.0.0.0
'''
# 构建header信息
header_dict = {}
for line in header_string.split('\n'):
if line.strip() == '':
continue
key, value = line.split(': ', 1)
header_dict[key] = value
def get_weather(city_id):
# 发起请求
url = f'{home_url}{city_id}'
print("request url:", url)
response = requests.get(url, headers=header_dict)
# print(response.text)
# 未完待续
通过网络获取到页面的内容后,再进行信息的提取。
从文本文件中提取信息,如果是电脑或者行空板M10上的Python环境中,可以使用正则,但在micropython中,用不上,所以只用最基础的字符串处理调用来实现。
对应的代码如下:
import urequests as requests
import json
# 提取信息
def extract_data(start, end, text):
pos1 = text.find(start) + len(start)
pos2 = text.find(end)
return text[pos1:pos2].strip()
def get_weather(city_id):
# 发起请求
url = f'{home_url}{city_id}'
print("request url:", url)
response = requests.get(url, headers=header_dict)
# print(response.text)
# 处理信息
print("process text:")
# response.text为多行文本,从其中var backstageData=到var loadingtime之间,提取文本
# 预处理
response_text = response.text
while ' =' in response_text or '= ' in response_text:
response_text = response_text.replace(' =', '=').replace('= ', '=')
city_name = extract_data('<span id="cityname">', '</span>', response_text)
weather_info = extract_data('var backstageData=', 'var loadingtime', response_text)
# 修复json格式数据
weather_info_lines = weather_info.split('\n')
for i, line in enumerate(weather_info_lines):
if line.startswith(','):
weather_info_lines[i] = ',\n'+weather_info_lines[i].split(',',1)[1]
if line.startswith('{'):
weather_info_lines[i] = '{\n'+weather_info_lines[i].split('{',1)[1]
weather_info = "\n".join(weather_info_lines)
weather_info_lines = weather_info.split('\n')
for i, line in enumerate(weather_info_lines):
if ':' in weather_info_lines[i]:
weather_info_lines[i] = '"' + weather_info_lines[i].split(':',1)[0] + '" : ' + weather_info_lines[i].split(':',1)[1]
weather_info = "\n".join(weather_info_lines)
# 解析json数据
backstageData = json.loads(weather_info)
# 未完待续
通过上面的处理,就能得到基础的天气信息,提取到 backstageData 中,便于后续代码调用了。
而后续的三项天气信息数据,则直接请求并作为json解析即可。
对应代码如下:
# 发起请求
url = f'{aqi_url}{city_id}'
print("request url:", url)
response = requests.get(url, headers=header_dict)
#print(response.text)
airData = json.loads(response.text)
#print('airData:', json.dumps(airData))
# 发起请求
url = f'{rain_url}{city_id}'
print("request url:", url)
response = requests.get(url, headers=header_dict)
#print(response.text)
rainData = json.loads(response.text)
#print('rainData:', json.dumps(rainData))
# 发起请求
url = f'{life_url}{city_id}'
print("request url:", url)
response = requests.get(url, headers=header_dict)
#print(response.text)
lifeData = json.loads(response.text)
#print('rainData:', json.dumps(rainData))
获取到所有的天气信息之后,进过处理,最终生成需要的数据结构。
showData = {
'city_id': city_id,
# 左上
'城市名称': city_name,
'城市图片': 'liveweather/dingwei.png',
# 右上
'报告时间': get_date(backstageData['reporttime']),
# 左中
'温度': backstageData['temperature'],
'天气图片': f"dayweather/{backstageData['weatherPic']}.png",
'天气类型': backstageData['weatherIndex'],
'温度范围': "%s°C" % backstageData['lhtemp'],
# 右中
'风向': backstageData['windDirDecoder'],
'风力': backstageData['windPowerDecoder'],
'空气质量': "%s%s" %('空气' if len(get_aqi_word(airData[0]['value'])) <= 2 else '', get_aqi_word(airData[0]['value'])),
'天气预警': backstageData['warninglist'][0]['title'] if len(backstageData['warninglist']) else '',
'天气预警图片': backstageData['warninglist'][0]['img'] if len(backstageData['warninglist']) else '',
# 底部1排
'列表': {
"湿度" : liveData_dict['湿度'],
"大气压" : liveData_dict['大气压'],
"能见度" : liveData_dict['能见度'],
"紫外线强度" : liveData_dict['紫外线强度'],
# 底部2排
"体感温度" : liveData_dict['体感温度'],
"风力" : liveData_dict['风力'],
"风向": liveData_dict['风向'],
"降水量" : liveData_dict['降水量'],
},
'生活指数': lifeData
}
注意,上面的代码,仅对关键部分做了解析,完整的内容,请查看附件。
五、天气牌显示处理代码
在行空板K10的micropython中,要点亮屏幕显示内容,可以参考我之前的一片帖子:在micropython中使用nano-gui点亮K10屏幕显示温度曲线 DF创客社区 (dfrobot.com.cn)
【特别说明:请使用本文结尾的micropython版本】
前面已经获取了需要的天气信息,那么现在,只需要在屏幕上,按照布局,显示对应的内容即可。
首先,在屏幕上显示出原始的内容,确定各个元素的坐标位置:
# 字体文件调用
import gui.fonts.freesans20 as font20
import gui.fonts.font6 as font6
import gui.fonts.freesans_6_char as font6_char
import gui.fonts.puhui_m_12_all as font_cn
import gui.fonts.puhui_m_32_char as font_char
# 屏幕元素
CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it
wri = CWriter(ssd, font_cn, WHITE, BG_COLOR) # Report on fast mode. Or use verbose=False
wri.set_clip(True, True, False)
wri1 = CWriter(ssd, font_cn, GREEN, BG_COLOR) # Report on fast mode. Or use verbose=False
wri1.set_clip(True, True, False)
wri2 = CWriter(ssd, font6_char, GREEN, BG_COLOR) # Report on fast mode. Or use verbose=False
wri2.set_clip(True, True, False)
wri3 = CWriter(ssd, font_char, RED, BG_COLOR) # Report on fast mode. Or use verbose=False
wri3.set_clip(True, True, False)
# 温度
lbs = {}
lbs['城市图片'] = Label(wri, 5, 5, " ")
lbs['城市名称'] = Label(wri, 5, 15, "城市")
lbs['报告时间'] = (
Label(wri, 5, 80, "某月某日"),
Label(wri, 5, 140, "星期几"),
Label(wri, 5, 180, "00:00:00"),
Label(wri, 25, 180, "00:00发布")
)
y1 = 25
ssd.rect(5, y1-3, 110, y1 + 35, YELLOW)
lbs['温度B'] = Label(wri3, y1+10, 15, " ")
lbs['温度'] = Label(wri3, y1+10, 25, "?")
lbs['温度单位'] = Label(wri2, y1, 55, "°C")
#lbs['天气图标'] = Label(wri, y1+20, 55, "晴")
lbs['天气类型'] = Label(wri, y1+20, 55, "*")
lbs['温度范围'] = Label(wri2, y1+40, 55, "低~高°C")
lbs['风向'] = Label(wri, y1+20, 150, "风向")
lbs['风力'] = Label(wri, y1+20, 200, "风力")
lbs['空气质量'] = Label(wri, y1+40, 180, "空气质量")
try:
temperature = aht.temperature + TEMPERATURE_DIFF
relative_humidity = aht.relative_humidity + RELATIVE_HUMIDITY_DIFF
except Exception as e:
print(e)
temperature = 0
relative_humidity = 0
y1 = 90
lbs['环境温度'] = (
Label(wri, y1, 10, "环境温度:"),
Label(wri2, y1, 70, "%d°C" % temperature)
)
lbs['环境湿度'] = (
Label(wri, y1, 120, "环境湿度:"),
Label(wri2, y1, 180, "%d%%" % relative_humidity)
)
lbs['列表'] = {}
y1 = 120
y2 = 136
ssd.line(0, y1 - 5, ssd.width - 1, y1 - 5, YELLOW)
lbs['列表']['湿度'] = (
Label(wri, y2, 10, "湿度"),
Label(wri2, y1, 10, "%d%%" % relative_humidity)
)
lbs['列表']['大气压'] = (
Label(wri, y2, 65, "大气压"),
Label(wri2, y1, 65-15, "****hPa")
)
lbs['列表']['能见度'] = (
Label(wri, y2, 120, "能见度"),
Label(wri2, y1, 120, "**.*KM")
)
lbs['列表']['紫外线强度'] = (
Label(wri, y2, 175, "紫外线"),
Label(wri, y2+12, 175, "强度"),
Label(wri1, y1, 185, "**")
)
y1 = 170-5
y2 = 186-5
lbs['列表']['体感温度'] = (
Label(wri, y2, 10, "体感", align=ALIGN_CENTER),
Label(wri, y2+12, 10, "温度", align=ALIGN_CENTER),
Label(wri2, y1, 10, "**°C", align=ALIGN_CENTER)
)
lbs['列表']['风力'] = (
Label(wri, y2, 65, "风力", align=ALIGN_CENTER),
Label(wri1, y1, 65, "*级", align=ALIGN_CENTER)
)
lbs['列表']['风向'] = (
Label(wri, y2, 120, "风向", align=ALIGN_CENTER),
Label(wri1, y1, 120, "**风", align=ALIGN_CENTER)
)
lbs['列表']['降水量'] = (
Label(wri, y2, 175, "降水量", align=ALIGN_CENTER),
Label(wri2, y1, 175, "*.**mm", align=ALIGN_CENTER)
)
lbs['生活指数'] = {}
y1 = 220
y2 = 236
ssd.line(0, y1 - 5, ssd.width - 1, y1 - 5, YELLOW)
lbs['生活指数']['限行指数'] = (
Label(wri, y2, 10, "限行", align=ALIGN_CENTER),
Label(wri, y2+12, 10, "尾号", align=ALIGN_CENTER),
Label(wri1, y1, 10, "* *", align=ALIGN_CENTER)
)
lbs['生活指数']['穿衣指数'] = (
Label(wri, y2, 65, "穿衣", align=ALIGN_CENTER),
Label(wri, y2+12, 65, "指数", align=ALIGN_CENTER),
Label(wri1, y1, 65, "*", align=ALIGN_CENTER)
)
lbs['生活指数']['紫外线强度指数'] = (
Label(wri, y2, 120, "紫外线", align=ALIGN_CENTER),
Label(wri, y2+12, 120, "强度", align=ALIGN_CENTER),
Label(wri1, y1, 120, "*", align=ALIGN_CENTER)
)
lbs['生活指数']['舒适度指数'] = (
Label(wri, y2, 175, "舒适度", align=ALIGN_CENTER),
Label(wri, y2+12, 175, "指数", align=ALIGN_CENTER),
Label(wri1, y1, 175, "****", align=ALIGN_CENTER)
)
y1 = 270
y2 = 286
lbs['生活指数']['洗车指数'] = (
Label(wri, y2, 10, "洗车", align=ALIGN_CENTER),
Label(wri, y2+12, 10, "指数", align=ALIGN_CENTER),
Label(wri1, y1, 10, "***", align=ALIGN_CENTER)
)
lbs['生活指数']['运动指数'] = (
Label(wri, y2, 65, "运动", align=ALIGN_CENTER),
Label(wri, y2+12, 65, "指数", align=ALIGN_CENTER),
Label(wri1, y1, 65, "***", align=ALIGN_CENTER)
)
lbs['生活指数']['感冒指数'] = (
Label(wri, y2, 120, "感冒", align=ALIGN_CENTER),
Label(wri, y2+12, 120, "指数", align=ALIGN_CENTER),
Label(wri1, y1, 120, "***", align=ALIGN_CENTER)
)
lbs['生活指数']['晾晒指数'] = (
Label(wri, y2, 175, "晾晒", align=ALIGN_CENTER),
Label(wri, y2+12, 175, "指数", align=ALIGN_CENTER),
Label(wri1, y1, 175, "****", align=ALIGN_CENTER)
)
refresh(ssd)
上面的代码,将会显示一个基础的界面:
然后,再根据前面取得的天气信息的数据,依次显示更新即可:
print("请求最新天气信息:")
try:
showData = get_weather(city_id)
print(json.dumps(showData))
#lbs['城市图片'].value(showData['城市图片'])
lbs['城市名称'].value(showData['城市名称'])
lbs['报告时间'][0].value(showData['报告时间'][0])
lbs['报告时间'][1].value(showData['报告时间'][1])
#lbs['报告时间'][2].value(f'{"%02d" % current_time[3]}:{"%02d" % current_time[4]}:{"%02d" % current_time[5]}')
lbs['报告时间'][3].value("%s发布" % showData['报告时间'][2])
temperature = int(showData['温度'])
lbs['温度B'].value('-' if temperature < 0 else '')
lbs['温度'].value(str(-temperature if temperature < 0 else temperature))
#lbs['温度单位'].value("°C")
#lbs['天气图标'].value(showData['天气图标'])
lbs['天气类型'].value(showData['天气类型'])
lbs['温度范围'].value(showData['温度范围'])
lbs['风向'].value(showData['风向'])
lbs['风力'].value(showData['风力'])
lbs['空气质量'].value(showData['空气质量'])
try:
temperature = aht.temperature + TEMPERATURE_DIFF
relative_humidity = aht.relative_humidity + RELATIVE_HUMIDITY_DIFF
except Exception as e:
print(e)
temperature = 0
relative_humidity = 0
lbs['环境温度'][-1].value("%d°C" % temperature)
lbs['环境湿度'][-1].value("%d%%" % relative_humidity)
lbs['列表']['湿度'][-1].value("%s%s" % (showData['列表']['湿度']['value'],showData['列表']['湿度']['unit']))
lbs['列表']['大气压'][-1].value("%s%s" % (showData['列表']['大气压']['value'],showData['列表']['大气压']['unit']))
lbs['列表']['能见度'][-1].value("%s%s" % (showData['列表']['能见度']['value'],showData['列表']['能见度']['unit']))
lbs['列表']['紫外线强度'][-1].value("%s%s" % (showData['列表']['紫外线强度']['value'],showData['列表']['紫外线强度']['unit']))
lbs['列表']['体感温度'][-1].value("%s%s" % (showData['列表']['体感温度']['value'],showData['列表']['体感温度']['unit']))
lbs['列表']['风力'][-1].value("%s%s" % (showData['列表']['风力']['value'],showData['列表']['风力']['unit']))
lbs['列表']['风向'][-1].value("%s%s" % (showData['列表']['风向']['value'],showData['列表']['风向']['unit']))
lbs['列表']['降水量'][-1].value("%0.2f%s" % (showData['列表']['降水量']['value'],showData['列表']['降水量']['unit']))
lbs['生活指数']['限行指数'][-1].value(showData['生活指数'][0]['index_level'])
lbs['生活指数']['穿衣指数'][-1].value(showData['生活指数'][1]['index_level'])
lbs['生活指数']['紫外线强度指数'][-1].value(showData['生活指数'][2]['index_level'])
lbs['生活指数']['舒适度指数'][-1].value(showData['生活指数'][3]['index_level'])
lbs['生活指数']['洗车指数'][-1].value(showData['生活指数'][4]['index_level'])
lbs['生活指数']['运动指数'][-1].value(showData['生活指数'][5]['index_level'])
lbs['生活指数']['感冒指数'][-1].value(showData['生活指数'][6]['index_level'])
lbs['生活指数']['晾晒指数'][-1].value(showData['生活指数'][7]['index_level'])
except Exception as e:
print(e)
refresh(ssd)
nano-gui驱动屏幕,使用了framebuffer,所以仅更新需要更新的部分即可,刷新效率非常高。
通过上面的处理,最终就能显示出实际需要的信息了:
六、环境温湿度信息读取
在这个天气牌中,还显示了环境温度和环境湿度,这是通过行空板K10上的AHT20温湿度传感器来获取的。
但是,在实际使用中,经过反复对比确认,其温度值比实际的要高8度,其湿度值要比实际的低11%。当然,这只是我个人的经验数据,需要根据你的实际环境,检查对比确定。
从AHT20 获取温湿度信息,对应的代码如下:
from machine import I2C, Pin
from ahtx0 import AHT20
i2c = I2C(scl=Pin(48), sda=Pin(47),freq=400000)
aht = AHT20(i2c)
try:
temperature = aht.temperature # 温度
relative_humidity = aht.relative_humidity # 湿度
except Exception as e:
print(e)
temperature = 0
relative_humidity = 0
七、中文字体
在nano-gui中,可以很方便的应用中英文字体,来显示对应的内容。
nano-gui本身已经提供了一下英文字体:
另外,也可以使用 nano-gui 作者peterhinch提供的字体处理工具micropython-font-to-py来生成需要的字体文件:
要使用该工具,只需要按照如下方式调用即可:
python font_to_py.py -x -c "需要用到的字符" "ttf字体文件" 字号 font_名称.py
注意,提供的字体文件需要为ttf字体文件。这个比较常见,电脑中通常用的都是这个类型的字体文件。
执行后,将会生成nano-gui调用字体文件:font_名称.py 其中包含了需要显示的字符的字模信息。
使用该工具,仅提取需要显示的字符的信息,极大的减小了字体文件的大小,非常的好用。
为了方便显示,我在源码中,直接提供了一个 gui/fonts/puhui_m_12_all.py ,包含常见的中文。因为包含的中文较多,所以开始调用速度会慢一些。
八、完整源码
为了方便大家学习,将完整的源码提供,源码仓库地址:https://gitee.com/honestqiao/dfrobot_k10_weather_board
下载源码以后,修改weather_config.py中的WiFi连接信息(注意使用2.4GHz而不是5G WiFi),城市编号,以及温湿度偏移量的经验值:
然后,按照下图,上传对应的目录和文件,到MicroPython设备的根目录下:
最后,运行weather_board.py,并且收到如下的输出信息:
就说明运行正常了,就能得到和我一样的天气牌了:
九、补充说明:
DFRobot官方的micropython版本可能存在问题:unihiker.com.cn/wiki/k10/micropython_unihiker_k10,硬件SPI不能初始化,所以会自动切换到软SPI,显示速度可能不快。因此,可以先尝试,如果显示速度满足要求,就可以继续使用,不用切换了。
否则的话,请使用 micropython官方固件版本,可从 MicroPython - Python for microcontrollers 下载 Firmware (Support for Octal-SPIRAM) ,或者使用我已下载的文件【推荐】:
DeadWalking2025.01.02
太优秀了!!!向乔老师学习
HonestQiao2025.01.02
一切学习,加油!
HonestQiao2025.01.02
一起学习,加油!
岑剑伟2025.01.02
非常成功,技术精湛。
HonestQiao2025.01.02
多谢夸奖!