c/app/locate.c · 22771 字节 · 下载原文
/* locate.c —— 常用小程序⑩:**定位 / GPS**(第 4 层:`ui_*` 图形界面)
* locate.c -- Everyday utility #10: **location / GPS** (layer 4: the `ui_*` graphical interface)
*
* 干什么:`ui_gps_fetch` 取一次定位,把**纬度 / 经度**用大字打出来,下面是海拔与精度;
* What it does: `ui_gps_fetch` takes one fix, the **latitude and longitude** are printed large, and
* 底下画一块**示意**的"地图角"(十字 + 一个小圆点)。
* the altitude and accuracy follow underneath, over a **schematic** "map corner" (a crosshair plus a
* dot).
*
* ## ⚠ 坐标是**定点整数**,全程不碰浮点
* ## ⚠ The coordinates are **fixed-point integers**; no floating point anywhere
*
* `ui_gps_get_lat_e6()` 是**纬度 × 1e6**(`31523000` = `31.523000` 度),经度同理;
* `ui_gps_get_lat_e6()` is **latitude x 1e6** (`31523000` = `31.523000` degrees) and longitude is the
* 海拔与精度是**米的整数**。所以格式化是纯整数活:整数部分 `v / 1000000`、
* same; altitude and accuracy are **integer metres**. Formatting is therefore pure integer work: the
* 小数部分 `v % 1000000` 取六位(**补零**,见 `deg6`)—— 第 4 层里根本没有 `%f`,
* integer part is `v / 1000000` and the fraction `v % 1000000` is printed as six zero-padded digits
* 而且 22 门语言共用一套 ABI,浮点那条路上还埋着"浮点当实参传位模式"的坑(见 `level.c` 坑 ③)。
* (see `deg6`). Layer 4 has no `%f` at all, the 22 languages share one ABI, and the floating-point
* path carries this repo's "float argument passed as a bit pattern" trap (pitfall 3 in `level.c`).
*
* ## ⚠ 手机上才有 —— 桌面(vmlcli)上「本机没有 GPS」才是**正确输出**
* ## ⚠ Phone-only -- on the desktop (vmlcli) "no GPS on this device" is the **correct output**
*
* 真机上几乎每台手机都能定位(没给权限时 `ui_gps_is_available()` 报 0);而桌面脚手架**没有
* Almost every real phone can locate (without the permission `ui_gps_is_available()` reports 0), while
* GPS 硬件**,所以本条也是 0。于是本文件的**第一条分支**不是"读数对不对",而是
* the desktop scaffold has **no GPS hardware**, so it reports 0 as well. The first branch here is
* "**这台机器根本没有 GPS**",界面上必须**明说**,而不是画一个 `0.000000 / 0.000000` 的假坐标
* therefore not "is the reading right" but "**this machine has no GPS at all**", and the screen must
* —— 那会让人以为"你真在几内亚湾"(0,0 就在那里)。
* **say so** instead of drawing a fake `0.000000 / 0.000000`, which would place you in the Gulf of
* Guinea. `0` is not an error: a timeout, a missing permission and a missing chip all look the same.
*
* ## 桌面怎么看到"有定位"那一半:宿主自带的输入脚本注入,**不用改一行源码**
* ## Seeing the located branch on the desktop: the host's input-script injection, **no source change**
*
* printf '0 gps 31523000 121470000 12 8\n' > in.txt
* vmlcli locate.c --vml-home third_party/vml --input in.txt --timeout 40 --frame locate.png
*
* 这四个数就是 `lat_e6 lon_e6 alt_m acc_m`,喂给宿主(`CliVmlHost.InjectLocation`)之后
* Those four numbers are `lat_e6 lon_e6 alt_m acc_m`; once fed to the host (`CliVmlHost.InjectLocation`)
* `ui_gps_is_available()` 报 1、`ui_gps_fetch` 成功,四个 get 就交回这四个数 —— 换一组数就是另一张图。
* `ui_gps_is_available()` reports 1, `ui_gps_fetch` succeeds and the four getters return them. Feeding
* ⚠ 注入只影响**脚手架**:手机端那条真实现(Geolocation + 权限)一个字节都不动。
* another set of numbers gives another picture. The injection only affects the **scaffold**.
*
* ## 踩到的坑(都是这一轮真撞上的,写在这里省得下一个人再撞)
* ## Pitfalls (all hit for real while writing this file; recorded so the next person does not)
*
* ① ⚠ **取数顺序不能乱**:`ui_gps_get_*` 读的是**上一次 `ui_gps_fetch` 的结果**(没取过就是 0),
* ① ⚠ **The order of calls is not free**: `ui_gps_get_*` return the result of the **last
* `ui_gps_fetch`** (0 if it never succeeded). So this file asks `ui_gps_is_available()` first, then
* —— 本文件先问"有没有",`fetch` 成功之后**紧接着**读那四个数,中间不加别的调用
* calls `ui_gps_fetch`, and reads those four numbers **immediately after** it without anything in
* (与 `level.c` 坑 ① 的传感器缓存同一条规矩)。
* between (the same rule as the sensor cache in pitfall 1 of `level.c`).
* ② ⚠ **别把 `printf` 一整行里的几个调用当"从左到右"**(实测踩到):`printf("%d %d", fetch(), get())`
* ② ⚠ **Never assume the calls inside one `printf` run left to right** (hit for real): with
* 的实参求值顺序由编译器定 —— 实测 `get` 先跑,于是读到的是**取之前**的 0,
* `printf("%d %d", fetch(), get())` the argument order is the compiler's to choose; measured, `get`
* 看起来像"fetch 成功了但坐标全是 0"(接口一点毛病没有)。所以取数与使用**拆成两条语句**。
* ran first and read the zeros from **before** the fetch, which looks exactly like "the fetch worked
* but the coordinates are all 0". Split the call and the use into two statements.
* ③ ⚠ **示意地图只是示意**:那个点按**小数部分**摆,与真实地理位置**没有任何关系**
* ③ ⚠ **The schematic map is only a sketch**: the dot is placed by the **fractional part** and has
* (真地图要投影、要底图、要比例尺,那是另一个程序的事)。所以旁边写着「示意」两个字,
* **no relation** to the real position (a real map needs a projection, a base map and a scale bar,
* and that is a different program). The box is therefore labelled "sketch" in the UI, and the label
* and the dot is positioned the same way in every run.
* ④ ⚠ **"有没有 GPS"每拍都要重问一次**(与 `level.c` 坑 ② 同源):`--input` 的事件由另一个线程按
* ④ ⚠ **"Is GPS there" must be re-asked every tick** (same as pitfall 2 in `level.c`): the `--input`
* 时刻投递,程序可能在注入落地之前就问过了。所以"没有"每拍重问,一旦有就不再问。
* events arrive from another thread on a timer, and the program may ask before the injection lands.
* Absent is re-asked every tick; once present it is not asked again.
* ⑤ ⚠ **文件级标量别用负值初值**(本仓刚归档的 E187):`static int x = -1;` 读到的是 **0**。
* ⑤ ⚠ **Never give a file-scope scalar a negative initializer** (this repo's E187): `static int x = -1;`
* 所以下面所有哨兵量一律用**正数**(0 = 没有/未知,1 = 有),别拿 -1 当"还没有"。
* reads back as **0**. Every sentinel below is a **positive** number (0 = absent/unknown, 1 = present).
*
* 版面:尺寸一律从宿主问(`ui_scr_get_w()/_h()`),颜色是真彩 `0xAARRGGBB`,画完必须 `ui_2d_present()`。
* Layout: every size is asked of the host (`ui_scr_get_w()/_h()`), colours are true colour
* 循环**必须有界**(例程要能被自动验收):空闲 10 秒自己退出,`Esc`/`q` 也退出。
* `0xAARRGGBB`, and the frame must end with `ui_2d_present()`. The loop **must be bounded** (samples
* 写真正的程序时把 `IDLE_EXIT_MS` 去掉即可。
* have to be automatically verifiable): it exits after 10 s of idling, and `Esc`/`q` quits too.
*/
#include <vml_ui.h>
#include <stdlib.h> /* itoa --- digits to text via the library */
#define IDLE_EXIT_MS 10000 /* 空闲 10 秒自己退出(验收用) */
/* idle exit after 10 s (for automated checks) */
#define FETCH_MS 2000 /* 取一次定位的超时(真机上是**头一次定位**的等待)*/
/* timeout for one fix (on a phone this is the first-fix wait) */
/* 配色(真彩 AA RRGGBB)*/
/* Colours (true colour AA RRGGBB) */
#define C_BG 0xFF0E1218
#define C_BOX 0xFF141A22
#define C_RING 0xFF3C4658
#define C_GRID 0xFF2A3346
#define C_TITLE 0xFFE8E8F0
#define C_DIM 0xFF8A94A8
#define C_ERR 0xFFFF8A80 /* 没有这项能力 */
/* capability absent */
#define C_LAT 0xFF6FC3FF /* 纬度 */
/* latitude */
#define C_LON 0xFF4ADE80 /* 经度 */
/* longitude */
#define C_DOT 0xFFFFD166 /* 示意地图上的那个点 */
/* the dot on the schematic map */
/* ── 全局状态 ───────────────────────────────────────────────────────── */
/* -- Global state -- */
static int SW;
static int SH;
static int g_pad;
static int lang;
static int g_avail; /* 1 = 这台机器有定位(每拍重问一次,见坑 ④)*/
/* 1 = this machine can locate (re-asked every tick, pitfall 4) */
static int g_ok; /* 1 = 取到过一次定位(四个读数有效)*/
/* 1 = a fix was taken once (the four readings are valid) */
static int g_lat; /* 纬度 × 1e6 */
/* latitude x 1e6 */
static int g_lon; /* 经度 × 1e6 */
/* longitude x 1e6 */
static int g_alt; /* 海拔(米)*/
/* altitude in metres */
static int g_acc; /* 水平精度(米)*/
/* horizontal accuracy in metres */
/* 版面(画与命中判定**必须用同一份**)*/
/* Layout (drawing and hit testing **must share this one copy**) */
static int mX;
static int mY;
static int mW;
static int mH; /* 示意地图那块方框 */
/* the schematic map box */
static int readY; /* 大字号那两行(纬度 / 经度)的起始 y */
/* the y where the two large lines (latitude / longitude) start */
/* ── 小工具 ─────────────────────────────────────────────────────────── */
/* -- Small helpers -- */
static char g_i10[16];
static char *numstr(int v) {
itoa(v, g_i10);
return g_i10;
}
/* ×1e6 的定点数 → "31.523000"(符号 + 整数部分 + **六位**小数,缺位补零)。
* A x1e6 fixed-point value to "31.523000" (sign, integer part, then **six** zero-padded decimals).
* ⚠ 返回的是**同一个静态缓冲** —— 别在一个表达式里连用两次(`strcat` 两次会互相覆盖)。
* ⚠ It returns **one static buffer** -- never use it twice in one expression (the second call
* overwrites the first before `strcat` copies it).
* ⚠ 负零:`-0.000400` 要写成 `-0.000400`(实测 `-400 / 1000000` 在 C 里是 0、靠 `neg` 那一位才不丢符号)。
* ⚠ Negative zero: `-0.000400` must keep its sign, which C's integer division would drop (`-400/1000000`
* is 0), so the sign is carried separately.
*/
static char g_deg[24];
static char *deg6(int e6) {
int i;
int n;
int neg;
int ip;
int fp;
char tmp[16];
neg = 0;
if (e6 < 0) { neg = 1; e6 = 0 - e6; }
ip = e6 / 1000000;
fp = e6 - ip * 1000000;
i = 0;
if (neg != 0) { g_deg[i] = '-'; i = i + 1; }
n = 0;
if (ip == 0) { tmp[n] = '0'; n = n + 1; }
while (ip > 0) { tmp[n] = (char)('0' + ip % 10); ip = ip / 10; n = n + 1; }
while (n > 0) { n = n - 1; g_deg[i] = tmp[n]; i = i + 1; }
g_deg[i] = '.'; i = i + 1;
g_deg[i] = (char)('0' + (fp / 100000) % 10); i = i + 1;
g_deg[i] = (char)('0' + (fp / 10000) % 10); i = i + 1;
g_deg[i] = (char)('0' + (fp / 1000) % 10); i = i + 1;
g_deg[i] = (char)('0' + (fp / 100) % 10); i = i + 1;
g_deg[i] = (char)('0' + (fp / 10) % 10); i = i + 1;
g_deg[i] = (char)('0' + fp % 10); i = i + 1;
g_deg[i] = 0;
return g_deg;
}
/* ── 取一次定位 ─────────────────────────────────────────────────────── */
/* -- Taking one fix -- */
/* 返回 1 = 画面要重画。⚠ 顺序:先问"有没有",再 `fetch`,**紧接着**读那四个数(见坑 ①)。
* Returns 1 when the screen needs a redraw. ⚠ Order: ask "is it there", then `fetch`, then read the
* "没有"每拍重问(注入与本程序启动之间有赛跑,见坑 ④);取到过就不再重复取。
* four numbers **immediately** (pitfall 1). Absent is re-asked every tick (the injection race,
* pitfall 4); once a fix has been taken it is not taken again.
*/
static int poll(void) {
int changed;
changed = 0;
if (g_avail == 0) {
g_avail = ui_gps_is_available();
if (g_avail != 0) changed = 1;
}
if (g_avail != 0 && g_ok == 0) {
if (ui_gps_fetch(FETCH_MS) != 0) {
/* ⚠ 取数与使用**拆成两条语句**(见坑 ②)*/
/* ⚠ The call and the use stay in separate statements (pitfall 2) */
g_lat = ui_gps_get_lat_e6();
g_lon = ui_gps_get_lon_e6();
g_alt = ui_gps_get_alt_m();
g_acc = ui_gps_get_accuracy_m();
g_ok = 1;
changed = 1;
}
}
return changed;
}
/* ── 版面 ───────────────────────────────────────────────────────────── */
/* -- Layout -- */
static void layout(void) {
int below;
SW = ui_scr_get_w();
SH = ui_scr_get_h();
if (SW <= 0) SW = 360;
if (SH <= 0) SH = 620;
g_pad = SW / 18;
if (g_pad < 10) g_pad = 10;
readY = g_pad + 56; /* 标题两行之下就是那两行大字 */
/* the two large lines start right below the two heading lines */
/* 示意地图:**正方形**(十字要对称),边长同时受宽与高约束 ——
* The schematic map is a **square** (a crosshair must be symmetric), bounded by both width and
* 只看宽度的话矮屏(横屏)上它会压住上面两行读数。
* height; width alone would push it over the readouts on a short (landscape) screen.
*/
below = readY + 116; /* 大字两行 + 海拔/精度一行 + 一点间距 */
/* two large lines plus the altitude/accuracy line and a gap */
mW = SW - g_pad * 2;
if (mW > 260) mW = 260;
mH = mW;
if (mH > SH - g_pad - 34 - below) mH = SH - g_pad - 34 - below;
if (mH < 70) mH = 70;
mW = mH;
mX = (SW - mW) / 2;
mY = below;
}
/* ── 绘制 ───────────────────────────────────────────────────────────── */
/* -- Drawing -- */
/* 大字那两行 + 海拔/精度一行 */
/* The two large lines plus the altitude/accuracy line */
static void drawReadout(void) {
char buf[64];
buf[0] = 0;
strcat(buf, lang == 0 ? "纬度 " : "lat ");
strcat(buf, deg6(g_lat));
ui_2d_text(SW / 2, readY, buf, C_LAT, 26, VML_ANCHOR_CENTER);
buf[0] = 0;
strcat(buf, lang == 0 ? "经度 " : "lon ");
strcat(buf, deg6(g_lon));
ui_2d_text(SW / 2, readY + 34, buf, C_LON, 26, VML_ANCHOR_CENTER);
buf[0] = 0;
strcat(buf, lang == 0 ? "海拔 " : "alt ");
strcat(buf, numstr(g_alt));
strcat(buf, lang == 0 ? " 米 精度 ±" : " m accuracy +-");
strcat(buf, numstr(g_acc));
strcat(buf, lang == 0 ? " 米" : " m");
ui_2d_text(SW / 2, readY + 74, buf, C_DIM, 14, VML_ANCHOR_CENTER);
}
/* 示意地图:方框 + 十字 + 一个点。
* The schematic map: a box, a crosshair and one dot.
* ⚠ **它只是示意**(见文件头坑 ③):点按**小数部分**摆,与真实地理位置无关 ——
* ⚠ **It is only a sketch** (pitfall 3): the dot is placed by the **fractional part** and has nothing
* 真地图要投影 + 底图 + 比例尺,那是另一个程序的事。方框上写着「示意」就是为了不骗人。
* to do with the real position (a real map needs a projection, a base map and a scale bar). The box
* says "sketch" so that it cannot be mistaken for one.
*/
static void drawMap(void) {
int cx;
int cy;
int r;
int fl;
int fo;
int dx;
int dy;
int dotR;
char buf[32];
cx = mX + mW / 2;
cy = mY + mH / 2;
r = mW / 2 - 4;
ui_2d_rect(mX, mY, mW, mH, C_BOX, 1, 0, 8);
ui_2d_rect(mX, mY, mW, mH, C_RING, 0, 1, 8);
/* 十字与四分格:给"点在哪儿"一个参照 */
/* A crosshair and quarters: a frame of reference for "where the dot is" */
ui_2d_line(mX + 6, cy, mX + mW - 6, cy, C_GRID, 1);
ui_2d_line(cx, mY + 6, cx, mY + mH - 6, C_GRID, 1);
ui_2d_circle(cx, cy, r / 2, C_GRID, 0, 1);
if (g_ok == 0) {
ui_2d_text(cx, cy - 8, lang == 0 ? "没有读数" : "no fix", C_ERR, 14, VML_ANCHOR_CENTER);
} else {
fl = g_lat % 1000000;
fo = g_lon % 1000000;
if (fl < 0) fl = 0 - fl;
if (fo < 0) fo = 0 - fo;
dx = (fo - 500000) * r / 500000;
dy = (fl - 500000) * r / 500000;
dotR = mW / 28;
if (dotR < 4) dotR = 4;
ui_2d_circle(cx + dx, cy - dy, dotR + 3, 0x60FFD166, 1, 0);
ui_2d_circle(cx + dx, cy - dy, dotR, C_DOT, 1, 0);
}
buf[0] = 0;
strcat(buf, lang == 0 ? "示意(按小数部分摆的点)" : "sketch (dot placed by the fraction)");
ui_2d_text(cx, mY + mH - 20, buf, C_DIM, 11, VML_ANCHOR_CENTER);
}
/* 没有 GPS 时整屏就一句话 —— 别画一个 (0,0) 的假坐标(见文件头那段)*/
/* With no GPS the whole screen is one sentence -- never a fake (0,0) coordinate (see the header) */
static void drawNoGps(void) {
ui_2d_text(SW / 2, SH / 2 - 40, lang == 0 ? "本机没有 GPS" : "no GPS on this device",
C_ERR, 24, VML_ANCHOR_CENTER);
ui_2d_text(SW / 2, SH / 2 + 4, lang == 0 ? "(手机上才有)" : "(phones only)",
C_ERR, 15, VML_ANCHOR_CENTER);
ui_2d_text(SW / 2, SH / 2 + 40, lang == 0 ? "读数是 0 不是错误:这一端没有这个能力"
: "a reading of 0 is not an error: this end lacks it",
C_DIM, 12, VML_ANCHOR_CENTER);
}
static void draw(void) {
ui_2d_clear(C_BG);
ui_2d_text_styled(SW / 2, g_pad, lang == 0 ? "定位 / GPS" : "Location / GPS",
C_TITLE, 18, VML_ANCHOR_CENTER, VML_FONT_BOLD);
if (g_avail == 0) {
ui_2d_text(SW / 2, g_pad + 24, lang == 0 ? "ui_gps_is_available() = 0" : "ui_gps_is_available() = 0",
C_ERR, 12, VML_ANCHOR_CENTER);
drawNoGps();
} else {
if (g_ok != 0) {
ui_2d_text(SW / 2, g_pad + 24, lang == 0 ? "ui_gps_fetch 取到的定点整数(×1e6)"
: "fixed-point (x1e6) values from ui_gps_fetch",
C_DIM, 11, VML_ANCHOR_CENTER);
drawReadout();
} else {
ui_2d_text(SW / 2, g_pad + 24, lang == 0 ? "有 GPS,但这一次没取到(超时 / 没权限)"
: "GPS present, but this fix failed (timeout / permission)",
C_ERR, 12, VML_ANCHOR_CENTER);
}
drawMap();
}
ui_2d_text(SW / 2, SH - 14, lang == 0 ? "手机上才有真读数 Esc / q 退出"
: "real readings are phone-only Esc / q quits",
C_DIM, 11, VML_ANCHOR_CENTER);
ui_2d_present(); /* ⚠ 不调这句 = 黑屏 */
/* without this call you get a black screen */
}
/* ── 主流程 ─────────────────────────────────────────────────────────── */
/* -- Main flow -- */
int main(void) {
int msg[4];
int t;
int t0;
int done;
lang = ui_scr_get_language();
/* "有没有"**开窗之前就能问**,先问再决定画什么 */
/* Availability is **askable before opening a window**; ask first, then decide what to draw */
g_avail = ui_gps_is_available();
poll();
layout();
ui_win_open_ex(lang == 0 ? "定位 / GPS" : "Location / GPS",
0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
layout(); /* 开窗之后画布尺寸才是准的,再算一次 */
/* the canvas size is only final after the window opens */
draw();
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) {
/* 没有消息也不代表没有变化:定位是**轮询**的(与 `level.c` 的传感器同一条)*/
/* No message does not mean nothing changed: location is **polled** (same as the sensors in `level.c`) */
if (ui_time_get_ms() - t0 > IDLE_EXIT_MS) done = 1;
else if (poll() != 0) draw();
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;
continue;
}
if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) {
layout();
draw();
continue;
}
if (t == VML_MSG_WINDOWCLOSE) done = 1;
}
return 0;
}