回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

行空板K10天气牌(包括天气信息获取、温湿度读取、显示屏显示、中文字体) 中等

头像 HonestQiao 2025.01.02 117 5

一、项目简介:

 

这个项目,基于行空板K10核micropython环境,从天气网站提取信息,并仿照天气网站的布局,制作了一个天气牌,并提供了完整的源码(往后看),具体界面和效果如下:

image.png

二、硬件清单:

材料清单

  • 行空板 K10 MicroPython编程教学/学习主控板(AI教学) X1 链接

三、天气网站:

 

这个天气牌,参考的天气网站为:https://zte.weatherol.com/

image.png
image.png

 

当然,也有很多提供天气信息API的站点,可以直接通过API接口获取天气信息。

 

这个项目中,直接从原始页面中获取天气信息,所以也展示了一些python语言的用法。

 

 

如果查看该页面的源代码,则可以在源代码中,看到基本天气信息的数据:

天气页面:天⽓预报-北京 (weatherol.com)

image.png

 

因此,只需要在程序中,获取backstageData对应的数据,就可以得到天气数据了。

 

另外,还需要分析一些辅助信息,以便获取或者计算最终需要的数据。

空气质量信息:zte.weatherol.com/getAqiforecast24h?id=101010100

image.png

 

降雨信息:zte.weatherol.com/getprecipitationByid?type=forecast&id=101010100

image.png

生活指数:zte.weatherol.com/getCurrentIndexDate?stationid=101010100

image.png

 

上述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 页面,选取对应的城市后,从网址中获取即可:

image.png

四、天气信息处理代码

 

这个天气牌的程序,核心代码有两个部分,一个是天气信息的提取,一个是界面的显示。

 

天气信息提取部分,涉及到网络请求和文本内容分析提取。

 

首先,需要联网,对应的调用代码如下:

代码
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)

上面的代码,将会显示一个基础的界面:

image.png

 

 

然后,再根据前面取得的天气信息的数据,依次显示更新即可:

 

 

代码
    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,所以仅更新需要更新的部分即可,刷新效率非常高。

 

通过上面的处理,最终就能显示出实际需要的信息了:

image.png

 

六、环境温湿度信息读取

 

在这个天气牌中,还显示了环境温度和环境湿度,这是通过行空板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本身已经提供了一下英文字体:

image.png

另外,也可以使用 nano-gui 作者peterhinch提供的字体处理工具micropython-font-to-py来生成需要的字体文件:

peterhinch/micropython-font-to-py: A Python 3 utility to convert fonts to Python source capable of being frozen as bytecode (github.com)

 

要使用该工具,只需要按照如下方式调用即可:

代码
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),城市编号,以及温湿度偏移量的经验值:

image.png

 

然后,按照下图,上传对应的目录和文件,到MicroPython设备的根目录下:

image.png

 

 

最后,运行weather_board.py,并且收到如下的输出信息:

image.png

 

 

就说明运行正常了,就能得到和我一样的天气牌了:

image.png

九、补充说明:

 

DFRobot官方的micropython版本可能存在问题:unihiker.com.cn/wiki/k10/micropython_unihiker_k10,硬件SPI不能初始化,所以会自动切换到软SPI,显示速度可能不快。因此,可以先尝试,如果显示速度满足要求,就可以继续使用,不用切换了。

 

 

否则的话,请使用 micropython官方固件版本,可从 MicroPython - Python for microcontrollers 下载 Firmware (Support for Octal-SPIRAM) ,或者使用我已下载的文件【推荐】

 

 

评论

user-avatar
  • DeadWalking

    DeadWalking2025.01.02

    太优秀了!!!向乔老师学习

    2
    • HonestQiao

      HonestQiao2025.01.02

      一切学习,加油!

    • HonestQiao

      HonestQiao2025.01.02

      一起学习,加油!

  • 岑剑伟

    岑剑伟2025.01.02

    非常成功,技术精湛。

    1
    • HonestQiao

      HonestQiao2025.01.02

      多谢夸奖!