c/app/weather.c · 28734 字节 · 下载原文
/* weather.c —— 常用小程序②:**天气**(联网抓一周预报 → 存文件 → 彩色显示)
* weather.c -- Everyday utility #2: **weather** (fetch a one-week forecast, save it, then show it)
*
* 四段:① 裸套接字取回 api.open-meteo.com 的预报(明文 HTTP,80 端口 ——
* Four stages: (1) fetch the forecast from api.open-meteo.com over a raw socket (plain HTTP on port 80 —
* VM 里没有 TLS,所以不能走 https);② 剥掉响应头,把正文写进 weather.txt
* there is no TLS in the VM, so https is impossible); (2) strip the response headers and write the body into weather.txt
* (**相对路径**:手机上沙箱根 = 工作区,绝对路径会被拒);③ 解析 CSV(**要 `format=csv`**,
* (**relative path**: on the phone the sandbox root is the workspace, absolute paths are rejected); (3) parse the CSV (**ask for `format=csv`**,
* 比 JSON 少一整层解析,三门语言移植时都省事);④ 用 `tty_*` 画一张彩色的周预报。
* which saves a whole parsing layer over JSON and makes porting to other languages easier); (4) draw a colour week card with `tty_*`.
*
* ⚠ **抓不到就回落读本地**(用户 2026-10-07 定:「真数据 + 离线回落」):DNS / 连接 / HTTP 任一失败,
* ⚠ **Falls back to the local file when the fetch fails** (user's call, 2026-10-07: "real data plus an offline fallback"):
* 就去读上一次留下的 `weather.txt` 并照常显示,只在标题上标一行"(离线:读本地缓存)"。
* when DNS, the connection or HTTP itself fails, it reads the `weather.txt` left by the last run and displays that,
* 于是**没网的机器上这个例程照样有东西看**,而"联网怎么写"那部分一行没少。
* marking the title "(offline: local cache)" — so the sample still shows something on a machine without a network,
* while the "how to do networking" part is unchanged.
*
* 跑法(桌面脚手架,秒级,别为改一行去打 APK):
* How to run (desktop scaffolding, seconds — do not build an APK just to change one line):
* dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll Examples/c/app/weather.c --vml-home third_party/vml --timeout 60
* 手机上:命令行页敲 `vml run examples/c/app/weather.c`,文件落在工作区根的 weather.txt。
* On the phone: type `vml run examples/c/app/weather.c` in the command-line page; the file lands in weather.txt at the workspace root.
*
* ⚠ 三个已实测的坑,改动本文件前先读:
* ⚠ Three pitfalls already confirmed by testing — read these before changing this file:
*
* ① **域名不由宿主解析**(**2026-10-04 复验仍成立**:宿主源码 `VMLRuntime/VMLRuntime.Syscall.OS.cs` 里
* ① **The host does not resolve domain names** (**still holds, re-checked 2026-10-04**: in the host source `VMLRuntime/VMLRuntime.Syscall.OS.cs`
* `#334 SocketConnect(fd, ip, port)` 的实现是 `IPAddress.Parse(ip)`),喂域名会直接抛异常返回 -1。
* `#334 SocketConnect(fd, ip, port)` is implemented as `IPAddress.Parse(ip)`), so feeding it a domain name throws and returns -1.
* 所以必须先 `#338 DnsResolve(host)` 把域名换成 IP 字符串。
* The host must therefore be turned into an IP string first with `#338 DnsResolve(host)`.
*
* ② **别写「先 `asm("SYSCALL #N")`、再 `return 某个局部量`」这种取值法**(**2026-10-04 复验仍成立**)。
* ② **Do not write "`asm("SYSCALL #N")` then `return some local`" as the way to get the result** (**still holds, re-checked 2026-10-04**).
* 原理:C 前端对**带 `#` 的 SYSCALL** 会额外补一条 `move [R12-4], R0`,把结果塞进一个**硬编码槽位**;
* Why: the C front end appends an extra `move [R12-4], R0` after a **SYSCALL carrying a `#`**, storing the result in a **hardcoded slot**,
* 函数的局部量却在 R12-8 —— 两者不是一个格子,于是读到的是**未初始化的栈内容**。
* while the function's locals live at R12-8 — not the same cell, so you read **uninitialized stack contents**.
* 实测(2026-10-04):`int badfopen(...){ int r; asm("SYSCALL #110, ${path}, ${mode}"); return r; }` 取回 **0**,
* Measured (2026-10-04): `int badfopen(...){ int r; asm("SYSCALL #110, ${path}, ${mode}"); return r; }` returned **0**,
* 而 `return asm("SYSCALL #110, ${path}, ${mode}");` 那条取回正确的句柄 **1**。⇒ **asm 必须当表达式用**。
* whereas `return asm("SYSCALL #110, ${path}, ${mode}");` returned the correct handle **1**. ⇒ **asm must be used as an expression**.
* ⚠ **2026-10-07 更新**:本文件那层手写的 `sys_*` 已经删掉,改调 `#include <vml_sys.h>` 里的库函数
* —— 上面这条"`asm` 必须当表达式用"如今是**写库那一层**的规矩(见 `Lib/shared/src/vmlsys.c` 文件头)。
* Updated 2026-10-07: the hand-rolled `sys_*` layer is gone in favour of `#include <vml_sys.h>`, and the
* "use `asm` as an expression" rule above now belongs to the **library** layer (`Lib/shared/src/vmlsys.c`).
*
* ③ ~~**函数名别叫 `f1` / `f2` / `d0` / `l3` 这类「一个字母 + 纯数字」**。~~
* ③ ~~**Do not name functions `f1` / `f2` / `d0` / `l3` — a single letter plus digits**.~~
* ✅ **已修好(2026-10-04 复验)**:寄存器形的函数名现在正常(见 `Examples/c/test/regname.c`)。
* ✅ **fixed (re-checked 2026-10-04)**: register-shaped function names work now (see `Examples/c/test/regname.c`).
* 避开这类的写法**照旧保留**(能跑就别动);**新写的代码不必再避**。
* Avoiding such names **stays** (if it runs, do not touch it); **new code need not avoid them**.
*
* 版面:**不越过第 80 列** —— `tty_*` 有个已量到的缺陷,输出跨过第 80 列会插进一条定位序列、
* Layout: **stay inside column 80** -- `tty_*` has a measured defect: crossing column 80 splices in
* 之后整屏变乱码(复现说明见 `demo_tty.c`)。本程序最宽 46 列。
* a positioning sequence and garbles the rest (repro note in `demo_tty.c`). This program is 46 columns at its widest.
*/
#include <vml_ui.h>
#include <stdlib.h> /* itoa --- digits to text via the library */
#include <vml_sys.h> /* vml_file_* —— 本文件不再自己包汇编 */
#include <http.h> /* http_get —— DNS/套接字/剥头/状态码全在库里,不再手写 */
#define HOST "api.open-meteo.com"
#define PORT 80
/* 请求路径(含查询串)—— 交给库里的 `http_get`,它自己拼请求行与那套头。
* The request path (query included); `http_get` in the library builds the request line and headers. */
#define 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"
#define OUTFILE "weather.txt"
#define BUFCAP 8192 /* 响应缓冲:CSV 正文实测 ~250 字节,8K 绰绰有余 */
/* Response buffer: the CSV body measures ~250 bytes, so 8K is plenty. */
#define MAXROUNDS 64 /* 读循环上限:防「对端不关连接」时死读。⚠ 注释不能跨行写在 #define 里 —— */
/* Read-loop cap: stops an endless read when the peer never closes. ⚠ Do not span lines in a #define — */
/* 预处理器按行吃掉宏体,注释的第二行会掉出来当代码。 */
/* the preprocessor eats the macro body line by line and the second comment line falls out as code. */
/* ── 系统调用返回值落点 ─────────────────────────────────────────────── */
/* -- Where syscall return values land -- */
/* ⚠ 这里原先有一层手写的 `sys_*` 包装(9 个 `asm("SYSCALL #nnn, …")` + 全局 `_sysret` 中转),
* 外加一个 `_sysret` 全局量。2026-10-07 起改调库(`#include <vml_sys.h>`):
* `sys_dns` → `vml_dns_resolve`(返回 `const char*`;**失败时是 -1 而不是 0**,见下面调用点)
* `sys_socket` → `vml_socket_create(2, 1)`(AF_INET / SOCK_STREAM)
* `sys_connect` → `vml_socket_connect` `sys_send` → `vml_socket_send`
* `sys_recv` → `vml_socket_recv` `sys_sockclose` → `vml_socket_close`
* `sys_fileopen` → `vml_file_open` `sys_fileread` → `vml_file_read`
* `sys_filewrite` → `vml_file_write` `sys_fileclose` → `vml_file_close`
* 这 9 个号**库里早就包好了**,抄在这里只会多一份要跟着改的东西。
* The hand-rolled `sys_*` wrappers and the `_sysret` global are gone; the library functions above
* (via `#include <vml_sys.h>`) cover the same nine syscall numbers.
*/
/* ── 小工具 ─────────────────────────────────────────────────────────── */
/* -- Small helpers -- */
/* 拼请求。`format=csv` 是这一版新加的:open-meteo 支持直接回 CSV,
* Build the request. `format=csv` is new in this version: open-meteo can return CSV directly,
* 正文长这样(前两行是表头与空行,数据从第 3 行起):
* and the body looks like this (the first two lines are a header row and a blank one; data starts on line 3):
* time,temperature_2m_max (°C),temperature_2m_min (°C),weather_code (wmo code)
* 2026-10-07,28.1,19.8,3
* 用 HTTP/1.0 + Connection: close ⇒ 服务端回完就关连接,读循环靠 recv 返回 0 收尾。
* HTTP/1.0 plus Connection: close means the server closes once it has replied, and the read loop ends on recv returning 0.
*/
/* ⚠ 这里原先有三个手写的协议辅助函数:`build_request`(拼请求行 + 那套头)、
* `find_body`(找 `\r\n\r\n`)、`http_code`(解析状态码)—— 约 45 行。
* 它们与库 `Lib/shared/src/http.c` 里的实现**逐字同源**(连 `User-Agent: Dolaima-VML/1.0`
* 和 `Connection: close` 都一样),只是那份还多做了重定向与 chunked ⇒ 全部删掉,
* 改用 `#include <http.h>` 的 `http_get`。
* These three hand-written protocol helpers were byte-for-byte the same as the library's own
* (`Lib/shared/src/http.c`), which additionally handles redirects and chunked bodies -- so they
* are gone in favour of `http_get` from `#include <http.h>`.
*/
/* 把一行 CSV 切成 4 段(原地改写:逗号换成 0)。返回实际段数。
* Split one CSV line into 4 fields (rewriting in place: commas become NUL). Returns the field count.
* 不用指针数组:VML 的 C 前端对「数组存指针」支持得晚,四段直接写死更稳。
* No pointer array here: VML's C front end supported "arrays of pointers" late, so four fields spelled out is safer.
*/
int split_csv(char* line, char* f1, char* f2, char* f3, char* f4) {
int i;
int fi;
int o;
char ch;
i = 0;
fi = 0;
o = 0;
f1[0] = 0;
f2[0] = 0;
f3[0] = 0;
f4[0] = 0;
while (line[i] != 0) {
ch = line[i];
if (ch == ',') {
if (fi == 0) f1[o] = 0;
else if (fi == 1) f2[o] = 0;
else if (fi == 2) f3[o] = 0;
fi = fi + 1;
o = 0;
i = i + 1;
continue;
}
if (fi == 0 && o < 23) f1[o] = ch;
else if (fi == 1 && o < 15) f2[o] = ch;
else if (fi == 2 && o < 15) f3[o] = ch;
else if (fi == 3 && o < 15) f4[o] = ch;
o = o + 1;
i = i + 1;
}
if (fi == 0) f1[o] = 0;
else if (fi == 1) f2[o] = 0;
else if (fi == 2) f3[o] = 0;
else f4[o] = 0;
return fi + 1;
}
/* WMO 天气代码 → 一句话。代码表来自 open-meteo 的文档(0 晴 / 1-3 云 / 45 雾 / 5x 毛毛雨 /
* WMO weather code -> a short phrase. The table follows open-meteo's docs (0 clear / 1-3 cloud / 45 fog / 5x drizzle /
* 6x 雨 / 7x 雪 / 8x 阵雨 / 9x 雷暴)。
* 6x rain / 7x snow / 8x showers / 9x thunderstorm).
*/
char* wmo_label(int code, int lang) {
if (code == 0) return lang == 0 ? "晴" : "clear";
if (code == 1 || code == 2) return lang == 0 ? "多云" : "cloudy";
if (code == 3) return lang == 0 ? "阴" : "overcast";
if (code == 45 || code == 48) return lang == 0 ? "雾" : "fog";
if (code >= 51 && code <= 57) return lang == 0 ? "毛毛雨" : "drizzle";
if (code >= 61 && code <= 67) return lang == 0 ? "雨" : "rain";
if (code >= 71 && code <= 77) return lang == 0 ? "雪" : "snow";
if (code >= 80 && code <= 82) return lang == 0 ? "阵雨" : "showers";
if (code >= 95) return lang == 0 ? "雷暴" : "storm";
return lang == 0 ? "未知" : "unknown";
}
/* 温度条:一格 = 2℃(10℃ 起,到 34℃ 满 12 格)。纯装饰,但一眼看得出哪天热。
* Temperature bar: one cell = 2 degrees (from 10 up to 34 for a full 12 cells). Purely decorative, but you see the hot day at a glance.
*/
void draw_bar(int tmax, int row) {
int n;
int i;
n = (tmax - 10) / 2;
if (n < 0) n = 0;
if (n > 12) n = 12;
tty_goto(30, row);
tty_color(9, 0);
i = 0;
while (i < n) {
tty_puts("#");
i = i + 1;
}
tty_color(7, 0);
}
#define IDLE_EXIT_MS 8000
/* 画布尺寸与解析结果。**一律扁平数组**:二维数组与"指针数组"在各语言前端之间最不稳,
* Canvas size and parsed rows. **Flat arrays only**: 2-D arrays and arrays of pointers are the least
* portable forms across the front ends,
* 而例程是要照着移植到 22 门语言的(本仓点名的头号坑就是"同一段代码写 22 遍")。
* and these samples are meant to be ported to all 22 languages (writing the same code 22 times is this repo's number-one pitfall).
*/
static int SW;
static int SH;
static int ui_lang;
/* 7 天 × 12 字节(只存 `MM-DD`)*/
/* 7 days x 12 bytes (only `MM-DD`) */
static char d_dates[84];
static int d_lo[8];
static int d_hi[8];
static int d_code[8];
static int d_n;
static int offline; /* 1 = 没连上,显示的是本地缓存 */
/* 1 = offline, showing the local cache */
#define C_BG 0xFF0E1116
#define C_CARD 0xFF1A1F28
#define C_TITLE 0xFFE8E8F0
#define C_DIM 0xFF9AA0B0
#define C_HI 0xFFE06C50
#define C_LO 0xFF4A90D9
/* 整数 → 十进制串(`ui_2d_text` 只收字符串,第 4 层没有 `print_int` 那种东西)
* Integer to a decimal string (`ui_2d_text` takes strings only; layer 4 has no `print_int`).
* ⚠ 返回的是**同一个静态缓冲**,所以别在一个表达式里连着用两次 —— 要用两次就先 `strcat` 到自己的串上。
* ⚠ It returns **one static buffer**, so never use it twice inside a single expression -- append it into your
* own string with `strcat` first if you need two numbers.
*/
static char numbuf[16];
static char *numstr(int v) {
itoa(v, numbuf);
return numbuf;
}
static void layout(void) {
SW = ui_scr_get_w();
SH = ui_scr_get_h();
if (SW <= 0) SW = 360;
if (SH <= 0) SH = 620;
}
static void draw(void) {
int cx = SW / 2;
int pad = SW / 18;
int top;
int avail;
int cardh;
int i;
if (pad < 6) pad = 6;
top = pad + 58; /* 标题两行 */
/* room for the two heading lines */
avail = SH - top - 40;
cardh = avail / (d_n > 0 ? d_n : 1);
ui_2d_clear(C_BG);
ui_2d_text_styled(cx, pad, ui_lang == 0 ? "一周预报" : "One-week forecast",
C_TITLE, 17, VML_ANCHOR_CENTER, VML_FONT_BOLD);
ui_2d_text(cx, pad + 26,
offline != 0 ? (ui_lang == 0 ? "离线:读本地缓存 weather.txt" : "offline: local cache weather.txt")
: (ui_lang == 0 ? "数据源 api.open-meteo.com" : "source api.open-meteo.com"),
offline != 0 ? 0xFFE0A040 : C_DIM, 11, VML_ANCHOR_CENTER);
for (i = 0; i < d_n; i++) {
int y = top + i * cardh;
int h = cardh - 6;
int midy = y + h / 2;
char ds[8];
char tb[16];
int k;
int barw;
int innerw;
ui_2d_rect(pad, y, SW - pad * 2, h, C_CARD, 1, 0, 8);
/* 日期(从扁平缓冲搬进一个小局部串 —— 不取数组元素的地址) */
/* The date (copied out of the flat buffer into a small local string -- no address-of on an array element) */
k = 0;
while (k < 6) { ds[k] = d_dates[i * 12 + k]; k = k + 1; }
ds[6] = 0;
ui_2d_text(pad + 12, midy - 8, ds, C_TITLE, 14, VML_ANCHOR_LEFT);
/* 天气一句话 */
/* The condition in words */
ui_2d_text(SW - pad - 12, midy - 7, wmo_label(d_code[i], ui_lang), C_DIM, 12, VML_ANCHOR_RIGHT);
/* 最低~最高 + 温度条(10℃ 起,34℃ 满条) */
/* Low~high plus a temperature bar (starts at 10 C and fills at 34 C) */
tb[0] = 0;
strcat(tb, numstr(d_lo[i]));
strcat(tb, "~");
strcat(tb, numstr(d_hi[i]));
ui_2d_text(pad + 12, midy + 10, 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, 0xFF2A3038, 1, 0, 3);
ui_2d_rect(pad + 12, midy + 26, barw, 6, d_hi[i] >= 30 ? C_HI : C_LO, 1, 0, 3);
}
ui_2d_present(); /* ⚠ 不调这句 = 黑屏 */
/* without this call you get a black screen */
}
/* ── 主流程 ─────────────────────────────────────────────────────────── */
/* -- Main flow -- */
char rbuf[BUFCAP]; /* 整个 HTTP 响应(头 + 正文),离线时当缓存正文用 */
/* The whole HTTP response (headers plus body); used as the cache body when offline. */
int main(void) {
char req[640];
char* ip;
int fd;
int port;
int code;
int total;
int n;
int want;
int addr;
int head;
int blen;
int rounds;
int fh;
int mode;
int wrote;
int lang; /* 界面语言:开局查一次(ui_scr_get_language 是 syscall,别每帧调) */
/* UI language: queried once at start (ui_scr_get_language is a syscall, do not call it every frame) */
lang = ui_scr_get_language();
offline = 0;
blen = 0;
head = 0;
print_str(lang == 0 ? "=== VML 天气预报 ===\n" : "=== VML Weather Report ===\n");
print_str(lang == 0 ? "主机 " : "Host "); print_str(HOST);
print_str(lang == 0 ? " 文件 " : " file "); print_str(OUTFILE); print_str("\n");
/* ① 域名 → IP —— ⚠ **这一步仍要自己做**:`http.c` 头部明说它**不代劳** DNS
* (宿主 `#334` 只吃 `IPAddress.Parse` 认得的那串点分十进制,喂它域名直接抛异常),
* 而且在库里做会把一次 `http_get` 变成两次网络往返 ⇒ 它把这条留给了调用方。
* Resolve the name yourself: `http.c` deliberately does **not** do DNS (`#334` only parses a
* dotted-quad, and doing it in the library would make one `http_get` into two round trips).
* ⚠ 失败时宿主把 R0 置 **-1**(不是 0)⇒ 判据必须是 `<= 0`。 */
if (offline == 0) {
ip = (char*)vml_dns_resolve(HOST);
if ((int)ip <= 0) {
print_str(lang == 0 ? "[警告] DNS 解析失败(转为离线)\n" : "[WARN] DNS lookup failed (going offline)\n");
offline = 1;
} else {
print_str("[1/4] DNS "); print_str(ip); print_str("\n");
}
}
/* ②③ 抓数据 —— 建套接字、连接、发请求、收完整响应、剥头、状态码**全在库里**
* (`Lib/shared/src/http.c` 的 `http_get`;它拼的那套请求头与本例程原先手写的**逐字相同**,
* 另外还多做了重定向与 chunked 解码)。返回值 = **HTTP 状态码**或负错误码。
* The rest of the fetch (socket, connect, send, read, strip headers, status code) lives in the
* library's `http_get`, which returns the **HTTP status code** or a negative error code. */
if (offline == 0) {
code = http_get_to(ip, PORT, HOST, PATH, rbuf, BUFCAP); /* 连 IP、Host 头写域名 */
/* connect to the IP, `Host:` carries the name */
if (code != 200) {
print_str(lang == 0 ? "[警告] 抓取失败(转为离线)\n" : "[WARN] fetch failed (going offline)\n");
offline = 1;
} else {
blen = strlen(rbuf); /* `http_get` 只往回装**正文**(响应头已经剥掉了)*/
/* `http_get` stores **only the body** (headers already stripped) */
head = 0; /* 正文从第 0 字节起 —— 下面写文件用 `rbuf + head` */
print_str("[1/4] HTTP "); print_int(code);
print_str(lang == 0 ? " 正文 " : " body "); print_int(blen);
print_str(lang == 0 ? " 字节\n" : " bytes\n");
}
}
/* ④ 剥头 → 写文件(离线时跳过)*/
/* ④ Strip the headers and write the file (skipped when offline). */
if (offline == 0) {
mode = 1; /* 1 = 只写(FileMode.Create),见 VMLRuntime 的 #110 */
/* 1 = write only (FileMode.Create), see #110 in VMLRuntime. */
fh = vml_file_open(OUTFILE, mode);
if (fh < 0) {
print_str(lang == 0 ? "[警告] 打不开 " : "[WARN] cannot open "); print_str(OUTFILE);
print_str(lang == 0 ? "(手机上路径必须是相对的)\n" : " (paths must be relative on the phone)\n");
} else {
addr = (int)rbuf + head;
wrote = vml_file_write(fh, addr, blen);
vml_file_close(fh);
print_str("[4/4] HTTP "); print_int(code);
print_str(lang == 0 ? " 正文 " : " body "); print_int(blen); print_str(lang == 0 ? " 字节" : " bytes");
print_str(lang == 0 ? " 写入 " : " wrote "); print_int(wrote); print_str(lang == 0 ? " 字节 -> " : " bytes -> ");
print_str(OUTFILE); print_str("\n");
/* 正文留在 `rbuf` 里(从 `head` 起),下面直接显示,不必再读一次文件 */
/* The body stays in `rbuf` (from `head` on), so the display below needs no second read */
}
}
/* ⑤ 离线回落:读上一次留下的 weather.txt(相对路径,各宿主自己解析)
* ⑤ Offline fallback: read the weather.txt left by the last run (a relative path each host resolves itself)
* ⚠ `mode = 0` 是**只读**:写成 1 会把缓存文件清空再读,拿到一份空数据。
* ⚠ `mode = 0` is **read-only**: passing 1 would truncate the cache file first and hand back nothing.
*/
if (offline != 0) {
mode = 0;
fh = vml_file_open(OUTFILE, mode);
if (fh < 0) {
print_str(lang == 0 ? "[失败] 离线且没有本地缓存 " : "[FAIL] offline and no local cache ");
print_str(OUTFILE);
print_str(lang == 0 ? " —— 先在联网的机器上跑一次,或把预报 CSV 放到同目录。\n"
: " -- run this once on a networked machine, or drop the forecast CSV next to it.\n");
return 1;
}
blen = vml_file_read(fh, (int)rbuf, BUFCAP - 1);
vml_file_close(fh);
if (blen <= 0) {
print_str(lang == 0 ? "[失败] 缓存文件是空的\n" : "[FAIL] the cache file is empty\n");
return 2;
}
rbuf[blen] = 0;
head = 0; /* 缓存里没有响应头,正文从 0 开始 */
/* The cache has no response headers, so the body starts at 0 */
}
/* ⑥ 解析成结构化的「天」*/
/* 6. Parse the CSV into structured days */
d_n = 0;
{
int i;
int li;
int row_ok;
char ch;
char line[96];
char f1[24];
char f2[16];
char f3[16];
char f4[16];
li = 0;
i = head;
while (i <= head + blen) {
ch = (i < head + blen) ? rbuf[i] : 10;
if (ch != 10) {
if (li < 95) { line[li] = ch; li = li + 1; }
i = i + 1;
continue;
}
line[li] = 0;
li = 0;
i = i + 1;
/* 只认「日期形状」的行:第 1 段必须是 `YYYY-MM-DD`。
* Only lines shaped like a date count: the first field must be `YYYY-MM-DD`.
* ⚠ 别只判"数字开头" —— open-meteo 的 CSV **第 2 行是经纬度那行**
* ⚠ Do not settle for "starts with a digit" -- line 2 of open-meteo's CSV is the latitude/longitude row
* (`22.530754,114.08714,8.0,28800,...`),数字开头,判错了会多出一行"日期"、
* (`22.530754,114.08714,8.0,28800,...`), which does start with a digit; judging it wrong adds a bogus
* 平均温也跟着歪(实测:多算一天、平均 19~40℃)。
* "day" and skews the averages (measured: one extra day, average 19-40 C).
*/
row_ok = 1;
if (line[0] < '0' || line[0] > '9') row_ok = 0;
if (line[4] != '-' || line[7] != '-') row_ok = 0;
if (row_ok == 0) continue;
if (split_csv(line, f1, f2, f3, f4) < 4) continue;
if (d_n >= 7) break;
/* 日期只留 `MM-DD`(年前两位对一周预报没意义,省下的宽度留给温度)
* Keep only `MM-DD` from the date (the century is noise for a week and the width is better spent on temperature) */
d_dates[d_n * 12 + 0] = f1[5];
d_dates[d_n * 12 + 1] = f1[6];
d_dates[d_n * 12 + 2] = f1[7];
d_dates[d_n * 12 + 3] = f1[8];
d_dates[d_n * 12 + 4] = f1[9];
d_dates[d_n * 12 + 5] = 0;
d_lo[d_n] = atoi(f3);
d_hi[d_n] = atoi(f2);
d_code[d_n] = atoi(f4);
d_n = d_n + 1;
}
}
if (d_n == 0) {
print_str(lang == 0 ? "[失败] 没解析出数据行(CSV 格式变了吗?)\n"
: "[FAIL] no data rows parsed (did the CSV format change?)\n");
return 3;
}
/* ⑦ 开窗 → 画 → 消息循环 */
/* Open a window, draw, then run the message loop */
ui_lang = lang;
layout();
ui_win_open_ex("weather.c", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
layout();
draw();
{
int msg[4];
int t;
int t0;
int done;
t0 = ui_time_get_ms();
done = 0;
while (!done && ui_win_is_closed() == 0) {
t = ui_msg_wait(msg, 200);
if (t == VML_MSG_NONE) {
if (ui_time_get_ms() - t0 > IDLE_EXIT_MS) done = 1; /* 空闲超时(验收用)*/
/* idle timeout (for automated checks) */
continue;
}
t0 = ui_time_get_ms();
if (t == VML_MSG_KEYDOWN) {
int k = msg[1];
if (k == VML_KEY_ESCAPE || k == 'q' || k == 'Q') done = 1;
else if (k == VML_KEY_LEFT || k == VML_KEY_RIGHT || k == VML_KEY_UP || k == VML_KEY_DOWN) draw();
continue;
}
if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) { layout(); draw(); continue; }
if (t == VML_MSG_WINDOWCLOSE) done = 1;
}
}
return 0;
}