为了充分发挥ESP32的WiFi功能,很多开发者通常会制作桌面天气日历摆件,而M5CoreS3的造型方方正正的,正适合做一个桌面摆件
我个人比较手残,画不好比较漂亮的UI设计,而且我也不太熟悉图形化编程,因此本项目的开发基于MicroPython
首先为开发板烧录UIFlow2.0,这主要是为了引入M5GFX的库
烧录完成后打开Thonny,可以看到识别成功(事实上,使用UiFlow2.0进行开发其背后同样是基于MicroPython的)
有一个开源网址http://t.weather.itboy.net/api/weather/city/+你的城市编号,可以得到当地的天气和天气预报,以上海为例
为了实现这些信息的获取,我们需要使用WiFi;由于我们地处东八区,因此时间计算的时候需要在格林尼治时间的基础上加上8小时,编写基础信息参数如下
WIFI_SSID = "******" WIFI_PSWD = "******" CITY_CODE = "101020100" # 上海 WEATHER_URL = "http://t.weather.itboy.net/api/weather/city/" + CITY_CODE TZ_OFFSET = 8 * 3600 # 上海 UTC+8 CLOCK_REFRESH_MS = 1000 WEATHER_REFRESH_MS = 15 * 60 * 1000
编写全局状态用于后续修改和调用
g_weather = {"ok": False, "city": "", "weatherCN": "", "tempC": "", "humidity": "", "aqi": -1, "updateTime": ""} last_weather_ms = 0 last_clock_ms = 0
编写工具函数用于WiFi连接、时间计算、天气转换、字符颜色随天气特征变化、天气参数获取等
def wifi_connect(ssid, pswd, timeout_ms=15000): wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(ssid, pswd) t0 = time.ticks_ms() while not wlan.isconnected() and time.ticks_diff(time.ticks_ms(), t0) < timeout_ms: time.sleep_ms(250) return wlan.isconnected() def setup_ntp(): try: import ntptime ntptime.host = "ntp1.aliyun.com" ntptime.settime() # 设置 RTC(UTC) except: pass def localtime(): return time.localtime(time.time() + TZ_OFFSET) def weekday_name(wd): return ("Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday")[wd % 7] def is_wifi_ok(): try: return network.WLAN(network.STA_IF).isconnected() except: return False def map_cn_to_en(cn): t = cn or "" if "晴" in t: return "Sunny" if "多云" in t: return "Cloudy" if "阴" in t: return "Overcast" if "雷" in t: return "Thunderstorm" if "雪" in t: return "Snow" if "雨夹雪" in t: return "Sleet" if "雨" in t: return "Rain" return "—" def color_from_aqi(aqi): if aqi < 0: return 0xAAAAAA if aqi <= 50: return 0x55FF55 if aqi <= 100: return 0xFCE000 if aqi <= 150: return 0xFF9900 if aqi <= 200: return 0xFF4444 if aqi <= 300: return 0x9955FF return 0x663300 def to_int(val, default=None): try: # 兼容 "27" / 27 / None / "" 等 return int(str(val).strip()) except: return default def fetch_weather(): global g_weather, last_weather_ms try: resp = requests2.get(WEATHER_URL) data = resp.json() resp.close() d = data.get("data", {}) or {} fc = d.get("forecast", [{}]) fst = fc[0] if isinstance(fc, list) and fc else {} # 先从 data.aqi 取;没有再尝试 forecast[0].aqi;还没有就保持 None aqi_val = to_int(d.get("aqi"), default=None) if aqi_val is None: aqi_val = to_int(fst.get("aqi"), default=None) g_weather["city"] = (data.get("cityInfo", {}) or {}).get("city", "") g_weather["tempC"] = str(d.get("wendu", "")) g_weather["humidity"] = str(d.get("shidu", "")) g_weather["aqi"] = aqi_val if aqi_val is not None else -1 g_weather["quality"] = str(d.get("quality", "")) # 例如 “优/良/轻度污染” g_weather["weatherCN"] = str(fst.get("type", "")) g_weather["updateTime"] = str(data.get("time", "")) g_weather["ok"] = True last_weather_ms = time.ticks_ms() return True except: g_weather["ok"] = False return False
设计UI界面
def draw_hline(y, color): M5.Display.fillRect(0, y, M5.Display.width(), 1, color) def draw_header(): M5.Display.fillRect(0, 0, M5.Display.width(), 26, 0x102030) M5.Display.setTextColor(0xFFFFFF, 0x102030) M5.Display.setCursor(8, 4) M5.Display.print("CoreS3 Weather Clock (MicroPython)") def draw_clock_area(): y0, h = 26, 74 M5.Display.fillRect(0, y0, M5.Display.width(), h, 0x000000) lt = localtime() hhmmss = "%02d:%02d:%02d" % (lt[3], lt[4], lt[5]) ymdw = "%04d-%02d-%02d %s" % (lt[0], lt[1], lt[2], weekday_name(lt[6])) # 时间 M5.Display.setTextColor(0xFFFFFF, 0x000000) M5.Display.setCursor(80, y0 + 16) M5.Display.print(hhmmss) # 日期 M5.Display.setTextColor(0xCCCCCC, 0x000000) M5.Display.setCursor(80, y0 + 36) M5.Display.print(ymdw) # 联网状态 M5.Display.setTextColor(0x55FF55 if is_wifi_ok() else 0xFF5555, 0x000000) M5.Display.setCursor(8, y0 + h - 18) M5.Display.print("Wi-Fi OK" if is_wifi_ok() else "Offline") def draw_weather_icon(x, y, size, cn): import math def sun(cx, cy, r): M5.Display.fillCircle(cx, cy, r, 0xFDB813) for i in range(8): a = i * math.pi / 4 x1 = int(cx + math.cos(a) * (r + 2)) y1 = int(cy + math.sin(a) * (r + 2)) x2 = int(cx + math.cos(a) * (r + 10)) y2 = int(cy + math.sin(a) * (r + 10)) M5.Display.drawLine(x1, y1, x2, y2, 0xFDB813) def cloud(cx, cy, w, h, col): M5.Display.fillEllipse(cx, cy, w//2, h//2, col) M5.Display.fillEllipse(cx - w//3, cy + 2, w//3, h//3, col) M5.Display.fillEllipse(cx + w//3, cy + 2, w//3, h//3, col) M5.Display.fillRect(cx - w//2, cy, w, h//2, col) def drop(rx, ry, r, col): M5.Display.fillTriangle(rx, ry - r, rx - r//2, ry, rx + r//2, ry, col) M5.Display.fillCircle(rx, ry, r//2, col) def bolt(lx, ly, s, col): M5.Display.fillTriangle(lx, ly, lx + s, ly + s//2, lx + s//2, ly + s//2, col) M5.Display.fillTriangle(lx + s//2, ly + s//2, lx + s*3//2, ly + s*3//2, lx + s, ly + s*3//2, col) t = cn or "" cloudCol = 0xB0B8C0 if "晴" in t: sun(x + size//2, y + size//2, size//3) elif "多云" in t: sun(x + size//3, y + size//3, size//5) cloud(x + size//2, y + size//2 + 6, size, size//2, cloudCol) elif "阴" in t: cloud(x + size//2, y + size//2 + 6, size, size//2, 0x808890) elif "雷" in t: cloud(x + size//2, y + size//2, size, size//2, cloudCol) bolt(x + size//2 + 8, y + size//2 + 4, 10, 0xFCE000) elif "雪" in t: cloud(x + size//2, y + size//2, size, size//2, cloudCol) for i in (-1,0,1): sx = x + size//2 + i*12 sy = y + size - 8 + (2 if i==0 else 0) M5.Display.drawLine(sx - 4, sy, sx + 4, sy, 0xFFFFFF) M5.Display.drawLine(sx, sy - 4, sx, sy + 4, 0xFFFFFF) elif "雨" in t: cloud(x + size//2, y + size//2, size, size//2, cloudCol) drop(x + size//2 - 12, y + size - 6, 6, 0x55AAFF) drop(x + size//2, y + size - 2, 6, 0x55AAFF) drop(x + size//2 + 12, y + size - 6, 6, 0x55AAFF) else: cloud(x + size//2, y + size//2, size, size//2, cloudCol) def draw_weather_area(): y0 = 110 h = M5.Display.height() - y0 M5.Display.fillRect(0, y0, M5.Display.width(), h, 0x000000) card = 0x081018 M5.Display.fillRoundRect(10, y0 + 6, M5.Display.width()-20, h-12, 10, card) if not g_weather["ok"]: M5.Display.setTextColor(0xFCE000, card) M5.Display.setCursor(20, y0 + 14) M5.Display.print("Weather: unavailable") M5.Display.setTextColor(0xFFFFFF, card) M5.Display.setCursor(20, y0 + 36) M5.Display.print("Tap right side to retry.") return M5.Display.setTextColor(0xFFFFFF, card) M5.Display.setCursor(20, y0 + 10) M5.Display.print("{} | {}".format(g_weather["city"], g_weather["updateTime"])) draw_weather_icon(30, y0 + 38, 40, g_weather["weatherCN"]) M5.Display.setTextColor(0xFFFFFF, card) M5.Display.setCursor(90, y0 + 44) M5.Display.print("Temperature: {} C {}".format(g_weather["tempC"], map_cn_to_en(g_weather["weatherCN"]))) M5.Display.setTextColor(0xA0D8F0, card) M5.Display.setCursor(90, y0 + 69) M5.Display.print("Humidity: {}".format(g_weather["humidity"])) M5.Display.setCursor(90, y0 + 94) M5.Display.setTextColor(color_from_aqi(g_weather["aqi"]), card) M5.Display.print("AQI: {}".format(g_weather["aqi"] if g_weather["aqi"] >= 0 else "--")) M5.Display.setTextColor(0x888888, card) M5.Display.setCursor(M5.Display.width() - 180, y0 + h - 22)
主流程和入口函数
def setup(): M5.begin() Widgets.fillScreen(0x222222) M5.Display.setRotation(1) # 横屏 M5.Display.setBrightness(180) draw_header() draw_hline(26, 0x334455) draw_hline(140, 0x334455) M5.Display.setTextColor(0xFFFFFF, 0x000000) M5.Display.setCursor(8, 30) M5.Display.print("Connecting Wi-Fi...") if wifi_connect(WIFI_SSID, WIFI_PSWD): M5.Display.print("\nWi-Fi connected.") setup_ntp() else: M5.Display.print("\nWi-Fi failed. Offline mode.") fetch_weather() draw_clock_area() draw_weather_area() def loop(): global last_clock_ms, last_weather_ms M5.update() if time.ticks_diff(time.ticks_ms(), last_clock_ms) >= CLOCK_REFRESH_MS: last_clock_ms = time.ticks_ms() draw_clock_area() if time.ticks_diff(time.ticks_ms(), last_weather_ms) >= WEATHER_REFRESH_MS: if is_wifi_ok() and fetch_weather(): draw_weather_area() # 右侧触摸立即刷新(兼容 getDetail/getCount) try: t = M5.Touch.getDetail() if t.state in (2, 10, 14): # touch_end / flick_end / drag_end if t.x > (M5.Display.width() * 0.6): M5.Display.setTextColor(0xFCE000, 0x102030) M5.Display.setCursor(M5.Display.width() - 110, 4) M5.Display.print("Refreshing...") if fetch_weather(): draw_weather_area() M5.Display.fillRect(M5.Display.width() - 112, 2, 110, 22, 0x102030) M5.Display.setTextColor(0xFFFFFF, 0x102030) except: if M5.Touch.getCount() > 0: if fetch_weather(): draw_weather_area() setup() while True: loop() time.sleep_ms(10)
实现效果如下,可以看到字体有点小了。这个天气的时间并不是实时的,这个网站的信息大概半小时会更新一次
工程代码如下