basic/app/weather.bas · 43286 字节 · 下载原文
' weather.bas —— 常用小程序②:**天气** —— 由 `Examples/c/app/weather.c` 移植
' weather.bas -- Everyday utility #2: **weather**, ported from `Examples/c/app/weather.c`
'
' 四段:① 裸套接字取回 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 --
' 所以不能走 https);② 剥掉响应头,把正文写进 weather.txt(**相对路径**:手机上沙箱根 = 工作区);
' there is no TLS in the VM, so https is impossible); (2) strip the response headers and write the body into
' ③ 解析 CSV(**要 `format=csv`**,比 JSON 少一整层解析);④ 用 `ui_*` 画一张彩色的周预报。
' weather.txt (**relative path**: on the phone the sandbox root is the workspace); (3) parse the CSV (**ask
' for `format=csv`**, which saves a whole parsing layer over JSON); (4) draw a colour week card with `ui_*`.
'
' ⚠ **抓不到就回落读本地**(用户 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
' 就去读上一次留下的 `weather.txt` 并照常显示,只在标题上标一行"(离线:读本地缓存)"。
' fallback"): 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)".
'
' 跑法(桌面脚手架,秒级,别为改一行去打 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 third_party/vml/Examples/basic/app/weather.bas \
' --vml-home third_party/vml --timeout 60
' 手机上:命令行页敲 `vml run examples/basic/app/weather.bas`,文件落在工作区根的 weather.txt。
' On the phone: type `vml run examples/basic/app/weather.bas`; the file lands in weather.txt at the workspace root.
'
' ── BASIC 这一门的三条坑(都实测过;本文件按绕开的写法写)────────────────
' -- Three BASIC-only traps (all measured; this file is written around them) --
' ① **循环终结符是 `LOOP`**(`DO WHILE … LOOP`),写 `END DO` 不报错、控制流静默歪掉;
' ① **The loop terminator is `LOOP`** (`DO WHILE ... LOOP`); `END DO` reports nothing and skews control flow;
' 本文件一律 `WHILE … WEND`。退出循环用标志位,不用 `EXIT WHILE`。
' this file always uses `WHILE ... WEND`, and leaves loops through a flag, never through `EXIT WHILE`.
' ② **单行 `IF … THEN …` 嵌在块 IF 的 THEN 里、后面再跟 `ELSE` 会让整个外层循环当场退出**
' ② **An inline `IF ... THEN ...` nested inside a block IF's THEN, followed by `ELSE`, exits the whole outer loop**
' (实测:本该跑 13 轮只跑了 1 轮)⇒ 本文件**所有分支都是多行块**;也不用内联 IF。
' (measured: a loop that should run 13 times ran once) => **every branch here is a multi-line block**, and no inline IFs.
' ③ **有返回值 + 当语句裸调的函数会被静默丢弃** ⇒ 每个有返回值的库函数都把结果**赋给变量再用**。
' ③ **A bare call to a function with a return value is silently dropped** => every such call assigns to a variable first.
'
' ── 网络与缓冲:BASIC 与 C 的四处**必须**不同的写法(不是风格,是能力边界)──────
' -- Networking and buffers: four places BASIC **must** differ from C (capability, not style) --
' ① **字符串临时量上限 256 字节**(库里的轮转池 `_strbuf[8][256]`,拼接也在 256 字节处截断)。
' ① **String temporaries cap out at 256 bytes** (the library's rotating pool is `_strbuf[8][256]`, and concatenation truncates there too).
' 而 HTTP 响应有 300~3400 字节 ⇒ **响应缓冲必须是一块 8192 字节的字面量**(`rbuf`),
' An HTTP response is 300-3400 bytes => **the response buffer must be an 8192-byte string literal** (`rbuf`),
' `recv` 收进它的**地址**(形参声明成 `INTEGER` 地址),读的时候直接 `MID$` 这个字面量。
' `recv` writes to its **address** (declared as an `INTEGER` address) and reading is plain `MID$` on the literal.
' ⚠ 直接写 `SPACE$(8192)` 是**错的**:`basic_space` 在 255 处封顶,`recv` 会写到隔壁去。
' ⚠ Writing `SPACE$(8192)` is **wrong**: `basic_space` caps at 255 and `recv` would scribble past it.
' ⚠ **别用 `vml_alloc`**:那个堆的起点(实测 ~102400)**落在本程序自己的数据段里**,
' ⚠ **Do not use `vml_alloc`**: its heap base (~102400 measured) **lands inside this program's own data segment**,
' 往里写就把 BASIC 的全局变量覆盖掉(实测:读一个 81 字节的文件,`base` 变成文件里的 4 个字节、
' so writing into it overwrites BASIC globals (measured: reading an 81-byte file turned `base` into four bytes
' `blen` 变成另外 4 个)—— 而且**覆不覆盖取决于变量声明顺序**,是个看运气的雷。
' of the file and `blen` into four others) -- and **whether it happens depends on declaration order**: pure luck.
' ② **`buf + 偏移` 不能当 recv 的目标**:`字符串 + 整数` 走的是**字符串拼接**(且截断到 256 字节),
' ② **`buf + offset` cannot be a `recv` target**: `string + integer` is **string concatenation** (truncated at 256 bytes),
' 于是偏移一路为 0、数据全落在临时量上(实测:接收成功 560 字节,缓冲里一个字节没变)。
' so the offset stays 0 and the data lands in a temporary (measured: 560 bytes received, the buffer unchanged).
' 本文件的读循环用**地址整数**相加,再收进 `base + total`。
' This file's read loop adds **raw addresses** and receives into `base + total`.
' 地址从哪来?`base = strcpy(rbuf, rbuf)` —— `strcpy` 返回 `dst`,自拷一次就拿到这块字面量的地址
' Where does the address come from? `base = strcpy(rbuf, rbuf)` -- `strcpy` returns `dst`, so one self-copy
' (这是本方言里唯一能"把字符串变成地址"的现成出口;`VARPTR` 是桩、返回 0)。
' hands back the literal's address (the only ready-made exit in this dialect; `VARPTR` is a stub returning 0).
' ③ **请求分几次 `send` 出去**:整条请求串 254 字节,正好卡在 256 的边上;
' ③ **The request goes out in several `send` calls**: the whole request is 254 bytes, right at the 256-byte edge;
' TCP 是流,分成几段发出去服务端收到的是**同一串字节**(C 版一次发完只是因为 C 的缓冲没有这个上限)。
' TCP is a stream, so sending it in pieces puts **the same bytes** on the wire (C sends it in one call only
' because C has no such cap).
' ④ **`vml_dns_resolve` 的返回类型声明成 `INTEGER` 而不是 `STRING`**:DNS 失败时它回 **-1**,
' ④ **`vml_dns_resolve` is declared `INTEGER`, not `STRING`**: on a failed lookup it returns **-1**,
' 而"字符串"读 `LEN()` 会去解引用 -1(症状是"内存错误"而不是"DNS 失败");先按整数收,
' and reading it as a string with `LEN()` dereferences -1 (the symptom is "memory error", not "DNS failure").
' 确认 > 0 之后再把它绑成字符串(`ip = ipaddr`:INTEGER 赋给字符串变量就是"这个地址上的 C 串")。
' Collect it as an integer, check it is > 0, and only then bind it (`ip = ipaddr` hands back the C string at that address).
'
' 版面:**不越过第 80 列**的那条约束是 `tty_*` 的;本文件走 `ui_*`,版式由宿主尺寸算出来。
' Layout: the "stay inside column 80" rule belongs to `tty_*`; this file uses `ui_*` and computes the layout from the host size.
' ── UI 接口:引用**库侧**的声明文件 ────────────────────────────────
' ── UI interface: include the **library-side** declaration file ────────────────────────────────
'$INCLUDE: 'vml_ui.bas'
' ── 共享库里的网络 / 文件 / 堆(都在 `Lib/shared/vmlsys.vml`,随 builtins 自动链上)──
' ── Networking / files / heap from the shared library (all in `Lib/shared/vmlsys.vml`, auto-linked with builtins) ──
' ⚠ 形参写 `AS STRING` 就是"把这个串的地址传过去"(`ui_clipboard_get(buf AS STRING, cap)` 同一套);
' ⚠ A parameter written `AS STRING` means "pass this string's address" (same scheme as `ui_clipboard_get(buf AS STRING, cap)`);
' `buf` 写 `AS INTEGER` 则是"这个整数就是地址",两者按**被调方要写还是要读**来挑。
' `buf` written `AS INTEGER` means "this integer is the address"; pick per whether the callee writes or reads.
' ⚠ **指针类形参没有 BASIC 声明文件可引用**(`Lib/basic/net.bas` 把 buf 声明成 INTEGER,
' ⚠ **There is no BASIC declaration file to include for pointer parameters** (`Lib/basic/net.bas` declares buf as INTEGER,
' 那样就只能自己造地址),所以这一层像 C 版自己的 `sys_*` 一样写在文件里。
' which forces you to build an address yourself), so this layer lives in the file, exactly like the C version's own `sys_*`.
NATIVE FUNCTION vml_dns_resolve(hostname AS STRING) AS INTEGER
NATIVE FUNCTION vml_socket_create(domain AS INTEGER, stype AS INTEGER) AS INTEGER
NATIVE FUNCTION vml_socket_connect(fd AS INTEGER, ip AS STRING, port AS INTEGER) AS INTEGER
NATIVE FUNCTION vml_socket_send(fd AS INTEGER, buf AS STRING, ln AS INTEGER) AS INTEGER
NATIVE FUNCTION vml_socket_recv(fd AS INTEGER, addr AS INTEGER, ln AS INTEGER) AS INTEGER
NATIVE FUNCTION vml_socket_close(fd AS INTEGER) AS INTEGER
' `http_get_to`:**连接地址与 Host 头分开** —— 连 IP(宿主不解析域名)、`Host:` 头写域名。
' (虚拟主机/CDN 按 Host 路由,而 `http_get` 把这两件事绑在一个参数上 ⇒ 传 IP 只会拿到 404。)
' `http_get_to`: connect to the IP while `Host:` carries the hostname (`http_get` binds both to one argument).
NATIVE FUNCTION http_get_to(ip AS STRING, port AS INTEGER, hostname AS STRING, path AS STRING, out AS INTEGER, cap AS INTEGER) AS INTEGER
' 上一次的**正文真实长度** —— ⚠ BASIC 里没有「按地址取长度」(`LEN` 只吃字符串),
' 而 `http_get_to` 写回的是地址 ⇒ 长度只能问库要这一个。
' The true body length of the last response -- BASIC has no "length at an address" (`LEN` takes a string).
NATIVE FUNCTION http_last_body_len() AS INTEGER
' `strcpy` 只借它"返回 dst"这一条:自拷一次就把一块字面量的地址拿到手(见文件头 ②)。
' `strcpy` is borrowed purely for "it returns dst": one self-copy yields a literal's address (see pitfall 2).
NATIVE FUNCTION strcpy(dst AS STRING, src AS STRING) AS INTEGER
NATIVE FUNCTION vml_file_open(path AS STRING, mode AS INTEGER) AS INTEGER
NATIVE FUNCTION vml_file_read(handle AS INTEGER, addr AS INTEGER, ln AS INTEGER) AS INTEGER
NATIVE FUNCTION vml_file_write(handle AS INTEGER, addr AS INTEGER, ln AS INTEGER) AS INTEGER
NATIVE FUNCTION vml_file_close(handle AS INTEGER) AS INTEGER
' ══ 常量 ══════════════════════════════════════════════════════
' ══ Constants ══════════════════════════════════════════════════════
DIM HOST AS STRING
DIM OUTFILE AS STRING
DIM BUFCAP AS INTEGER
DIM MAXROUNDS AS INTEGER
DIM IDLE_EXIT_MS AS INTEGER
DIM PORT AS INTEGER
' 配色(真彩 AA RRGGBB)
' Colours (true colour AARRGGBB)
DIM C_BG AS INTEGER
DIM C_CARD AS INTEGER
DIM C_TITLE AS INTEGER
DIM C_DIM AS INTEGER
DIM C_HI AS INTEGER
DIM C_LO AS INTEGER
DIM C_WARN AS INTEGER
' ══ 全局状态 ══════════════════════════════════════════════════
' ══ Global state ══════════════════════════════════════════════════
DIM SW AS INTEGER
DIM SH AS INTEGER
DIM ui_lang AS INTEGER
DIM base AS INTEGER
DIM rbuf AS STRING
DIM total AS INTEGER
DIM head AS INTEGER
DIM blen AS INTEGER
DIM offline AS INTEGER
DIM g_first AS INTEGER
' 解析结果:**扁平定长**(一天一格),与 C 版的 d_* 一一对应。
' Parsed result: **flat and fixed-size** (one cell per day), one for one with the C version's d_*.
' ⚠ 日期不存字符串数组(`DIM a(8) AS STRING` 实测是坏的,读出来是地址)—— 存**在缓冲里的偏移**。
' ⚠ Dates are not kept in a string array (`DIM a(8) AS STRING` is measurably broken: it reads back addresses);
' the **offset into the buffer** is kept instead.
DIM d_doff(8) AS INTEGER
DIM d_lo(8) AS INTEGER
DIM d_hi(8) AS INTEGER
DIM d_code(8) AS INTEGER
DIM d_n AS INTEGER
' CSV 切分的结果(4 段的起点/长度),由 `split4` 写、`parse_csv` 读。
' The CSV split result (start/length of the four fields), written by `split4` and read by `parse_csv`.
DIM f_off(5) AS INTEGER
DIM f_len(5) AS INTEGER
' 文案(开局按宿主语言算一次)
' Strings (computed once at startup from the host language)
DIM sWin AS STRING
DIM sTitle AS STRING
DIM sOff AS STRING
DIM sSrc AS STRING
DIM sNoData AS STRING
DIM sClear AS STRING
DIM sCloudy AS STRING
DIM sOvercast AS STRING
DIM sFog AS STRING
DIM sDrizzle AS STRING
DIM sRain AS STRING
DIM sSnow AS STRING
DIM sShowers AS STRING
DIM sStorm AS STRING
DIM sUnknown AS STRING
SUB initLang()
ui_lang = ui_scr_get_language()
IF ui_lang = 0 THEN
sWin = "天气(BASIC)"
sTitle = "一周预报"
sOff = "离线:读本地缓存 weather.txt"
sSrc = "数据源 api.open-meteo.com"
sNoData = "没有数据"
sClear = "晴"
sCloudy = "多云"
sOvercast = "阴"
sFog = "雾"
sDrizzle = "毛毛雨"
sRain = "雨"
sSnow = "雪"
sShowers = "阵雨"
sStorm = "雷暴"
sUnknown = "未知"
ELSE
sWin = "Weather (BASIC)"
sTitle = "One-week forecast"
sOff = "offline: local cache weather.txt"
sSrc = "source api.open-meteo.com"
sNoData = "no data"
sClear = "clear"
sCloudy = "cloudy"
sOvercast = "overcast"
sFog = "fog"
sDrizzle = "drizzle"
sRain = "rain"
sSnow = "snow"
sShowers = "showers"
sStorm = "storm"
sUnknown = "unknown"
END IF
END SUB
' ── 缓冲里的第 i 个字节(0 起)。`ASC` 给的是**有符号**字节,负的先加回 256。──
' ── Byte i of the buffer (0-based). `ASC` hands back a **signed** byte, so negatives get 256 added back. ──
FUNCTION gbyte(i AS INTEGER) AS INTEGER
DIM v AS INTEGER
v = ASC(MID$(rbuf, i + 1, 1))
IF v < 0 THEN
v = v + 256
END IF
gbyte = v
END FUNCTION
' 缓冲里 [off, off+ln) 这段十进制整数(C 版用的 `atoi`)。
' The decimal integer in [off, off+ln) of the buffer (the C version's `atoi`).
' ⚠ 不用内建 `VAL`:它回浮点,写进整型数组元素要靠隐式收窄,而那条路在 SUB 里曾经漏收窄
' ⚠ The built-in `VAL` is not used: it returns a float, and storing it into an integer array element relies on
' 隐式收窄(实测症状是"温度显示成 1e-44 那种垃圾")。自己按字节算,返回类型就是整型。
' implicit narrowing (measured symptom: temperatures displaying as junk like 1e-44). Digits are parsed here.
FUNCTION atoi_buf(off AS INTEGER, ln AS INTEGER) AS INTEGER
DIM i AS INTEGER
DIM v AS INTEGER
DIM c AS INTEGER
DIM neg AS INTEGER
v = 0
neg = 0
i = 0
IF ln > 0 THEN
IF gbyte(off) = 45 THEN
neg = 1
i = 1
END IF
IF gbyte(off) = 43 THEN
i = 1
END IF
' ⚠ **遇到第一个非数字就停**(照 `atoi` 的语义):`19.8` 要读成 19,不是 198。
' ⚠ **Stop at the first non-digit** (matching `atoi`): `19.8` must read as 19, not 198.
WHILE i < ln
c = gbyte(off + i)
IF c >= 48 AND c <= 57 THEN
v = v * 10 + (c - 48)
i = i + 1
ELSE
i = ln + 1
END IF
WEND
END IF
IF neg = 1 THEN
v = 0 - v
END IF
atoi_buf = v
END FUNCTION
FUNCTION wmo_label(code AS INTEGER) AS STRING
' 代码表来自 open-meteo 的文档:0 晴 / 1-3 云 / 45 雾 / 5x 毛毛雨 / 6x 雨 / 7x 雪 / 8x 阵雨 / 9x 雷暴。
' The table follows open-meteo's docs: 0 clear / 1-3 cloud / 45 fog / 5x drizzle / 6x rain / 7x snow / 8x showers / 9x thunderstorm.
wmo_label = sUnknown
IF code = 0 THEN
wmo_label = sClear
END IF
IF code = 1 OR code = 2 THEN
wmo_label = sCloudy
END IF
IF code = 3 THEN
wmo_label = sOvercast
END IF
IF code = 45 OR code = 48 THEN
wmo_label = sFog
END IF
IF code >= 51 AND code <= 57 THEN
wmo_label = sDrizzle
END IF
IF code >= 61 AND code <= 67 THEN
wmo_label = sRain
END IF
IF code >= 71 AND code <= 77 THEN
wmo_label = sSnow
END IF
IF code >= 80 AND code <= 82 THEN
wmo_label = sShowers
END IF
IF code >= 95 THEN
wmo_label = sStorm
END IF
END FUNCTION
' 整数 → 十进制串(不补零)。⚠ 借库里的 `STR$` 再 `LTRIM$`,不自己拼字符。
' Integer to a decimal string (no padding). ⚠ Borrow `STR$` plus `LTRIM$` instead of hand-rolling it.
FUNCTION num2s(v AS INTEGER) AS STRING
num2s = LTRIM$(STR$(v))
END FUNCTION
' ⚠ 这里原先有 `find_body`(找 `\r\n\r\n`)与 `http_code`(解析状态码)两个手写辅助 ——
' 库的 `http_get_to` 已经把剥头与状态码都做了,只往回装**正文**(长度另问 `http_last_body_len`)。
' The hand-written `find_body` / `http_code` are gone; `http_get_to` covers both.
' ⚠ 这里原先有 `send_request`(把整条请求**分几次发** —— 254 字节正好卡在 BASIC 字符串
' 临时量 256 字节的边上,见文件头 ③)。现在请求由库里的 `http_get_to` 拼与发,
' 这个 SUB 连同那条"卡边界"的顾虑一起没了。
' The hand-written `send_request` is gone; `http_get_to` builds and sends the request.
' ── CSV 切分:把 [ls, le) 这段切 4 段,起点/长度写进 f_off/f_len ────
' ── CSV split: cut the [ls, le) range into four fields, writing start/length into f_off/f_len ────
' ⚠ 不用指针数组、不取数组元素地址 —— 一律**下标 + 偏移**(本仓多语言移植的头号坑就是"同一段代码写 22 遍")。
' ⚠ No pointer arrays and no address-of on an element -- always **index plus offset** (this repo's number-one
' porting pitfall is having to write the same code 22 times).
SUB split4(ls AS INTEGER, le AS INTEGER)
DIM i AS INTEGER
DIM fi AS INTEGER
DIM fs AS INTEGER
i = ls
fi = 0
fs = ls
WHILE i <= le
IF i = le OR gbyte(i) = 44 THEN
IF fi < 4 THEN
f_off(fi) = fs
f_len(fi) = i - fs
END IF
fi = fi + 1
fs = i + 1
END IF
IF fi > 4 THEN
i = le + 1
ELSE
i = i + 1
END IF
WEND
IF fi < 4 THEN
f_len(0) = 0
END IF
END SUB
' ── 解析 CSV:只认「日期形状」的行(第 1 段必须是 YYYY-MM-DD)───────
' ── Parse the CSV: only lines shaped like a date count (field 1 must be YYYY-MM-DD) ───────
' ⚠ 别只判"数字开头" —— open-meteo 的 CSV **第 2 行是经纬度那行**(`22.53,114.087,8.0,...`),
' ⚠ Do not settle for "starts with a digit" -- line 2 of open-meteo's CSV is the latitude/longitude row
' (`22.53,114.087,8.0,...`),数字开头;判错了会多出一行"日期"、平均温也跟着歪。
' (`22.53,114.087,8.0,...`), which does start with a digit; judging it wrong adds a bogus day and skews the averages.
SUB parse_csv()
DIM i AS INTEGER
DIM ch AS INTEGER
DIM ls AS INTEGER
DIM rowok AS INTEGER
DIM nd AS INTEGER
i = head
ls = head
nd = 0
WHILE i <= head + blen
ch = 10
IF i < head + blen THEN
ch = gbyte(i)
END IF
IF ch <> 10 THEN
i = i + 1
ELSE
rowok = 1
IF gbyte(ls) < 48 OR gbyte(ls) > 57 THEN
rowok = 0
END IF
IF gbyte(ls + 4) <> 45 OR gbyte(ls + 7) <> 45 THEN
rowok = 0
END IF
IF rowok = 1 AND nd < 7 THEN
split4(ls, i)
IF f_len(0) >= 10 THEN
' 日期只留 `MM-DD`(年前两位对一周预报没意义,省下的宽度留给温度)。
' Keep only `MM-DD` from the date (the century is noise for a week; the width goes to temperature).
d_doff(nd) = f_off(0) + 5
d_lo(nd) = atoi_buf(f_off(2), f_len(2))
d_hi(nd) = atoi_buf(f_off(1), f_len(1))
d_code(nd) = atoi_buf(f_off(3), f_len(3))
nd = nd + 1
END IF
END IF
ls = i + 1
i = i + 1
END IF
WEND
d_n = nd
END SUB
' ── 取数据(在线优先,失败回落本地)──────────────────────────────
' ── Acquire the data (network first, local fallback) ──────────────────────────────
SUB acquire()
DIM ipaddr AS INTEGER
DIM ip AS STRING
DIM fd AS INTEGER
DIM n AS INTEGER
DIM want AS INTEGER
DIM code AS INTEGER
DIM rounds AS INTEGER
DIM fh AS INTEGER
DIM reading AS INTEGER
d_n = 0
offline = 0
total = 0
head = 0
blen = 0
IF g_first = 1 THEN
IF ui_lang = 0 THEN
PRINT "=== VML 天气预报 ==="
ELSE
PRINT "=== VML Weather Report ==="
END IF
PRINT "主机 "; HOST; " 文件 "; OUTFILE
END IF
' ① 域名 → IP:宿主**不解析域名**(`#334` 内部是 `IPAddress.Parse`),必须先走 `vml_dns_resolve`。
' (1) Hostname to IP: the host **does not resolve names** (`#334` is `IPAddress.Parse`), so DNS comes first.
' ⚠ 失败时回的是 **-1**(不是 0),所以先按**整数**收、判 `<= 0`,确认之后才绑成字符串(见文件头 ④)。
' ⚠ A failed lookup returns **-1** (not 0), so it is collected as an **integer**, tested `<= 0`, and only
' then bound to a string (see pitfall 4 above).
ipaddr = vml_dns_resolve(HOST)
IF ipaddr <= 0 THEN
IF g_first = 1 THEN
PRINT "[警告] DNS 解析失败(转为离线)"
END IF
offline = 1
ELSE
ip = ipaddr
IF g_first = 1 THEN
PRINT "[1/4] DNS "; ip
END IF
END IF
' ②③ 抓数据 —— 建套接字、连接、发请求、收完整响应、剥头、状态码**全在库里**。
' ⚠ 用 `http_get_to`(不是 `http_get`):连 IP、`Host:` 头写域名 —— 虚拟主机按 Host 路由,
' 而 `http_get` 把这两件事绑在一个参数上 ⇒ 传 IP 只会拿到 404(见声明处那段)。
' The fetch lives in the library's `http_get_to`; the body length comes from `http_last_body_len`.
IF offline = 0 THEN
code = http_get_to(ip, PORT, HOST, "/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", base, BUFCAP)
IF code <> 200 THEN
IF g_first = 1 THEN
PRINT "[警告] 抓取失败(转为离线)"
END IF
offline = 1
ELSE
blen = http_last_body_len() ' 库里记着上一次的正文长度(BASIC 没有"按地址取长度")
head = 0
IF g_first = 1 THEN
PRINT "[3/4] HTTP "; code; " 正文 "; blen; " 字节"
END IF
END IF
END IF
' ④ 剥头 → 写文件(离线时跳过)。mode:**0 = 只读、1 = 只写(先截断)**,两者各写一次、别共用。
' (4) Strip the headers and write the file (skipped when offline). mode: **0 = read, 1 = write (truncating)**; never shared.
IF offline = 0 THEN
fh = vml_file_open(OUTFILE, 1)
IF fh < 0 THEN
PRINT "[警告] 打不开 "; OUTFILE; "(手机上路径必须是相对的)"
ELSE
n = vml_file_write(fh, base + head, blen)
n = vml_file_close(fh)
IF g_first = 1 THEN
PRINT "[4/4] HTTP "; code; " 正文 "; blen; " 字节 -> "; OUTFILE
END IF
END IF
END IF
' ⑤ 离线回落:读上一次留下的 `weather.txt`。⚠ `mode = 0` 是**只读**。
' (5) Offline fallback: read the `weather.txt` left by the last run. ⚠ `mode = 0` is **read-only**.
' (写成 1 会把缓存文件清空再读,拿到一份空数据。)
' (passing 1 would truncate the cache first and hand back nothing.)
IF offline <> 0 THEN
fh = vml_file_open(OUTFILE, 0)
IF fh < 0 THEN
PRINT "[失败] 离线且没有本地缓存 "; OUTFILE
PRINT " —— 先在联网的机器上跑一次,或把预报 CSV 放到同目录。"
ELSE
total = vml_file_read(fh, base, BUFCAP - 1)
n = vml_file_close(fh)
IF total <= 0 THEN
PRINT "[失败] 缓存文件是空的"
d_n = 0
ELSE
' 缓存里没有响应头,正文从 0 开始。
' The cache has no response headers, so the body starts at 0.
head = 0
blen = total
END IF
END IF
END IF
IF blen > 0 THEN
parse_csv()
ELSE
d_n = 0
END IF
IF d_n = 0 THEN
PRINT "[失败] 没解析出数据行(CSV 格式变了吗?)"
END IF
END SUB
' ── 版面与绘制(绘图全在 SUB 里;模块级只留调用)──────────────────
' ── Layout and drawing (all drawing lives in SUBs; module level only calls them) ──────────────────
SUB render()
DIM cx AS INTEGER
DIM pad AS INTEGER
DIM top AS INTEGER
DIM avail AS INTEGER
DIM cardh AS INTEGER
DIM i AS INTEGER
DIM y AS INTEGER
DIM h AS INTEGER
DIM midy AS INTEGER
DIM innerw AS INTEGER
DIM barw AS INTEGER
DIM bar AS INTEGER
DIM ds AS STRING
DIM tb AS STRING
DIM sub1 AS STRING
pad = SW \ 18
IF pad < 6 THEN
pad = 6
END IF
cx = SW \ 2
top = pad + 58
avail = SH - top - 40
IF d_n > 0 THEN
cardh = avail \ d_n
ELSE
cardh = avail
END IF
ui_2d_clear(C_BG)
ui_2d_text_styled(cx, pad, sTitle, C_TITLE, 17, VML_ANCHOR_CENTER, VML_FONT_BOLD)
IF offline <> 0 THEN
ui_2d_text(cx, pad + 26, sOff, C_WARN, 11, VML_ANCHOR_CENTER)
ELSE
ui_2d_text(cx, pad + 26, sSrc, C_DIM, 11, VML_ANCHOR_CENTER)
END IF
IF d_n <= 0 THEN
ui_2d_text(cx, SH \ 2 - 8, sNoData, C_DIM, 14, VML_ANCHOR_CENTER)
ui_2d_present()
ELSE
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)
' 日期(从缓冲里现取 5 个字节 —— 不存字符串数组,见全局状态那段)
' The date (five bytes taken straight from the buffer -- no string array, see the global-state block)
ds = MID$(rbuf, d_doff(i) + 1, 5)
ui_2d_text(pad + 12, midy - 8, ds, C_TITLE, 14, VML_ANCHOR_LEFT)
' 天气一句话
' The condition in words
ds = wmo_label(d_code(i))
ui_2d_text(SW - pad - 12, midy - 7, ds, C_DIM, 12, VML_ANCHOR_RIGHT)
' 最低~最高。⚠ 先各落一个局部量再拼:同一表达式里两次字符串函数调用会互相顶掉。
' Low~high. ⚠ Land each number in a local first: two string calls in one expression clobber each other.
ds = num2s(d_lo(i))
sub1 = num2s(d_hi(i))
tb = ds + "~"
tb = tb + sub1
ui_2d_text(pad + 12, midy + 10, tb, C_LO, 12, VML_ANCHOR_LEFT)
' 温度条:一格 = 2℃,10℃ 起、34℃ 满条。纯装饰,但一眼看得出哪天热。
' Temperature bar: one cell is 2 degrees, starting at 10 and filling at 34. Decorative, but the hot day stands out.
innerw = SW - pad * 2 - 24
IF innerw < 40 THEN
innerw = 40
END IF
barw = (d_hi(i) - 10) * innerw \ 24
IF barw < 8 THEN
barw = 8
END IF
IF barw > innerw THEN
barw = innerw
END IF
bar = C_LO
IF d_hi(i) >= 30 THEN
bar = C_HI
END IF
ui_2d_rect(pad + 12, midy + 26, innerw, 6, &HFF2A3038, 1, 0, 3)
ui_2d_rect(pad + 12, midy + 26, barw, 6, bar, 1, 0, 3)
i = i + 1
WEND
ui_2d_present()
END IF
END SUB
' ── 重新问一次尺寸(开窗前后各一次:画布尺寸与开窗前的估算**可能不同**)──
' ── Ask for the size again (once before and once after opening: the canvas may differ from the pre-open estimate) ──
SUB measure()
SW = ui_scr_get_w()
SH = ui_scr_get_h()
IF SW <= 0 THEN
SW = 360
END IF
IF SH <= 0 THEN
SH = 620
END IF
END SUB
' ── 开窗 + 消息循环(整段搬进 SUB:模块级的 IF/ELSE 代码流会读坏实参,见文件头那段实测)──
' ── Open the window and run the message loop (all inside a SUB: module-level IF/ELSE code misreads arguments) ──
SUB run_ui()
DIM t AS INTEGER
DIM t0 AS INTEGER
DIM done AS INTEGER
DIM k AS INTEGER
measure()
ui_win_open_ex(sWin, 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD)
measure()
render()
t0 = ui_time_get_ms()
done = 0
WHILE done = 0
IF ui_win_is_closed() <> 0 THEN
done = 1
END IF
IF done = 0 THEN
t = ui_msg_fetch_wait(200)
IF t = VML_MSG_NONE THEN
' 空闲超时(自动验收用)
' Idle timeout (for automated checks)
IF ui_time_get_ms() - t0 > IDLE_EXIT_MS THEN
done = 1
END IF
END IF
IF t <> VML_MSG_NONE THEN
t0 = ui_time_get_ms()
IF t = VML_MSG_KEYDOWN THEN
k = ui_msg_get_a()
IF k = VML_KEY_ESCAPE OR k = 113 OR k = 81 THEN
done = 1
END IF
IF k = VML_KEY_LEFT OR k = VML_KEY_RIGHT OR k = VML_KEY_UP OR k = VML_KEY_DOWN THEN
render()
END IF
END IF
IF t = VML_MSG_WINDOWORIENT OR t = VML_MSG_WINDOWRESIZE THEN
measure()
render()
END IF
IF t = VML_MSG_WINDOWCLOSE THEN
done = 1
END IF
END IF
END IF
WEND
END SUB
' ══ 主程序 ════════════════════════════════════════════════════
' ══ Main program ════════════════════════════════════════════════════
HOST = "api.open-meteo.com"
OUTFILE = "weather.txt"
PORT = 80
BUFCAP = 8192
MAXROUNDS = 64
IDLE_EXIT_MS = 8000
C_BG = &HFF0E1116
C_CARD = &HFF1A1F28
C_TITLE = &HFFE8E8F0
C_DIM = &HFF9AA0B0
C_HI = &HFFE06C50
C_LO = &HFF4A90D9
C_WARN = &HFFE0A040
initLang
' 响应缓冲:**一整块 8192 字节的字符串字面量**(见文件头 ①)。下面这一行**故意很长** ——
' The response buffer: **one 8192-byte string literal** (see pitfall 1 above). The line below is **deliberately long**:
' 它必须是字面量,不能拆行、也不能用 `+` 去拼(拼接在 256 字节处截断)。内容是什么无所谓,
' it has to be a literal, not split or joined (concatenation truncates at 256 bytes). Its content does not matter:
' `recv` 会从第一个字节开始覆盖它;`MID$(rbuf, i, 1)` 就是缓冲里的第 i 个字节。
' `recv` overwrites it from the first byte, and `MID$(rbuf, i, 1)` is byte i of the buffer.
' ⚠ 长度一律看 `total`/`blen`,**别看 `LEN(rbuf)`**(那始终是 8192)。
' ⚠ Lengths always come from `total`/`blen`, **never `LEN(rbuf)`** (which stays 8192).
' ⚠ **别用 `vml_alloc`** —— 那个堆会盖掉 BASIC 自己的全局变量(见文件头 ①的实测)。
' ⚠ **`vml_alloc` is not used** -- that heap overwrites BASIC's own globals (the measurement is in pitfall 1).
rbuf = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
base = strcpy(rbuf, rbuf)
g_first = 1
acquire()
g_first = 0
IF d_n > 0 THEN
run_ui()
END IF
' ⚠ **不调 `ui_win_close()`**:桌面脚手架的 `--frame` 是"退出后合成一张图",关窗会把场景置空。
' ⚠ **`ui_win_close()` is not called**: the scaffold's `--frame` composites after exit, and closing nulls the scene.
PRINT "weather done (BASIC)"