python/app/weather.py · 24223 字节 · 下载原文
# weather.py —— 常用小程序②:**天气**(联网抓一周预报 → 存文件 → 彩色显示)
# weather.py -- Everyday utility #2: **weather** (fetch a one-week forecast, save it, then show it)
#
# 四段:① 裸套接字取回 api.open-meteo.com 的预报(明文 HTTP,80 端口 —— VM 里没有 TLS);
# Four stages: (1) fetch the forecast from api.open-meteo.com over a raw socket (plain HTTP on port
# 80 -- there is no TLS in the VM); (2) strip the response headers and write the body into
# weather.txt (**relative path**: on the phone the sandbox root is the workspace);
# ② 剥掉响应头,把正文写进 weather.txt(**相对路径**:手机上沙箱根 = 工作区);
# (2) strip the response headers and write the body into weather.txt (**a relative path**: on the
# phone the sandbox root is the workspace and absolute paths are rejected);
# ③ 解析 CSV(**要 `format=csv`**,比 JSON 少一整层解析);④ 画一张彩色的周预报。
# (3) parse the CSV (**ask for `format=csv`**, which saves a whole parsing layer over JSON);
# (4) draw a colour week card.
#
# 这是 `Examples/c/app/weather.c` 的 Python 移植:联网那部分**照搬**(DNS → 裸套接字 → 明文
# This is the Python port of `Examples/c/app/weather.c`: the networking is ported as is
# (DNS -> raw socket -> plain HTTP on port 80 -> `format=csv`), and so is the offline fallback.
# HTTP 80 → `format=csv`),离线回落读 `weather.txt` 也照搬。
# The offline fallback reads `weather.txt` exactly like the C version does.
#
# ## Python 前端没有 `asm()` ⇒ 系统调用只能走共享库的包装(**都实测过**)
# ## This front end has no `asm()`, so syscalls go through the shared library's wrappers
#
# `Lib/python/shared.py` 里那套 `net_*` / `vml_dns_resolve` / `vml_file_*` 就是 `svc` 号
# The `net_*` / `vml_dns_resolve` / `vml_file_*` set in `Lib/python/shared.py` are thin wrappers
# 的薄包装(`net_connect` = SYSCALL #330 + #334、`net_recv` = #336、`vml_file_read` = #112 …),
# over the syscall numbers (`net_connect` = #330 + #334, `net_recv` = #336, `vml_file_read` = #112),
# 与 C 版自己包的那层 `sys_*` 一一对应。
# one for one with the `sys_*` layer the C version wraps itself.
# ⚠ 域名仍然**不能直接连**:宿主 #334 内部是 `IPAddress.Parse` ⇒ 必须先 `vml_dns_resolve`。
# ⚠ A hostname still **cannot be connected directly**: host #334 uses `IPAddress.Parse` inside, so
# `vml_dns_resolve` has to run first.
#
# ## 收数据这一步是本移植最别扭的地方(原因不是"想不到",是**没得选**)
# ## Reading the response back is the awkward part of this port (not by choice)
#
# 本语言**没有取地址、没有 `peek`/`poke`**(`peekb(x)` 会编成 `CALL vml_peekb`,而 `Lib/` 下
# This language has **no address-of and no `peek`/`poke`** (`peekb(x)` compiles to `CALL vml_peekb`,
# 没有这个标签 —— 实测 `error: 未定义的函数 'vml_peekb'`),所以"往缓冲里收、再逐字节读回来"
# and no such label exists under `Lib/` -- measured: `error: undefined function 'vml_peekb'`), so
# 只能借共享库的两件工具:
# "receive into a buffer, then read it back byte by byte" has to lean on two shared-library tools:
# · `str_charat(addr, i)` —— 按**地址**取一个字节(C 里叫 `buf[i]`);
# · `str_charat(addr, i)` -- one byte **by address** (what C writes as `buf[i]`);
# · `str_substr(dst, src, pos, count)` —— 从地址抄一段到另一个地址(C 里叫"取子串")。
# · `str_substr(dst, src, pos, count)` -- copy a slice from one address to another.
# 收来的字节因此**从头到尾都以地址形式流动**:`ui_2d_text` 要的也是地址,两者正好同源。
# The received bytes therefore stay **addresses end to end**, which is also what `ui_2d_text`
# wants -- the two agree.
# ⚠ 代价:`str_substr` 的边界靠 `strlen(src)` 定,所以**每写一段都要自己在缓冲尾补一个 0**
# ⚠ The cost: `str_substr` bounds itself by `strlen(src)`, so **every write must NUL-terminate the
# (本文件里就是 `memset(RBUF + TOTAL, 0, 1)` 那几处)。
# buffer tail first** (the `memset(RBUF + TOTAL, 0, 1)` calls in this file).
#
# ## 为什么"空闲 8 秒就自己退出"
# ## Why it exits by itself after 8 seconds of idling
#
# 例程要能被**自动验收**(`scripts/vmlcli` 加 `--frame` 出一张图就够了),所以循环必须有界。
# Samples have to be **automatically verifiable** (one `scripts/vmlcli` run with `--frame` is
# enough), so the loop must be bounded.
# ── 显式引用:`ui_*` 来自共享调用库 `vmlui.vml`;套接字/文件/字符串助手来自共享库 ──
# ── explicit references: `ui_*` from the shared call library `vmlui.vml`; the socket/file/string
# helpers from the shared library ──
import vml_ui
import shared
HOST = "api.open-meteo.com"
PORT = 80
OUTFILE = "weather.txt"
BUFCAP = 8192 # 响应缓冲:CSV 正文实测 ~250 字节,8K 绰绰有余
# Response buffer: the CSV body measures ~250 bytes, so 8K is plenty.
MAXROUNDS = 64 # 读循环上限:防"对端不关连接"时死读
# Read-loop cap: stops an endless read when the peer never closes.
IDLE_EXIT_MS = 8000
# 配色(真彩 AA RRGGBB)
# Colours (true colour AA RRGGBB)
C_BG = 0xFF0E1116
C_CARD = 0xFF1A1F28
C_TITLE = 0xFFE8E8F0
C_DIM = 0xFF9AA0B0
C_HI = 0xFFE06C50
C_LO = 0xFF4A90D9
C_TRACK = 0xFF2A3038
C_WARN = 0xFFE0A040
# 请求正文。C 版一段一段 `strcat` 拼(前端没有字符串拼接),本语言的字符串字面量**直接支持**
# The request body. The C version stitches it together with `strcat` because its front end has no
# string concatenation; here a plain string literal works.
# `\r\n` 转义(实测 `strlen` 对得上、服务端也认)。**必须写在一行**:本语言不做相邻字面量拼接
# (`strlen` matches and the server accepts it, measured). It **must stay on one line**: adjacent
# string literals are not concatenated here (measured: `"x" "y"` compiles down to `"x"`).
# ⚠ `format=csv` 是这一版的关键:open-meteo 支持直接回 CSV,省掉一整层 JSON 解析。
# ⚠ `format=csv` is the crux here: open-meteo can return CSV directly, saving a JSON layer.
# ⚠ 现在只留**请求路径**:请求行与那套头由库里的 `http_get_to` 拼(它那份与本例程原先手写的逐字相同)。
# ⚠ Only the **path** is kept now: `http_get_to` in the library builds the request line and headers.
PATH = "/v1/forecast?latitude=22.54&longitude=114.06&daily=temperature_2m_max,temperature_2m_min,weather_code&forecast_days=7&timezone=Asia/Shanghai&format=csv"
# ── 缓冲与状态 ────────────────────────────────────────
# ── buffers and state ─────────────────────────────────
RBUF = vml_alloc(BUFCAP) # 整个 HTTP 响应(头 + 正文),离线时当缓存正文用
# The whole HTTP response (headers plus body), and the cache body when offline
S_NUM = vml_alloc(16) # 整数转字符串 / 切字段用
# scratch for `itoa` and for a cut-out field
S_TB = vml_alloc(24) # "19.8~28.1" 这种温度串
# the "19.8~28.1" temperature line
D_DATES = vml_alloc(96) # 7 天 × 12 字节(只存 `MM-DD`,长度足够放下 7 段)
# 7 days x 12 bytes (only `MM-DD` is stored, room enough for seven)
# 画布尺寸(由顶层算好,函数只读)
# Canvas size (computed at top level, only read inside functions)
SW = 0
SH = 0
LANG = 0
OFFLINE = 0 # 1 = 没连上,显示的是本地缓存
# 1 = offline, showing the local cache
D_N = 0 # 解析出几天
# how many days were parsed
# 每天的最低/最高/天气代码。C 版是定长数组,本语言有列表 ⇒ 直接摆列表(函数里读、顶层写)。
# Low/high/weather code per day. The C version uses fixed arrays; this language has lists, so the
# lists are written at top level and read inside functions.
D_LO = [0, 0, 0, 0, 0, 0, 0, 0]
D_HI = [0, 0, 0, 0, 0, 0, 0, 0]
D_CODE = [0, 0, 0, 0, 0, 0, 0, 0]
# ── 小工具 ────────────────────────────────────────────
# ── small helpers ─────────────────────────────────────
# ⚠ 这里原先有两个手写的协议辅助:`find_body`(找 `\r\n\r\n`)与 `http_code`(解析状态码)——
# 库 `Lib/shared/src/http.c` 的 `http_get_to` 已经把剥头与状态码都做了,只往回装**正文**。
# The two hand-written protocol helpers used to live here; `http_get_to` covers both.
# 一个字节是不是十进制数字
# Is one byte a decimal digit
def is_digit_at(at):
c = str_charat(RBUF, at)
if c >= 48 and c <= 57:
return 1
return 0
# WMO 天气代码 → 一句话。代码表来自 open-meteo 的文档:
# WMO weather code -> a short phrase. The table follows open-meteo's docs:
# 0 晴 / 1-3 云 / 45 雾 / 5x 毛毛雨 / 6x 雨 / 7x 雪 / 8x 阵雨 / 9x 雷暴。
# 0 clear / 1-3 cloud / 45 fog / 5x drizzle / 6x rain / 7x snow / 8x showers / 9x thunderstorm.
def wmo_label(code, lg):
if code == 0:
return "晴" if lg == 0 else "clear"
if code == 1 or code == 2:
return "多云" if lg == 0 else "cloudy"
if code == 3:
return "阴" if lg == 0 else "overcast"
if code == 45 or code == 48:
return "雾" if lg == 0 else "fog"
if code >= 51 and code <= 57:
return "毛毛雨" if lg == 0 else "drizzle"
if code >= 61 and code <= 67:
return "雨" if lg == 0 else "rain"
if code >= 71 and code <= 77:
return "雪" if lg == 0 else "snow"
if code >= 80 and code <= 82:
return "阵雨" if lg == 0 else "showers"
if code >= 95:
return "雷暴" if lg == 0 else "storm"
return "未知" if lg == 0 else "unknown"
# ── 绘制 ──────────────────────────────────────────────
# ── drawing ───────────────────────────────────────────
def draw():
cx = SW // 2
pad = SW // 18
top = 0
avail = 0
cardh = 0
i = 0
y = 0
h = 0
midy = 0
innerw = 0
barw = 0
n = D_N
if pad < 6:
pad = 6
top = pad + 58 # 标题两行
# room for the two heading lines
avail = SH - top - 40
if n <= 0:
n = 1
cardh = avail // n
ui_2d_clear(C_BG)
ui_2d_text_styled(cx, pad, "一周预报" if LANG == 0 else "One-week forecast",
C_TITLE, 17, VML_ANCHOR_CENTER, VML_FONT_BOLD)
if OFFLINE != 0:
ui_2d_text(cx, pad + 26, "离线:读本地缓存 weather.txt" if LANG == 0
else "offline: local cache weather.txt", C_WARN, 11, VML_ANCHOR_CENTER)
else:
ui_2d_text(cx, pad + 26, "数据源 api.open-meteo.com" if LANG == 0
else "source api.open-meteo.com", C_DIM, 11, VML_ANCHOR_CENTER)
i = 0
while i < D_N:
y = top + i * cardh
h = cardh - 6
midy = y + h // 2
ui_2d_rect(pad, y, SW - pad * 2, h, C_CARD, 1, 0, 8)
# 日期:`D_DATES + i * 12` 就是第 i 天那 5 个字节的**地址**,`ui_2d_text` 直接收
# The date: `D_DATES + i * 12` is the **address** of that day's five bytes and
# (C 版是"从扁平缓冲搬进一个小局部串"——本语言连这步都不用)。
# `ui_2d_text` takes it as is (the C version first copies it into a local string).
ui_2d_text(pad + 12, midy - 8, D_DATES + i * 12, C_TITLE, 14, VML_ANCHOR_LEFT)
# 天气一句话
# The condition in words
ui_2d_text(SW - pad - 12, midy - 7, wmo_label(D_CODE[i], LANG), C_DIM, 12,
VML_ANCHOR_RIGHT)
# 最低~最高 + 温度条(10℃ 起,34℃ 满条)
# Low~high plus a temperature bar (starts at 10 C and fills at 34 C)
itoa(D_LO[i], S_NUM)
strcpy(S_TB, S_NUM)
strcat(S_TB, "~")
itoa(D_HI[i], S_NUM)
strcat(S_TB, S_NUM)
ui_2d_text(pad + 12, midy + 10, S_TB, C_LO, 12, VML_ANCHOR_LEFT)
innerw = SW - pad * 2 - 24
if innerw < 40:
innerw = 40
barw = (D_HI[i] - 10) * innerw // 24
if barw < 8:
barw = 8
if barw > innerw:
barw = innerw
ui_2d_rect(pad + 12, midy + 26, innerw, 6, C_TRACK, 1, 0, 3)
if D_HI[i] >= 30:
ui_2d_rect(pad + 12, midy + 26, barw, 6, C_HI, 1, 0, 3)
else:
ui_2d_rect(pad + 12, midy + 26, barw, 6, C_LO, 1, 0, 3)
i = i + 1
ui_2d_present() # ⚠ 不调这句 = 黑屏
# ⚠ without this call you get a black screen
# ── 主流程 ────────────────────────────────────────────
# ── main flow ─────────────────────────────────────────
LANG = ui_scr_get_language()
TOTAL = 0
HEAD = 0
BLEN = 0
FAIL = 0
IP = 0
FD = -1
N = 0
WANT = 0
ROUNDS = 0
CODE = 0
FH = 0
WROTE = 0
print_str("=== VML 天气预报 ===\n" if LANG == 0 else "=== VML Weather Report ===\n")
print_str("主机 " if LANG == 0 else "Host ")
print_str(HOST)
print_str(" 文件 " if LANG == 0 else " file ")
print_str(OUTFILE)
print_str("\n")
# ① 域名 → IP(宿主不解析域名)
# ① Hostname to IP (the host does not resolve names)
# ⚠ 解析失败时宿主把 R0 置 **-1**(不是 0),所以判据是 `<= 0`;只判 `== 0` 会把 -1
# ⚠ On a failed lookup the host sets R0 to **-1** (not 0), so the test is `<= 0`; testing only
# `== 0` would hand -1 to the next call as if it were an address (measured on the C side).
# as if it were an address (measured on the C side: a "memory error", not a DNS failure).
IP = vml_dns_resolve(HOST)
if IP <= 0:
print_str("[警告] DNS 解析失败(转为离线)\n" if LANG == 0
else "[WARN] DNS lookup failed (going offline)\n")
OFFLINE = 1
else:
print_str("[1/4] DNS ")
print_str(IP)
print_str("\n")
# ②③ 抓数据 —— 建套接字、连接、发请求、收完整响应、剥头、状态码**全在库里**
# ⚠ 用 `http_get_to` 而不是 `http_get`:后者把「连哪儿」与「Host 写什么」交给**同一个参数**,
# 传域名连不上(#334 不解析域名)、传 IP 则 Host 头成了 IP ⇒ 虚拟主机按 Host 路由返回 **404**。
# The rest of the fetch lives in the library's `http_get_to` (connect to the IP, `Host:` carries the name).
if OFFLINE == 0:
CODE = http_get_to(IP, PORT, HOST, PATH, RBUF, BUFCAP)
if CODE != 200:
print_str("[警告] 抓取失败(转为离线)\n" if LANG == 0
else "[WARN] fetch failed (going offline)\n")
OFFLINE = 1
else:
BLEN = strlen(RBUF) # `http_get_to` 只往回装**正文**(头已经剥掉了)
# it stores **only the body**; headers are already stripped
HEAD = 0
print_str("[3/4] HTTP ")
print_int(CODE)
print_str(" 正文 " if LANG == 0 else " body ")
print_int(BLEN)
print_str(" 字节\n" if LANG == 0 else " bytes\n")
# ④ 剥头 → 写文件(离线时跳过)
# ④ Strip the headers and write the file (skipped when offline)
if OFFLINE == 0:
FH = vml_file_open(OUTFILE, 1) # 1 = 只写(FileMode.Create)
# 1 = write only
if FH < 0:
print_str("[警告] 打不开 " if LANG == 0 else "[WARN] cannot open ")
print_str(OUTFILE)
print_str("(手机上路径必须是相对的)\n" if LANG == 0
else " (paths must be relative on the phone)\n")
else:
WROTE = vml_file_write(FH, RBUF + HEAD, BLEN)
vml_file_close(FH)
print_str("[4/4] HTTP ")
print_int(CODE)
print_str(" 正文 " if LANG == 0 else " body ")
print_int(BLEN)
print_str(" 字节" if LANG == 0 else " bytes")
print_str(" 写入 " if LANG == 0 else " wrote ")
print_int(WROTE)
print_str(" 字节 -> " if LANG == 0 else " bytes -> ")
print_str(OUTFILE)
print_str("\n")
# ⑤ 离线回落:读上一次留下的 weather.txt(相对路径,各宿主自己解析)
# ⑤ Offline fallback: read the weather.txt left by the last run (a relative path each host resolves)
# ⚠ `mode = 0` 是**只读**:写成 1 会把缓存文件清空再读,拿到一份空数据。
# ⚠ `mode = 0` is **read-only**: passing 1 would truncate the cache file first and hand back nothing.
if OFFLINE != 0:
FH = vml_file_open(OUTFILE, 0)
if FH < 0:
print_str("[失败] 离线且没有本地缓存 " if LANG == 0 else "[FAIL] offline and no local cache ")
print_str(OUTFILE)
print_str(" —— 先在联网的机器上跑一次,或把预报 CSV 放到同目录。\n" if LANG == 0
else " -- run this once on a networked machine, or drop the forecast CSV next to it.\n")
FAIL = 1
else:
BLEN = vml_file_read(FH, RBUF, BUFCAP - 1)
vml_file_close(FH)
if BLEN <= 0:
print_str("[失败] 缓存文件是空的\n" if LANG == 0 else "[FAIL] the cache file is empty\n")
FAIL = 1
else:
memset(RBUF + BLEN, 0, 1)
HEAD = 0 # 缓存里没有响应头,正文从 0 开始
# the cache has no headers
# ⑥ 解析成结构化的"天"
# ⑥ Parse the CSV into structured days
# ⚠ C 版先切好 `line[96]` 再 `split_csv`,本语言直接在缓冲上按**下标**做(`str_charat` 读、
# ⚠ The C version copies each line into `line[96]` and calls `split_csv`; here the buffer is walked
# by index instead (`str_charat` to read, `str_substr` to cut a field out).
# by index instead (`str_charat` reads, `str_substr` cuts a field out).
# ⚠ 只认「日期形状」的行:第 1 段必须是 `YYYY-MM-DD`。别只判"数字开头" —— open-meteo 的 CSV
# ⚠ Only lines shaped like a date count: the first field must be `YYYY-MM-DD`. Do not settle for
# "starts with a digit" -- line 2 of open-meteo's CSV is the latitude/longitude row
# **第 2 行是经纬度那行**(`22.530754,114.08714,...`),数字开头,判错了会多出一行"日期"。
# (`22.530754,114.08714,...`), which starts with a digit and would add a bogus "day".
if FAIL == 0:
D_N = 0
LI = HEAD
LE = HEAD
C1 = 0
C2 = 0
C3 = 0
while LI < HEAD + BLEN:
LE = LI
while LE < HEAD + BLEN:
if str_charat(RBUF, LE) == 10:
break
LE = LE + 1
if LE > LI:
if is_digit_at(LI) == 1:
if str_charat(RBUF, LI + 4) == 45 and str_charat(RBUF, LI + 7) == 45:
C1 = LI
while C1 < LE:
if str_charat(RBUF, C1) == 44:
break
C1 = C1 + 1
C2 = C1 + 1
while C2 < LE:
if str_charat(RBUF, C2) == 44:
break
C2 = C2 + 1
C3 = C2 + 1
while C3 < LE:
if str_charat(RBUF, C3) == 44:
break
C3 = C3 + 1
if C3 < LE and D_N < 7:
str_substr(S_NUM, RBUF, C1 + 1, C2 - C1 - 1)
D_HI[D_N] = atoi(S_NUM)
str_substr(S_NUM, RBUF, C2 + 1, C3 - C2 - 1)
D_LO[D_N] = atoi(S_NUM)
str_substr(S_NUM, RBUF, C3 + 1, LE - C3 - 1)
D_CODE[D_N] = atoi(S_NUM)
# 日期只留 `MM-DD`(年前两位对一周预报没意义,省下的宽度留给温度)
# Keep only `MM-DD` from the date (the century is noise for a week)
str_substr(D_DATES + D_N * 12, RBUF, LI + 5, 5)
memset(D_DATES + D_N * 12 + 5, 0, 1)
D_N = D_N + 1
LI = LE + 1
if D_N == 0:
print_str("[失败] 没解析出数据行(CSV 格式变了吗?)\n" if LANG == 0
else "[FAIL] no data rows parsed (did the CSV format change?)\n")
FAIL = 1
else:
print_str("解析 " if LANG == 0 else "parsed ")
print_int(D_N)
print_str(" 天:\n" if LANG == 0 else " days:\n")
I = 0
while I < D_N:
print_str(" ")
print_str(D_DATES + I * 12)
print_str(" ")
itoa(D_LO[I], S_NUM)
print_str(S_NUM)
print_str("~")
itoa(D_HI[I], S_NUM)
print_str(S_NUM)
print_str(" code=")
itoa(D_CODE[I], S_NUM)
print_str(S_NUM)
print_str("\n")
I = I + 1
# ⑦ 开窗 → 画 → 消息循环
# ⑦ Open a window, draw, then run the message loop
# ⚠ 版面那几项是**模块级标量**,赋值只能写在顶层(函数里写就是新建局部量,实测)。
# ⚠ The layout values are **module-level scalars**, so the assignments live at the top level
# (inside a function they would only create locals, measured).
if FAIL == 0:
# 开窗时传 0:尺寸交给宿主(用真实可用区);开窗后再问一次真实画布(见下)。
# Open with 0 so the host decides the size (the real usable area), then ask the real canvas
# again below.
ui_win_open_ex("weather.py", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD)
# 开窗后再问一次 —— 画布尺寸与开窗前的估算**可能不同**
# Ask again after opening: the canvas may differ from the pre-open estimate
SW = ui_scr_get_w()
SH = ui_scr_get_h()
if SW <= 0:
SW = 360
if SH <= 0:
SH = 620
draw()
T0 = ui_time_get_ms()
DONE = 0
while DONE == 0 and ui_win_is_closed() == 0:
T = ui_msg_fetch_wait(200)
if T == VML_MSG_NONE:
if ui_time_get_ms() - T0 > IDLE_EXIT_MS:
DONE = 1 # 空闲超时(验收用)
# idle timeout (for automated checks)
elif T == VML_MSG_KEYDOWN:
K = ui_msg_get_a()
T0 = ui_time_get_ms()
if K == VML_KEY_ESCAPE or K == 113 or K == 81:
DONE = 1
elif K == VML_KEY_LEFT or K == VML_KEY_RIGHT:
draw()
elif K == VML_KEY_UP or K == VML_KEY_DOWN:
draw()
elif T == VML_MSG_WINDOWORIENT or T == VML_MSG_WINDOWRESIZE:
SW = ui_scr_get_w()
SH = ui_scr_get_h()
if SW <= 0:
SW = 360
if SH <= 0:
SH = 620
draw()
elif T == VML_MSG_WINDOWCLOSE:
DONE = 1