firmware/ ├── main/ ← 通用层(改必两板回归) │ ├── app_main.c 启动装配 │ ├── emgw.h / emgw_version.h │ ├── cli_json.c 行协议命令分发(串口 / BLE / USB CDC 共用) │ ├── http_srv.c HTTP + CORS(Studio 直连) │ ├── ota_mgr.c 双槽 OTA + rollback + 启动自证【已存在】 │ ├── kb_ota.c .emf 镜像 OTA(POST /kbota)【已存在】 │ ├── transport.c wifi_mgr.c ble_mgr.c hub_mgr.c em_agent.c │ ├── em_runtime.c 【新增】策略层:遗忘 / 去重 / 冲突消解 / 时效 │ ├── em_ts.c 【新增】时序源:合成数据源 + 特征化 │ ├── em_usb.c 【新增】TinyUSB CDC + MSC 复合设备 │ └── (MVP 删除)cam_mgr.c mic_mgr.c gps_mgr.c 及其命令 ├── components/edgehippom/ ← 【新增】引擎层,板无关 │ ├── include/edgehippom.h HAL 公开接口(本章第 2 节) │ ├── em_hal_dispatch.c 后端分派(桩/实现互斥的唯一入口,见 §1.1) │ ├── emlite/ 自研后端(先行) │ │ ├── em_store_blk.c 块管理 + 4 KB 页对齐 + 区域相对 IO 入口 │ │ ├── em_flash_io.c flash 抽象(PC 上用文件仿真)【唯一允许含平台 #if 的文件】 │ │ ├── em_wal.c WAL 环形介质结构:落位 / 记录读写 / 页回收 │ │ ├── em_wal_rec.c 【ADR-016 新增】启动恢复:扫描与前滚 / 回滚判定 │ │ ├── em_tx.c 【ADR-016 新增】事务:脏页表 / 原子提交 / abort / 前滚执行 │ │ ├── em_mem.c 【ADR-016 新增】记录编解码 + 追加 / 取回 / 墓碑 │ │ ├── em_core.c 【ADR-016 新增】后端装配体:句柄生命周期与 API 编排 │ │ ├── em_pool.c 【ADR-016 新增】预分配内存池(malloc-free) │ │ ├── em_util.c 【ADR-016 新增】CRC32 / UTF-8 安全截断 │ │ ├── em_ann.c IVF + graph 混合 ANN(Sprint 2) │ │ └── em_ts_col.c 列式时序环 L1/L2(Sprint 2) │ └── emdb/ eXtremeDB 后端(授权到位后实现,接口不变) ├── boards/ ← 板级真源,禁止跨板拷贝 │ ├── sdkconfig.xiao_esp32s3_n8r8 │ └── sdkconfig.devkitc_esp32s3_n16r8 ├── partitions_emgw.csv 8 MB(Dev-A) └── partitions_emgw_16m.csv 16 MB(Dev-B)
| 层 | 代码 | 职责 | 硬约束 |
|---|---|---|---|
| L5 宿主接口 | main/cli_json.cmain/http_srv.cmain/em_usb.c | 把宿主请求翻译成引擎调用 | 不得直接访问 flash;不含业务规则 |
| L4 网络 | main/transport.clink_mgr / hub_mgr | EdgeMesh 发现与接力(见 03 号分册) | 不得直接写库,须经 L3 |
| L3 策略 Runtime | main/em_runtime.c | 遗忘、去重(余弦 > 0.95 合并)、冲突消解、时效、优先级 | 不得直接访问 flash,只经 HAL |
| L2 引擎 Core | components/edgehippom/ | 存储、事务、ANN、时序 | HAL 接口在 emdb 替换时不得变更 |
| L1 平台 | ESP-IDF / 驱动 | flash、PSRAM、USB、Wi-Fi、BLE | — |
components/ 而不是 main/:引擎是板无关的(Dev-A / Dev-B / 未来的 α 模组共用),放在组件里才能做到「换板不换引擎」,也才能在 PC 上跑单测(配合 em_flash_io 文件仿真)。
| 项 | Dev-A(XIAO N8R8) | Dev-B(DevKitC N16R8) |
|---|---|---|
| 形态 | γ 磁吸卡 ( form=gamma-card) | β USB Dongle ( form=usb-dongle) |
| Kconfig 选项 (二选一,编译期固化) | CONFIG_EMGW_FORM_GAMMA_CARD | CONFIG_EMGW_FORM_USB_DONGLE |
| 运行时字符串 | gamma-card | usb-dongle |
| 设备信息字段 | sys.info 的 form 字段(原 role 字段已改名);互联邻居字段 peer_form(原 peer_role) | |
| 命令 | sys.form / sys.set_form(旧名 sys.role / sys.set_role 保留为兼容别名) | |
EMGW_ROLE_EDGE / EMGW_ROLE_GATEWAY 与运行时字符串 edge / gateway 一律废止;板位差异只由 CONFIG_EMGW_FORM_* 决定,与固件分层职责无关。板级差异仍一律走 boards/sdkconfig.<board_id> + 分区表 + CONFIG_EMGW_*,禁止跨板拷贝 sdkconfig。
eXtremeDB SDK 不在开发机上(全盘检索 C:\emgw-build 与 C:\Espressif 无任何 mco/extreme 头文件或库)。若把引擎当成前置依赖,项目第一天就停在等商务。
对策:emlite(自研)先跑通全部闭环,emdb(eXtremeDB)授权到位后替换,接口不变。授权谈判从「阻塞全部」降级成「性能优化项」——而且手上先有实测数据,谈价时地位完全不同。
| 能力 | emlite(先行) | emdb(eXtremeDB,后替换) |
|---|---|---|
| ANN 索引 | IVF + graph 混合(MCU 级推荐) | 原生 HNSW / Vamana,事务一致 |
| 时序 | 自研列式环(L1 分钟聚合 / L2 pattern) | 原生 sequence 列式时序类型 |
| ACID | WAL + CoW + 原子提交(自实现) | 原生事务(含 MVCC 变体) |
| 内存分配 | 全预分配池,运行期 malloc 0 次 | 自研内存管理器,运行期不调用 malloc |
| 复制 / 同步 | 需自实现 | Active Replication Fabric:写事务 → commit → 上行复制 → 回收本地空间 |
| footprint | 目标 .text ≤ 60 KB | Edge Client ≤ 8 KB(官方口径,需实测) |
em_sync_* 接口——将来是直接启用,而不是重写协议层。
components/edgehippom/include/edgehippom.h)/* ---------- 生命周期 ---------- */ int em_open (const em_cfg_t *cfg, em_db_t **out); int em_close (em_db_t *db); int em_stat (em_db_t *db, em_stat_t *out); /* 条数/字节/水位/碎片率 */ /* ---------- 事务 ---------- */ int em_tx_begin (em_db_t *db); int em_tx_commit(em_db_t *db); int em_tx_abort (em_db_t *db); /* ---------- 记忆条目 ---------- */ int em_mem_put (em_db_t *db, const em_mem_t *rec); /* 单条(隐含事务) */ int em_mem_get (em_db_t *db, em_id_t id, em_mem_t *out); int em_mem_del (em_db_t *db, em_id_t id); /* ---------- 混合查询 ---------- */ /* 【ADR-014】reject 可为 NULL;但当 n==0 时调用方必须传它才能区分拒因 */ int em_query (em_db_t *db, const em_query_t *q, em_hit_t *hits, int max_hits, em_reject_e *reject); /* ---------- 时序 ---------- */ /* 【ADR-019】t0/t1 构成左闭右开区间 [t0, t1):含 t0 所属分钟、不含 t1 所属分钟。 * 逐分钟查询请写 t1 = 下一分钟起点。两个函数必须用**同一套分钟序号**判定 * (minute = t / EM_TS_MIN_PER_US),不可一个比微秒、一个比整除分钟—— * t1 非整分钟时二者会分叉。闭区间的实测后果见 ADR-019 取证。 */ int em_ts_append (em_db_t *db, uint32_t stream, int64_t ts_us, const void *sample, size_t n); int em_ts_window (em_db_t *db, uint32_t stream, int64_t t0, int64_t t1, em_ts_iter_t *it); int em_ts_agg (em_db_t *db, uint32_t stream, int64_t t0, int64_t t1, em_agg_e op, em_agg_t *out); int em_ts_downsample(em_db_t *db, em_ts_level_e from, em_ts_level_e to); /* ---------- 碎片 ---------- */ int em_frag_import (em_db_t *db, const uint8_t *emf, size_t len, const em_frag_opt_t *opt, em_frag_rpt_t *rpt); int em_frag_export (em_db_t *db, const em_frag_sel_t *sel, uint8_t **buf, size_t *len); int em_frag_list (em_db_t *db, em_frag_info_t *out, int max); /* ---------- 同步(预留 Active Replication Fabric) ---------- */ int em_sync_push (em_db_t *db, const em_sync_desc_t *d); int em_sync_pull (em_db_t *db, const em_sync_desc_t *d); int em_sync_status(em_db_t *db, em_sync_stat_t *out);
typedef struct {
em_id_t id; /* 0 = 由引擎分配 */
uint32_t frag_id; /* 所属碎片 */
int64_t ts_ms;
uint16_t dim; /* 向量维度 */
uint8_t dtype; /* EM_VEC_NONE / INT8 / FP16 */
uint16_t prio; /* 0–1000,越大越不易被遗忘 */
uint16_t ttl_h; /* 时效(小时),0 = 永不过期 */
uint16_t vec_len; /* 向量字节数 */
uint16_t text_len;
uint16_t meta_len;
const void *vec; /* 序列化时置于记录末尾(ADR-010) */
const char *text;
const char *meta; /* JSON 片段 */
} em_mem_t;
typedef struct {
const int8_t *vec; /* NULL = 纯结构化查询 */
uint16_t dim, k;
const char *filter; /* 迷你表达式:dev_id==3 && ts_ms>t0 */
uint32_t flags; /* EM_Q_ANN|EM_Q_TS|EM_Q_FILTER|EM_Q_EXPLAIN */
uint16_t min_score_q8; /* 【ADR-014】拒答阈值 ×256,0 = 不启用 */
} em_query_t;
typedef struct {
em_id_t id;
uint32_t frag_id; /* 出处的碎片(对应「这是谁 / 哪份资料的记忆」) */
int64_t ts_ms; /* 出处时间戳 */
uint16_t score_q8; /* 置信度 ×256 */
uint8_t matched_by; /* EM_M_ANN / EM_M_FILTER / EM_M_TS */
uint16_t text_len;
const char *text; /* 摘要(≤150 B) */
const char *src; /* 【ADR-014】出处标识,取自 meta.src */
} em_hit_t;
typedef enum {
EM_REJECT_NONE = 0, /* 正常命中 */
EM_REJECT_LOW = 1, /* 全库最高分仍低于 min_score_q8 */
EM_REJECT_REDLINE = 2, /* 命中红线条目(meta.refusal),不得输出 */
EM_REJECT_EXPIRED = 3, /* 命中条目已过 ttl_h */
EM_REJECT_EMPTY = 4 /* 碎片为空 / 无匹配 token */
} em_reject_e;
typedef struct {
uint16_t vec_bytes; /* 向量字节数(桶大小) */
uint16_t text_max; /* 摘要上限,默认 150 */
uint32_t wal_bytes; /* WAL 大小,默认 512 KB */
uint16_t ann_M; /* graph 出度,默认 16 */
uint16_t ann_ef_search; /* 查询宽度,默认 32 */
uint8_t ivf_nlist; /* IVF 聚类数(MCU 级建议 32–64) */
uint8_t merge_threshold_q8; /* 去重阈值 ×256,默认 0.95*256 = 243 */
bool malloc_free; /* true = 全预分配,运行期禁止 malloc */
} em_cfg_t;
em_sync_* 必须在 emlite 阶段就定义并实现为空操作或最小实现,不得留到 emdb 阶段才设计。em_hit_t 的查询必须带 score_q8 与 matched_by(区分 ANN / 过滤 / 时序),否则策略层无法做冲突消解。
EdgeHippo 的产品承诺包含「能在没有依据时说不知道」。这不是策略层的可选行为,而是引擎层必须支持的语义——否则上层拿不到足够信息做判断。em_query 的拒答码通过出参 em_reject_e *reject 返回(可为 NULL 表示调用方不关心)。
| 返回情形 | em_query 返回 | 策略层 / 宿主的应然行为 |
|---|---|---|
| 有命中且分数达标 | n > 0,填充 hits | 正常引用。必须连同 frag_id / ts_ms / score_q8 一起交给宿主,让宿主能说「依据是你在 X 时间留下的 Y,置信度 Z」 |
| 有命中但全不达标 | n = 0,EM_REJECT_LOW | 返回「不在我的知识范围内」+ 拒因。严禁回退为「返回最接近的一条」 |
| 命中红线条目 | n = 0,EM_REJECT_REDLINE | 拒绝回答,且不透露该条目存在的事实(只回「此话题不在范围内」) |
| 命中已过期条目 | n = 0,EM_REJECT_EXPIRED | 可回「我记得过,但已过期」——允许说明时效,这比沉默更有价值 |
| 碎片为空 | n = 0,EM_REJECT_EMPTY | 区分「没装东西」与「装了但没答上来」,二者运维含义完全不同 |
min_score_q8 必须是引擎层过滤,不是「上层拿到结果再扔掉」——后者会让 top-k 被低分项占满而真正的高分项被截断,属于实现缺陷。reject,视为缺陷——宿主无法区分「不知道」与「库空了」。src 允许为空(老碎片无该字段),但新建碎片必须写,由 Studio 打包器强制填充。
tests/emcontract.h 自行 typedef 了一整套 em_cfg_t / em_stat_t / em_hit_t / em_agg_t / em_ts_iter_t / em_sync_* / em_frag_*,形成第二份独立的 HAL 契约副本。两份契约在同一编译单元内不可能共存,致使真实引擎无法链入 QA 测试二进制——这正是 G7 长期挂「未验证」的真根因。components/edgehippom/SPEC.md §9,该节为唯一权威。本分册不再重复抄录(复制必然漂移)。
| 结构体 | 字段裁定 | 判据 |
|---|---|---|
em_sync_desc_t | {peer_id, frag_id, max_bytes} | 弃 QA 侧的 src_uri:MVP 是断网环境(G6),网络地址在设备端无意义 |
em_frag_rpt_t | 拆为 skipped(按规则跳过)+ crc_bad(坏数据) | QA 侧单一 rejected 桶把「灌库文件坏了」与「策略挡了」混在一起,诊断时无法区分,属静默信息丢失 |
em_frag_info_t.name | 定长 char[64] 改为 const char* | 固定 64 B 会在 UTF-8 中文名上截断 |
em_frag_opt_t | 位标志 flags 改为具名字段 | 位的含义散落在实现里,易漂移且不可自描述 |
em_stat_t.bytes | 定 u32(QA 侧 u64 作废) | 记录区上限 15 MB 量级,u32 足够;与 page_total / rec_count 同量级口径 |
em_sync_stat_t | {pushed, pulled, last_ms, link_up}(字段不变,QA 侧 {pending, bytes_out, bytes_in} 作废) | MVP 阶段 em_sync_* 是预留(无实际传输),应上报「传了多少、链路是否通」而非流量字节统计;pending 无数据来源,不得凭空返回 |
em_agg_t | {double v[8]; uint32_t n}:v[0]=所求 op 结果,v[1..7] 必须置 0,n=参与聚合的样本数 | QA 侧 {double v[6]} 无 n;且未定义保留位语义,实现会填未定义值 |
em_ts_iter_t | {level, stream, t0_us, t1_us, row, rows};row 相对本流非全局,区间为半开 [t0, t1) | QA 侧为 {int dummy} 占位;row 的参照系不写死会在多流时算错窗口 |
em_frag_sel_t | {frag_id, include_vec}(QA 侧 {frag_sel, flags} 作废) | 与 em_frag_opt_t 同理:具名字段优于位标志 |
em_stat_t | 新增 rec_pages_used | 占用率判据是页数口径,测试须能直接读页数 |
em_cfg_t.ts_streams | uint16_t,0 = 取默认 3;取值 1..min(16, ts_l1_pages);解析后的 n > ts_l1_pages 时 em_open 返回 EM_E_INVAL | 流数决定 L1 每流页数(ts_l1_pages / n),进而决定保留期。每流至少 1 页是 base_minute 语义成立的最低要求;按 0 页运行会让该流一条都存不下,是静默逻辑错误,必须拒绝 |
em_cfg_t.ts_l1_pages | uint16_t,0 = 取默认;非 0 时取值 1..ts_pages−1。取证区槽数 = ts_pages − ts_l1_pages(派生量,不单列字段) | 用绝对页数,不用比例:真正生效的量是页数,比例与生效值之间隔了一层取整,而本项目明令禁止用派生量反推基础量;且三套几何的 L1 占比本就不一致(112→96 是 85.7%,24→16 是 66.7%),没有共同比例可写。取证区至少留 1 槽,否则取证写入无处可落 |
SPEC.md §2.8.0,字段本身以 SPEC.md §9.11 为唯一权威。本分册不复制整表(复制必然漂移,见下方规则①)。[注意] L1 保留期是派生量,不得在本分册硬写。换算链(每一环均为整数运算):
ts_l1_pages ÷ ts_streams = 页/流 → × 每页行数 → ÷ 1440 = 天。
当前值快照(2026-10-26 定稿):行 12 B(数据 8 B + mark 4 B,最后写)→ 每页 340 行。保留期随时序区大小变化,两态并存,引用时必须注明是哪一个:rec_cap 无论记录区多大都不可能超过 12,600。故记录区只需够到临界点(X ≥ 2,340 页且取整余量 ≥ 30 条),多给纯属浪费,省下的全给时序区。本次定案取整余量 33 条,两块板 rec_cap 均零损失(Dev-B 12,600 / Dev-A 3,300)。mark == 0xA55AA55A 才算已提交,否则终止扫描。撕裂页按「已满」处理、强制下一次追加翻页——理由是扫描在撕裂行处终止,其后写入的行根本不可达(不是跳到下一行继续写就行)。std 钳位保留(防溢出),但不再是承重不变量。off 与 len 均 4 字节对齐(esp_partition_write 的硬件要求,非我们的选择)。9 B 步长时行首偏移为 16 / 25 / 34 / 43,25 非 4 的倍数,写第 2 行即返 EM_E_INVAL。EM_PCUT_BIT_AT(位级撕裂注入:第 N 字节只编程 bit_mask 指定的位,其余保持原值,符合 NOR 只能 1→0)实测:直接对 mark 字节做位级部分编程(只写最低 1 位)→ 结果 ≠ 目标值 → 检出,有效样本数保持正确。mark 判据在位级撕裂下 100% 检出,且不依赖数据取值、不依赖端序。std < 0 判据:它只在「字节级撕裂」假设下完备;在「字节内可部分编程」前提下枚举 std 高字节 256 种部分编程态,128 种检出 / 127 种漏检(约 50%)。这正是仿真粒度决定你能证明什么的实例——原有仿真只有字节级注入,用例全过≠判据完备。现已补位级注入,可反向实证其漏检。emcontract.h 须改为 #include "edgehippom.h",只保留 QA 骨架自身的东西(用例注册表、em_halvt_t、EM_ERR_NOT_IMPL、探针接口)。今日已因「复制一份」吃两次亏:em_crc32 在仓库里出现三份实现、结构体又分叉一次。复制必然漂移,这是定律不是概率。vec_len 反推 dim(int8 与 fp16 换算不同,猜错即静默逻辑错误);用 bytes / 4080 反推页数(漏掉页头与碎片)。凡是可直接记录的量,一律直接记录,不做反推。rec_cap 即可由几何与 PSRAM 上限完全推导,故不加进 em_cfg_t,改为 open 时推导)。
2.3 节给出了 em_err_e 的码值,但此前未定义语义边界——各码在什么条件下返回、调用方应如何处理没有正式约定,测试侧只能自行推断一套建议码顶着。本节定死语义边界,自本节起不得新增码值或复用含义相近的码。
EM_E_NOENT、空窗口、空查询都属于正常分支。空结果一律走 EM_OK + n = 0 / rows = 0,不得用错误码表达「没找到」。EM_E_CORRUPT 或 em_open 失败。这条直接关系 G7「随机掉电 100 次,库 100% 可开」。EM_E_CORRUPT 只留给「库整体不可信」。if,不能是「某种异常情况下」。
| 码 | 值 | 触发条件(精确,可写成 if) | 调用方应然处理 | 可重试 |
|---|---|---|---|---|
EM_OK | 0 | 成功 | — | — |
EM_E_INVAL | -1 | ① 必填指针为 NULL;② cfg.ts_streams > cfg.ts_l1_pages;③ em_blk_reg_* 的 off/len 非 4 对齐或越出 region;④ 记录体 > 632 B、text_len > 150、meta_len > 64、vec_len > 384 | 修参数。同样参数重试必失败 | 否 |
EM_E_NOMEM | -2 | ① cfg.arena == NULL 或 arena_bytes < EM_ARENA_MIN;② em_pool_alloc 失败 | 加大 arena(按 PSRAM / DRAM 拆分),或先淘汰再重试 | 部分 |
EM_E_FULL | -3 | ① 记录区无空闲页可分;② ANN 节点数已达 rec_cap;③ 时序 L1 无页可写。取证区不返此码(定长槽,满则覆盖最旧) | 先触发策略层遗忘再重试;或向宿主报「容量已满」 | 是(淘汰后) |
EM_E_NOENT | -4 | em_mem_get / em_mem_del 的 id 不在记录集;em_frag_export 的 frag_id 不存在 | 正常分支,不是错误。按「没有」处理 | — |
EM_E_EXISTS | -5 | ① em_mem_put 指定 id 且该 id 已存在(非覆盖语义);② L1 同一分钟重复写:8 B 行没有样本计数字段,覆盖要擦整页,会连坐同页其它分钟 | 改用新 id,或先删再写。引擎不得静默覆盖 | 否 |
EM_E_TX | -6 | 无活动事务时 commit / abort;嵌套 em_tx_begin;事务外调用需事务上下文的写操作 | 修调用流程 | 否 |
EM_E_TXFULL | -7 | ① 脏页数 > cfg.max_dirty_pages;② WAL 剩余空间 < 本事务需求 | 分批提交——这是设计意图不是失败:拆成多个较小事务后重试 | 是(分批后) |
EM_E_IO | -8 | ① em_blk_reg_write 撞 INV-2(目标页未擦);② 底层介质读写失败。严禁做「读-改-写」兜底 | 记日志;事务内则 abort。同样参数重试必失败 | 否 |
EM_E_CORRUPT | -9 | ① 两槽 superblock 均无效(magic 或 CRC);② 记录结构自相矛盾(rec_len 越界、页链断裂) | 停止写入;尝试只读导出快照后重新格式化 | 否 |
EM_E_NOSUPPORT | -10 | ① emdb 后端未授权时调用其专属能力;② em_sync_push / em_sync_pull 的 MVP 最小实现 | 降级处理 | 否 |
EM_E_POWER | -11 | io 对象被 em_flash_pcut_arm 置为失效,其后所有 IO 均返回此码,直到重新 em_flash_open | 仅 PC 仿真存在,板端不会出现。生产代码不得依赖 | 否 |
| 易混点 | 裁定 |
|---|---|
单条记录 CRC 失败 与 EM_E_CORRUPT | 前者是遍历终止条件,库仍可正常打开;后者是库整体不可信。混用会把一次可恢复的断电残留升级成「库坏了」 |
EM_E_NOENT 与 空结果集 | 查询无命中走 EM_OK + n = 0 + reject;EM_E_NOENT 只用于「按 id 定位不到」这类明确定位失败 |
EM_ERR_NOT_IMPL(-999) | 不属于 HAL 契约,是测试桩专用码,不得出现在引擎实现里。引擎对未实现能力返回 EM_E_NOSUPPORT |
em_err_e,不得返回自定义负数或 errno;QA 骨架的自建码只在测试桩内部有效。EM_E_FULL 与 EM_E_TXFULL,不得合并)。上表定义了每个码各自的触发条件,但没定义「同一条调用同时满足两个条件」时返回哪个。没有阶梯,同一个调用在不同模块会返回不同的码,QA 就写不出稳定断言。
L1 EM_E_POWER 介质对象已失效(仅 PC 断电注入)——任何 IO 之前先判,最高优先 L2 EM_E_INVAL 参数 / 句柄状态非法(调用方 bug) L3 EM_E_TX 事务状态机错误 L4 EM_E_NOSUPPORT 本构建 / 本 cfg 下该能力不存在 L5 EM_E_NOMEM arena 池耗尽(配置问题) L6 EM_E_TXFULL 单事务装不下(可分批重试) L7 EM_E_FULL 目标区满(compact / 淘汰后可重试) L8 EM_E_EXISTS / EM_E_NOENT 存在性判定(正常业务分支) L9 EM_E_IO / EM_E_CORRUPT 介质层给出的结果,不参与上层码值选择
EM_E_INVAL。调用方拿到 INVAL 就知道是自己的问题,不必去查容量。EM_E_NOENT。「这台设备没有时序能力」与「这一分钟没有数据」对策略层是两个决策。IO / CORRUPT 是介质层给出来的结果,不是上层在候选里挑的。上层不得把「区满」改判成 IO,也不得把「配置与介质布局不匹配」升级成 CORRUPT。
| 原表述 | 更正 | 理由 |
|---|---|---|
EM_E_FULL 触发条件③「时序 L1 无页可写」 | 改为 EM_E_NOSUPPORT | L1 是环形,按页回绕覆盖最旧页,不存在「写满」这种状态。所谓「无页可写」实为 L1 区 0 页,那是「本 cfg 下该区不存在」,属能力缺失。FULL 的语义是「区在但暂时没有落点,compact / 淘汰后可重试」,而 0 页区重试多少次都不会有——两者的调用方动作完全不同,写错会让上表的「可重试」列给出错误指引 |
EM_E_INVAL 触发条件②「cfg.ts_streams > cfg.ts_l1_pages」 | 改为「解析后的 n > ts_l1_pages」 | ts_l1_pages == 0 表示取默认(SPEC §2.8.0 解析顺序 O2),拿 cfg 原值 0 去比较会把 ts_pages=8 这类合法配置误判为非法 |
| # | 主张 | emlite 的落地方式 | 对应判据 |
|---|---|---|---|
| 1 | 运行时不调用 malloc(自研内存管理器) | 全预分配池;em_cfg_t.malloc_free = true 时运行期 malloc 计数必须为 0 | G3 / G8 |
| 2 | flash 的不可预测性来自介质之上的层(FTL / 文件系统),不是 flash 本身 | 擦除与 GC 只在事务边界外执行;查询路径只读 + 顺序写 | G2 |
| 3 | 回滚成本必须绑定已知写集(CoW 显式跟踪修改页) | CoW 显式跟踪脏页;abort 只回滚已记录页,耗时可分析 | G8(abort P99 ≤ 2×P50) |
| 指标 | 目标 | 测量方式 |
|---|---|---|
.text 体积 | ≤ 60 KB | 解析 .map,统计 libedgehippom.a 的 .text 段 |
| 运行期 malloc 次数 | 0 | 包装 malloc/calloc/realloc,进入 RUNNING 状态后计数 |
| abort 耗时抖动 | P99 ≤ 2 × P50 | 1,000 次 em_tx_begin → put ×N → abort,记录耗时分布 |
tx_id、页号、旧值长度、CRC32em_cfg_t 决定)COMMIT 标记(4 KB 页内,单次编程)→ 提交成功后才更新 superblock 指针seq 与 CRC32;启动时取 seq 大且 CRC 有效的一份em_flash_io 抽象 + PC 文件仿真实现:引擎层单测全部在 PC 上跑,掉电测试还能精确控制断电时刻(写到第 N 个字节后中止)——这是 G7 能被测得可信的前提,也是最快拿到结论的路径。
| 项 | 规格 | 依据 |
|---|---|---|
| 索引结构 | IVF + graph 混合(不是纯 HNSW) | eXtremeDB 官方白皮书在 MCU 级推荐 IVF+graph 混合 |
聚类数 nlist | 32–64(12,000 条约 每簇 200–400 条) | 需实测扫描簇数 nprobe 与 P99 的关系 |
| 落盘顺序 | 离线聚类后按簇顺序落盘 | 白皮书:建库是 I/O 主导而非 CPU 主导,顺序写可显著降低 flush 抖动 |
| 距离 | int8 内积 / 余弦,SIMD(ESP32-S3 有向量指令扩展) | — |
| 调优目标 | 为可预测性调优,而非峰值召回 | G2 的 P99/P50 < 4 |
若 P99 超标,解法是这三件事(不是换 MCU):① 按簇顺序落盘;② IVF+graph 混合与 nprobe 调参;③ 为可预测性调优。量级不足时再考虑降维量化(W1-14)。
MVP 只有四种实体。三场景(S1 工业振动 / S2 野外巡检 / S3 私域知识库)共用同一份 schema——差异只在灌进去的碎片和宿主是谁。这是「做器官」的工程证明。
| 字段 | 类型 | 说明 | 约束 |
|---|---|---|---|
id | u32 | 全局唯一,0 表示由引擎分配 | 主键 |
frag_id | u32 | 所属碎片。用于按碎片整体删除 / 导出 / 接力 | 索引 |
ts_ms | i64 | 记忆时间戳(写入时刻,非事件时刻) | 时效与遗忘依据 |
prio | u16 | 优先级 0–1000。越大越不易被遗忘 | 默认 500 |
ttl_h | u16 | 时效(小时),0 = 永不过期 | 策略层判据 |
dim / dtype | u16 / u8 | 向量维度与类型(NONE / INT8 / FP16) | v1 碎片为 NONE |
vec | bytes | 向量。序列化时置于记录末尾(ADR-010) | 默认 384 B(int8/384d) |
text | utf8 | 摘要 / 原文片段 | ≤ 150 B,截断用 em_utf8_cut |
meta | json | 结构化元数据:dev_id / src / tags / scene / author | ≤ 64 B 建议 |
flags | u16 | DELETED / PINNED(不参与遗忘)/ CONFLICTED / SYNTHETIC | — |
vec 放在最后:现有 em_kb_search 是文本/标签匹配(无向量)。.emf 分 v1(纯文本)/ v2(带向量)两代,v1 固件读到 v2 记录时,按 rec_len 跳过末尾的向量段即可安全处理。schema 从第一天就预留向量字段,是向 v2 平滑演进的前提。
| 字段 | 类型 | 说明 |
|---|---|---|
frag_id | u32 | 碎片唯一 ID |
name | utf8 | 例 docs-core-v1 / expert-pack-v3 |
scene | u8 | 1=工业振动 / 2=野外巡检 / 3=私域知识库 |
count / bytes | u32 | 条数与占用 |
ver | u32 | 版本号。同 name 高版本覆盖低版本 |
created_ms | i64 | 生成时刻 |
quota | u32 | 接力配额(内部记账,见 03 号分册) |
时序数据的提炼结果。靠去重收敛——新 Pattern 与已有条目余弦 > 0.95 则合并更新而非新增(ADR-007)。
| 字段 | 类型 | 说明 |
|---|---|---|
pattern_id | u32 | — |
vec | bytes | 窗口特征向量(与 Memory 同维度,可同一索引) |
label | utf8 | 例 inner-race-fault-BPFI |
hit_count | u32 | 被命中次数。参与优先级提升 |
last_hit_ms | i64 | 最近命中时刻。参与遗忘 |
example_ref | u32 | 指向取证区中一个代表性原始窗口(L0 转存) |
| 层 | 内容 | 落哪里 | 保留 | 说明 |
|---|---|---|---|---|
| L0 | 原始波形 | PSRAM 环形缓冲 (不落 flash,ADR-006) | 10 s | 10 s @ 4 kHz × 3 轴 int16 = 240 KB。只有判定异常的窗口才转存到取证区 |
| L1 | 分钟级聚合 | flash 时序区(列式) | 30 天 | 每流每分钟:min/max/mean/std/rms/峰峰值 |
| L2 | Pattern(模式) | flash 记忆区(与 Memory 同索引) | 长期 | 靠去重收敛,这就是容量控制阀 |
| 规则 | 触发条件 | 动作 |
|---|---|---|
| 去重 | 新条目与已有条目余弦 > 0.95 | 合并更新(保留较高 prio,hit_count 累加)而非新增 |
| 冲突消解 | 同 tags 下出现语义相反条目 | 保留 ts_ms 更新者,旧条目标记 CONFLICTED 并降 prio |
| 时效过期 | now - ts_ms > ttl_h 且非 PINNED | 标记 DELETED,参与 compact |
| 容量压力 | 占用 > 85% | 按 prio 升序 + last_hit_ms 升序淘汰,至 75% |
| 拒答话术 | top-1 分数 < 阈值 | 返回「不知道」,不编造。断网时的话术模板见 04 号分册 P5 |
偏移 长度 字段 说明 0 8 magic "EMKBIMG1"(8 字节 ASCII,无结尾 \0) 8 1 version 1 = 纯文本;2 = 带向量 9 1 vec_dtype 0 = NONE;1 = INT8;2 = FP16 10 2 vec_dim 向量维度(version=1 时为 0) 12 2 header_len 固定 32 14 2 flags 位0=PINNED_ALL;位1=OVERWRITE_SAME_NAME 16 4 rec_count 记录条数 20 4 payload_len payload 字节数 24 4 payload_crc32 CRC32(payload) 28 4 frag_id 碎片 ID(0 = 由设备分配)
偏移 长度 字段
0 4 rec_len 整条记录字节数(含本字段,4 对齐)
4 4 id
8 4 frag_id
12 8 ts_ms
20 2 prio
22 2 ttl_h
24 2 text_len
26 2 meta_len
28 2 vec_len
30 2 flags
32 ... text[text_len]
... meta[meta_len]
... vec[vec_len] ← 向量置于末尾(ADR-010)
... pad to 4
rec_len → 消费 text + meta → 按 rec_len 直接跳到下一条,安全跳过末尾的 vec。反之 v2 读 v1 时 vec_len = 0,视为无向量。
payload_crc32 校验通过才开始写name 高 ver 覆盖低 ver;OVERWRITE_SAME_NAME 未置位时同名碎片导入应被拒绝并给出明确错误magic 错 / version 不支持 / CRC 不符 / rec_count 与实际不符 / 单条 text_len > 150现有 em_crc32() 已实现,可直接复用。KB 镜像 OTA 走 POST /kbota,支持 ?dry=1 预检。
① 分块大小:4 KB 是 HTTP 侧口径,串口侧必须 1 KB。
承载差异,不是退化。串口 CLI 单行上限 LINE_MAX = 2048,base64 膨胀 4/3:
1024 B → 1368 字符,加 JSON 包裹约 77 B = 1445 ≤ 2047(余量 602);
而 4096 B → 5464 + 77 = 5541 ≫ 2047,整行会被 cli_task
直接丢弃且零响应(连"块太大"都报不出来,现场表现为超时)。
两侧共用同一套 commit 校验(累计长度 + 整文件 CRC32),语义一致。
② 头域完整性由传输层负责,引擎只管 payload。
em_frag_import() 在拿到完整字节流后,对 32 B 文件头只做结构校验
(magic / version / header_len / rec_count 与实际条数一致 /
单条 text_len ≤ 150),不校验头自身的完整性——
payload_crc32 只覆盖 header 之后的 payload。
头字节损坏若未破坏上述结构约束,不会被引擎检出;
这份风险由传输层接住:frag.commit 验的是整文件(含头)CRC32,
不符即丢弃、imported = 0、不碰库。
板端实证(COM3,2026-10-28 重构后固件 v1.20261028-160744,不是推断):
造一个合法包,运输途中把头里 frag_id(offset 28)最低位翻转(8403 → 8402,payload 一个字节没动):
0x6DAF6629,
即引擎侧所验的那一项必然放行(这是反证:引擎单独看不出这个包坏了);frag.commit 的整体 CRC32 拦下:rc = -9,
实收 0x300719CD vs begin 时承诺 0x68828C92;imported = 0,库条数 1085 → 1085 不变,翻转出来的碎片域 8402 没有被凭空建出来。若不拦的后果:那批记录会静默落进一个凭空冒出来的碎片域,
事后在 frag.list 上只看到「多了个不知道哪来的碎片」,无从追溯。
这条实证说明 frag.commit 的整文件 CRC 是承重的,不是冗余校验。
⚠ 引用纪律(务必遵守):上面两组 16 进制数是那一次、那个包的量。
同一天里三次复现就得到了三组互不相同的数值:
0x9AA0EE81(旧固件)→ 0x18B91930 → 0x6DAF6629(重构后固件),
结论始终稳定、数值每次不同。
它们是留痕的实测证据,不是固定期望值,禁止抄进任何断言:
判据是「两者是否相等」(payload CRC 字段 == 实算值 ⇒ 引擎必然放行;
整体 CRC != begin 承诺值 ⇒ 被拦),不是和常数比;写成常数会在换包后变成
「期望值过期导致的假红」,把一次正常复现误判成回归。
③ 实测错误分野(G7 判据口径,勿混用):
magic → EM_E_INVAL(-1)「包非法」version 改成不支持的 2 → EM_E_INVAL(-1)rec_count(CRC 仍自洽)→ EM_E_INVAL(-1)EM_E_CORRUPT(-9)「包校验失败」EM_E_CORRUPT(-9)(头损坏传导到 payload 解析失败),
不是引擎在验头 CRC——是整文件 CRC 先被传输层接住才表现为 CORRUPT。
两层的分工必须分清,否则 G7「坏包一律拒绝」的判据会被误读成「引擎会验头」。④ 单包容量上限(串口侧):会话缓冲 2 MB(PSRAM);
超过 2 MB 的 frag.begin 明确返回 EM_E_NOMEM(-2),不静默改小。
已实测单包灌入 12,000 条(907,992 B / 887 块)成功(Dev-B,该包 76.6 B/条)。
★ 2 MB 是「会话缓冲上限」,不是「可导入容量」的承诺。
分界存在,且落在 60 与 64 B/条之间——谁先到顶取决于记录尺寸 s。
三个量、两条公式:
s = 落库每条字节数。EM_REC_STORE_MAX=644 且 4 字节对齐 ⇒ s 只能取 {44, 48, …, 644},连续值不可达。s − 12 字节。实测锚点:12,000 条 / 线上 1,003,960 B = 83.663 B/条,落库 95.66 B/条,差 11.997 ≈ 12。C = 板记录区字节数(见下)。| 量 | 公式 | 含义 |
|---|---|---|
N_board | C / s | 板能装的条数(用落库尺寸算) |
N_sess | 2,097,120 / (s − 12) | 2 MB 会话上限内线上能装的条数(用线上尺寸算;2,097,152 − 32 B 头) |
两者不约掉 s(一个除 s、一个除 s−12),所以必然存在分界。
Dev-A 的 C = 2,590,800 B = rec 区 635 页 × 4080 B/页。两个因子的证据强度不同,必须分开说:
| 因子 | 值 | 证据强度 |
|---|---|---|
| rec 区页数 | 635 | 板端实证:em.stat 报 page_total=992;灌满撞 EM_E_FULL 时恰落在 635/992;与分区表几何逐级扣除(保留/WAL/时序/ANN)所得一致 |
| 页内可用字节 | 4080 | 引擎常量推算(EM_PAGE_SIZE 4096 − 16 B 页头),从未被直接读取 |
前提:实际烧录的分区表 == 仓库 partitions_emgw.csv(Dev-A 8MB / 992 页 / rec 区 635 页)。分区表若换,C 重算。
⚠ em.stat 的 bytes 不可当作 C 引用。
它是写指针的页字节位置,不是容量——源码为证:em_core.c:127-130
occupied = (rec_cur_page − rec_first) × EM_PAGE_SIZE + rec_cur_off; out->bytes = occupied。
反常信号:清库后一页零记录却报 bytes=16。任何用 em.stat.bytes 直接当容量、或用 bytes/count 反推记录尺寸的做法,都会得到错数。
⇒ 更不要用 em.stat.bytes 去裁决细小的字节差:它是个口径本身就含糊的量(还夹着页尾弃置),
拿它分辨 S=44 上两法 72 B 的预测差(2,600,808 vs 2,600,880)等于零信噪比——那一档要裁决就数条数,不要称字节。
⚠ 读表前必读:本表 P(进入页数)是两列,含义不同,引用时必须写明是哪一列。
P_full = 灌到理论最大条数时的进入页数;P@accepted = 灌到实际提交水位(EM_FRAG_BATCH=8 整批回滚后的条数)时的进入页数。
板端 em.stat 的 rec_pages_used 读到的是 P@accepted,不是 P_full。
两列是否相等取决于用哪种算法:S=644 上读码推定(闭式)给 P@accepted = 634、读码推定(逐页模拟)给 635
(两者都不是实测;见 ★ 与下方 note 块)——
未实测裁决前并列列出,禁止只挑一个写。
⚠ 抬头说明(读这张表前第二条必读):
① 本表 N_board / P 两列的两种取值全部是「读码推定」,没有任何一个是实测——
「读码推定(逐页模拟)」模拟的是对 em_mem.c 的理解,「读码推定(闭式)」是代数近似,二者都不是板子行为的观测值(都不是实测);
② 两法差异只来自一件事:是否计入「页尾弃置」(见下方 note 块)——这是记录劈两段导致相位漂移后的二阶效应;
一阶命题「记录级零头不浪费、slack ≡ 0」两法一致(这也是第三次撤回的正确结论,未被推翻);
③ 权威值取「读码推定(逐页模拟)」(不是实测,只是建模更细),闭式值以「(闭式 …)」并列留痕;S=48/60/64/112 四档两法一致,S=44 / S=644 两档 ★ 处为分歧点;
④ 本表的实测锚点有两行:S=60 与 S=112(S=60 = #59,2026-10-28,Dev-A / EME-71FAC8,日志 _out59.txt;S=112 见 ⑤,三条限定缺一不可):
count=43,176 / em.stat.bytes=2,600,720 / rec_pages_used=635 / frag.list.bytes=2,590,560 / 尾页 cur_off=3,856。
该行同时锚定了四个量:U=4080(反解)、H=16、T=0、W=0
(2026-10-28 team-lead 裁定:【留痕·原稿笔误】原稿曾把本档记录大小 S = 60 误写进 T 的位置,
已裁定订正为 T=0——60 是 S,不是 T;
T 的符号位已被 R = 16P + W + (P−1)T 中的「每页追加开销」占用,不要给它换新名字)。
其余四档(44 / 48 / 64 / 644)仍是「读码推定(逐页模拟 / 闭式)」,不是实测——
它们没有板端数据支撑,谁标实测谁负责。
由实测锚点反解出的公式是 U = 4080 = (2,600,720 − 16×635) / 635,其中 635 与 2,600,720 是实测、16 是源码常量,
4080 因此是"反解"不是"直接实测"——对外一律写"板端实测反解",不许简写成"实测 4080"。
⚠ 落地前算术复核(spec-cap-table,2026-10-28):上面这段里两处存疑,先按原文留痕、不擅自改字,待 team-lead 裁定。
【2026-10-28 裁定结果:② 已裁定(原稿误把本档记录大小 S = 60 写进 T 的位置 ⇒ 订正为 T=0,见下;
T 与 S 不是同一个量);① 仍存疑未裁定,保留原文。】
① 该反解式差 240 B:(2,600,720 − 16×635) / 635 = 2,590,560 / 635 = 4,079.62… ≠ 4080。
缺的 240 B 恰是本档尾批被整批回滚的 4 条(4 × 60,见下方「★ 批量粒度」段)——补回即
(2,600,720 − 16×635 + 240) / 635 = 2,590,800 / 635 = 4,080 ✓。
⇒ 严格写法是 U = (bytes − 16×P + 尾批残留) / P,而尾批残留(240 B)本身不是观测量、是批粒度推论;
这恰好加强了原结论:4080 只可能是"反解",不可能"直接实测",但原式右侧的数是算不到 4080 的。
② T 的符号冲突 —— 已于 2026-10-28 由 team-lead 裁定:本文件下方 note 块把 T 定义为 R = 16P + W + (P−1)T 中的每页追加开销,并由同一组实测反解得 T = 0(见「† / ✓ 弃置标记」段);
【留痕·原稿笔误】原稿曾把本档记录大小 S = 60 写进 T 的位置,与上式直接矛盾
(那个 60 是 S,不是 T)⇒ 裁定为笔误,已改为 T = 0;
T 的符号位继续由 R = 16P + W + (P−1)T 占用,不换名(给它换新名字会让两个 T 并存,比笔误更难查)。
裁定后本表可以引用 T,但只能按「每页追加开销」这一个定义用。
承接(原 ④ 的其余内容,未被上面覆盖,保留留痕):
反解链 cur_off = 2,600,720 − 634×4096 = 3,856 = 16 + 3,840 ⇒ 尾页 64 条、前 634 页各 68 条(634 × 4080 = 2,586,720)
⇒ U = 4080、H = 16 由实测反解确认;又因 R = 10,160 = 16×635 恰等无余量 ⇒ W(S=60) = 0。
由 U = 4080 与实测 rec_pages_used = 635 ⇒ C = 635 × 4080 = 2,590,800 B,口径由「引擎常量推算」升为「实测反解」。
S=112(三条限定缺一不可,漏一条就是假绿):
【实测(旧固件 v1.20261028-104711 · em.bulk 路径 · C:\emgw-build\_cap_devA_final.txt:2/13)】
count = 23,132 / bytes = 2,600,944 / rec 页 = 635/992(清库前基线 :2 与灌库后 :13 两次读数一致);
反解 2,600,944 − 16×635 = 2,590,784 = 23,132 × 112 ⇒ 落库 112 B/条,
且 23,132 = ⌊2,590,800/112⌋ 是紧界(余 16 B,装不下第 23,133 条),末轮日志 rc=-3 库已满(EM_E_FULL) 亦为证。v1.20261028-104711(_cap_devA_final.txt:1),不是当前 19:18:32 固件;
限定二:走 em.bulk 路径(em_capacity.py:104-107 传 {"n","batch","prefix","frag_id","prio","start"}),不是 frag.begin/chunk/commit;
限定三:因此不受 EM_FRAG_BATCH=8 约束——23,132 % 8 = 4,它不是 8 的倍数(见下方「★ 粒度」段的路径分野)。v1.20261028-104711 · em.bulk 路径 · _cap_devA_final.txt:2/13)」。
谁把它写成"当前固件实测",谁负责(写成"当前固件实测"就是假绿——当前固件的 frag.* 路径上没有这个数)。C = 2,590,800 的独立佐证——23,132 = ⌊2,590,800/112⌋ 是紧界(余 16 B,装不下第 23,133 条),
与 #59(S=60 余 240 B)并列 ⇒ C = 2,590,800 现被两个不同 S、两条不同导入路径独立佐证(两者余量都在 C 之内、且都装不下下一条)⇒ 比只有一个锚点强得多;
同档 N_board 列的读码推定值 23,132(读码推定,不是实测)与实测逐字相等。S=112 上本就一致 ⇒ 该锚点只验证 C 与 ⌊C/S⌋,不裁决两算法(裁决仍靠 S=44 / S=644);
② accepted = 23,128(8 截断)那一列在 em.bulk 路径上不成立,实测是 23,132 ⇒ 本表 accepted 列是 frag.* 路径口径;
③ 它也不验证 S=112 的 bytes 预测 2,600,496——那是 8 截断后的 frag.* 预测,实测 2,600,944 与之差 448 B = 4×112,是路径差不是误差。
⑥ accepted 的 8 截断是「路径专属」,不是普适结论:accepted_max(S) = 8 × ⌊⌊C/S⌋/8⌋(EM_FRAG_BATCH=8,em_frag.c:69,整批回滚)em_frag.c:69 #define EM_FRAG_BATCH 8u——不是 :67(:67-68 只是它上方的注释块,:26 是注释提及、:210 是使用点)。#define 有说服力,「整批回滚」这四个字就出自这两行)
【读码推定 · em_frag.c:265-266】:
265: accepted += batch_n; /* 越过提交点才并账:只有落库的才算 accepted */ /
266: bytes += batch_bytes;
⇒ 越过提交点才并账 ⇒ 撞 EM_E_FULL 时整批回滚 ⇒ accepted 必然是 8 的倍数,末页不足一批的空间被整批吃掉。
⇒ 末批残留最多浪费 7 条(S=60 时 420 B;S=644 时 4,508 B)。
实测佐证:S=60 尾页仅剩 240 B(= 4 条),而下一批要 8×60 = 480 B ⇒ 整批回滚,那 4 条的空间被吃掉 ⇒ 43,180 → 43,176。
反例(不许跨路径套用):S=112 走 em.bulk 实测 23,132,而 23,132 % 8 = 4 ⇒ 拿该式预测会差 4 条。
⇒ 用 ⌊C/S⌋ 还是用它,先看导入走哪条路;只有 frag.* 路径才做 8 截断。
S (B/条) | N_board | P_full | accepted | P@accepted | N_sess | 谁先到顶 |
|---|---|---|---|---|---|---|
| 44 | 58,843★(闭式 58,881) | 635 | 58,840★(闭式 58,880) | 635 | 65,535 | 板容(2 MB 过配)· text 全空,不可抽样回查 |
| 48 | 53,975 | 635 | 53,968 | 635 | 58,253 | 板容(2 MB 过配) |
| 60 | 43,180 | 635 | 43,176 | 635 | 43,690 | 板容 —— 最后一条"过配"取值 |
| 64 | 40,481 | 635 | 40,480 | 635 | 40,329 | 会话上限 —— 第一条"不过配"取值 |
| 112 | 23,132 | 635 | 23,128 | 635 | 20,971 | 会话上限(2 MB 不过配) |
| 644 | 4,021★(闭式 4,022) | 635 | 4,016 | 635★(闭式 634) | 3,318 | 会话上限(2 MB 不过配) |
连续解 s = 62.97(解 2,590,800/s = 2,097,120/(s−12)),
但 s ≡ 0 (mod 4) ⇒ 62.97 不可达,实际翻转点在 60 → 64 之间。
自洽锚点:s=112 时 N_board = 23,132,与容量实测 em.stat count = 23,132 完全吻合
【实测(旧固件 v1.20261028-104711 · em.bulk 路径 · C:\emgw-build\_cap_devA_final.txt:2/13)——三条限定见抬头说明 ⑤,一个字都不能漏】。
不加这三条限定就写"实测",读者会以为它是当前固件 frag.* 路径的数 —— 那就是假绿。
且它吻合的是 N_board 列(⌊C/S⌋ = 23,132),不是 accepted 列(23,128,8 截断只属于 frag.* 路径)。
◆ 六档表「两条路径并存」脚注:23,128 与 23,132 并存不是矛盾
本表 S=112 这一行同时出现两个数,这是两条导入路径,不是误差、不是笔误、不是版本冲突:
accepted 列的 23,128 —— 这是 frag.* 分块灌库路径的值:
8 × ⌊⌊C/S⌋/8⌋ = 8 × ⌊23,132/8⌋ = 8 × 2,891 = 23,128。
【读码推定】粒度 8 来自 em_frag.c:69 的 #define EM_FRAG_BATCH 8u;
整批回滚见 em_frag.c:265-266(accepted 越过提交点才并账)。s=112 实测 count = 23,132」的 23,132 —— 这是
em.bulk 路径的值:⌊C/S⌋ = ⌊2,590,800/112⌋ = 23,132。
【实测】旧固件 v1.20261028-104711 · em.bulk 路径 ·
C:\emgw-build\_cap_devA_final.txt:2(就绪基线)/ :13(灌库后),同一块板 EME-71FAC8。
⇒ 23,128 与 23,132 并存不是矛盾,是两条路径:差 4 条 = 448 B,
全部来自「frag.* 有 8 条整批回滚、em.bulk 没有」这一条机理差异。
不许把 23,132 当成 23,128 的"实测证伪",
也不许反过来拿 23,128 去"修正"实测的 23,132。
◆ 识别指纹(一眼分辨这是哪条路径的档):23,132 % 8 = 4
23,132 % 8 = 4 ≠ 0 ⇒ 它不是 8 的倍数 ⇒ 它不可能是 frag.* 路径的 accepted
(frag.* 的 accepted 必然是 8 的倍数)⇒ 这一档是 em.bulk,指纹成立。
反向对照:S=60 档实测 43,176,43,176 % 8 = 0,且 frag 公式给 43,176、
bulk 公式给 ⌊C/S⌋ = 43,180 ≠ 43,176 ⇒ #59 这一档确定是 frag.*,不是 em.bulk
【实测】_out59.txt,固件 v1.20261028-191832。
⇒ 判据一句话:count % 8 ≠ 0 ⇒ em.bulk 档;count % 8 = 0 且 count ≠ ⌊C/S⌋ ⇒ frag.* 档。
⚠ 指纹的失效边界:若 ⌊C/S⌋ 本身是 8 的倍数,两条路径给出同一个数,
指纹不可分辨 ⇒ 此时只能按"用的是哪条命令"判路径,不许靠数判。
◆ ⑨ 判据的盲区(显式登记,不许伪装成已覆盖)
⚠ 整块是「推算 / 读码推定」,没有一个是实测:它推导的是判据本身的能力边界;
板端那两个锚点上 Δ = 0(R = 10,160 = 16×635 精确相等),
没有任何实测证据表明存在 1..40 B 量级偏差 ⇒ 盲区是算式推出的能力缺口,不是板子上的观测现象。
⑨ 的判据是 |U8 − U6| ≥ 1e-4,其中 U8 − U6 ≡ (R − 16·P) / [P·(P−1)]
(恒等式,C:\emgw-build\_cfverify.py 的 check_identity() 逐格核对)。
记 Δ ≡ R − 16·P:
|Δ| ≥ 1e-4 × P × (P−1) = 1e-4 × 635 × 634 = 40.259 B
⇒ ⑨ 的盲区 = |Δ| ∈ 1..40 B(此区间内 ⑨ 静默)。
【推算】阈值换算式,非板端实测。R % 16 == 0 且 R // 16 == P
⇔ R == 16·P ⇒ ⑩ 静默 ⟺ Δ == 0,任何 Δ ≠ 0 都报警。
【读码推定】判据定义见 em_measure_cap.py:1133
(行号会漂移,以 grep -n "r % 16 == 0 and r // 16 == p1" em_measure_cap.py 为准)。|Δ| ∈ 1..40 B 由 ⑩ 全部接住
⇒ ⑨ 有盲区,⑩ 没有,二者互补构成完整覆盖。
【推算】逐点实算(P = 635,纯离线复算,非板端实测):
| Δ (B) | R | ⑨ U8−U6 | ⑨ | ⑩ 判据 | ⑩ |
|---|---|---|---|---|---|
| 0 | 10,160 | 0 | 静默 | R%16=0,R//16=635 == P | 静默 |
| +1 | 10,161 | +2.484e-06 | 静默 | R%16=1 ≠ 0 | 报警 |
| +16 | 10,176 | +3.974e-05 | 静默 | R%16=0,R//16=636 ≠ P | 报警 |
| +32 | 10,192 | +7.949e-05 | 静默 | R%16=0,R//16=637 ≠ P | 报警 |
| +40 | 10,200 | +9.936e-05 | 静默 | R%16=8 ≠ 0 | 报警 |
| +41 | 10,201 | +1.018e-04 | 报警 | R%16=9 ≠ 0 | 报警 |
| −16 | 10,144 | −3.974e-05 | 静默 | R%16=0,R//16=634 ≠ P | 报警 |
⚠ 留痕 —— 已被证伪的旧表述(不许复活):
旧表述一:曾写「注入 R += 16 ⇒ ⑨ 不报警、⑩ 也不报警」。
后半句错:Δ=+16 时 R//16 = 636 ≠ P = 635 ⇒ ⑩ 报警
(Δ=−16 时 R//16 = 634,同样报警)。正确表述是上面第三条。
旧表述二(team-lead 2026-10-28 自查,留痕不擦):曾写「⑩ 只在 Δ ≢ 0 (mod 16) 时报警」——
错,漏看了 R // 16 == P 这一半(属"没读全判据就下结论"):
⑩ 的两条判据必须同时成立,Δ=+16 时 R%16=0 虽成立、但 R//16=636 ≠ 635 ⇒ ⑩ 报警
(Δ=+32 时 R//16=637,同样报警)
⇒ ⑩ 对任意 Δ ≠ 0 都报警,根本没有盲区;正确口径是「⑨ 有盲区,⑩ 没有,二者互补构成完整覆盖」。
本块由纯离线复算得出,不是板端实测;复核脚本 C:\emgw-build\_cfverify.py 的
check_blind_spot()(行号会漂移,以 grep -n "^def check_blind_spot" _cfverify.py 为准),
把上面每条都钉成断言 —— 谁把阈值收紧到能覆盖 16 B(如 1.24e-6),
「⑨ 静默」那几条立刻变红。
◆ 复核脚本的两种模式与覆盖边界(team-lead 2026-10-28 裁定「甲」方案)
C:\emgw-build\_cfverify.py 有快模式与全量模式两种跑法
(开关 --check-anchors-only;行号会漂移,以 grep -n "check-anchors-only" _cfverify.py 为准):
· 快模式覆盖:2 个实板锚点(check_anchors())+ 恒等式(check_identity())
再加三类纯算术自检——⑨ 盲区断言(check_blind_spot())、
阈值语义(check_threshold_semantics())、写指针回滚(check_abort_rollback());
· 快模式不含空库锚点(P = 1 边界,check_real_board())——那一条只在全量模式跑。
⚠ 名字防混(必读):check_anchors()(2 个实板锚点:#59 S=60 frag 档、
旧跑 S=112 bulk 档)与 check_real_board()(空库锚点:em.stat 空库
P=1 / last=0 那三格)都叫"锚点"但不是一个东西——前者是容量锚点,后者是 P=1 边界锚点;
快模式只含前者。
★ 实测规模与耗时【实测 · 本机 subprocess 计时,非板端实测,frag-dev-2 2026-10-28】:
全量(默认)200 项断言,绿 200 / 红 0,exit 0;快模式(甲版)83 项断言,绿 83 / 红 0,exit 0;
快模式端到端墙钟 5 次:0.137 / 0.144 / 0.149 / 0.166 / 0.147 s(中位 0.147 s),
其中纯算术部分只 +0.0002 s,其余几乎全是 Python 解释器启动开销。
⇒ 触发条件(照这个选跑哪种,别一律跑全量):改 EM_FRAG_BATCH 或改 ⑨ 阈值后,
跑快模式即可(83 项里已含受影响的那三条);只有动到「空库 / P = 1 边界」或新增实板锚点时,才需要跑全量。
为什么必须补那三类(批甲的理由,别当随手改):裁决的触发清单里有「改 EM_FRAG_BATCH 后必须重跑」,
而 check_abort_rollback() 里"没回滚"的读数 240 B = 4 条 × 60 是半批量级,
BATCH 一变它跟着变 ⇒ 甲案之前快模式不含它,改了 BATCH 只跑快模式会看着全绿、实际漏判——
立了规矩但工具跟不上,比没规矩更糟。
★ 甲案收益的实证(不是纸上推演)【实测】:改前「快模式 + 阈值打回 0.5」是静默绿;
改后 rc=1 变红 ⇒ 快模式确实能抓到"改 BATCH / 改阈值"这类回归。
⚠ 永久口径(不是临时落后标记,别删):本段记录的是已裁定的目标覆盖;
实际覆盖以 python _cfverify.py --check-anchors-only 的标题行为准
(标题行由代码自己打印,代码改了它跟着变;本段文字不会跟着变)⇒
不要以本段文字代替实跑。核对调用项用 grep -n "check_blind_spot(t)" _cfverify.py
(行号会漂移,以 grep 为准)。
★ 落库口径(算"装不装得下"用这个):S = 12 + L,其中 L 是线上记录长度,12 = EM_REC_HDR_BYTES。
S=60 的实例校验(三条全部成立,已逐项复核):
线上 Σ L = 43,176 × 48 = 2,072,448 B;
落库 Σ S = 43,176 × 60 = 2,590,560 B(= frag.list.bytes);
写指针 = 落库 + 16 × 页数 = 2,590,560 + 10,160 = 2,600,720 B(= em.stat.bytes)。
校验:2,590,560 − 2,072,448 = 518,112 = 43,176 × 12 ✓;2,600,720 − 2,590,560 = 10,160 = 635 × 16 ✓。
三个口径不许混用:算容量用落库,算分块 / 线上传输用线上,em.stat.bytes 是写指针位置、不是剩余容量(见上方红框)。
★ accepted 的粒度由导入路径决定:frag.* 是 8 条,em.bulk 不是。
frag.* 按 EM_FRAG_BATCH=8 整批提交,放不下就整批回滚(em_frag.c:69 的 #define;
「越过提交点才并账 ⇒ 整批回滚」见 em_frag.c:265-266)⇒
accepted_frag(S) = 8 × ⌊⌊C/S⌋ / 8⌋(C = rec_pages × 4080)。
S=60:⌊2,590,800/60⌋ = 43,180,8 取整 ⇒ 43,176;frag.* 实测正是 43,176 ✓(#59)。
⚠ 别跨路径套用(有实测反例):em.bulk 路径 S=112 实测 23,132,而 23,132 % 8 = 4 ⇒
拿上式去预测 em.bulk 会差 4 条。em.bulk 侧与「accepted = ⌊C/S⌋」一致(23,132 = ⌊2,590,800/112⌋);
但仅凭这一个数据点无法把它的粒度钉死为 1——总数 23,132 与末轮写入数 3,132 都只排除了 8(3,132 % 8 = 4),
粒度 1 / 2 / 4 都不被这个数排除 ⇒ 本文只写「em.bulk 粒度 ≠ 8」,不写「= 1」,直到有人补跑一档能分辨 1/2/4 的测量。
推论(仅 frag.* 路径成立):由 accepted 反推 C 的不确定度粒度是 8×S,不是 1×S
(⌊C/S⌋ ∈ [accepted, accepted+7] ⇒ C ∈ [accepted×S, accepted×S + 8×S));
也解释了为什么 #59 停在 99.99%(余 240 B = 4 条,不够下一批的 480 B)。
⇒ 这条同时是上面"反解式差 240 B"的来源:2,590,800 − 2,590,560 = 240 就是被整批回滚吃掉的 4 条——
它不在任何观测量里,是从批粒度推出来的,所以带它的式子(含 C = 635 × 4080)一律是"反解"口径。
★ P 列的两种算法、算草与当前分歧(2026-10-28 第二轮复核)
量定义:N_board = 该 S 下能落库的最大条数;
accepted = ⌊N_board/8⌋×8(EM_FRAG_BATCH=8 整批回滚);
P = 进入页数;R = 16 × P@accepted(每页 16 B 页头的累计开销);
bytes 预测 = accepted × S + R;
N_sess = ⌊2,097,120/(S−12)⌋(线上每条 = 落库每条 − 12 B,EM_REC_HDR_BYTES=12,结构性差值非拟合)。
算法甲 · 读码推定(闭式)(不是实测;第一轮用的):N_board = ⌊C/S⌋,P = ⌈accepted·S/4080⌉,4080 = EM_PAGE_SIZE 4096 − 16 B 页头。
隐含前提:每页 payload 恒 4080。取整方向各自成立(条数只能整条 ⇒ 向下;最后一页哪怕只落进一个记录头也算进入 ⇒ 向上)。
算法乙 · 读码推定(逐页模拟)(不是实测;本轮新增,★★ 与上表 ★ 同源):照抄 mem_append_raw() 的三条分支逐条搬运——
① avail < 12 ⇒ 跳页(em_mem.c:109);
② S ≤ avail ⇒ 整条写下;
③ 否则 劈两段:先填满本页尾,余下部分写到下一页 off = 16 + (S − avail)(em_mem.c:155-164)。
③ 会把相位推到下一条 ⇒ 每一页内容起始的 off 不再总是 16 ⇒
跑一段时间后会出现 avail ∈ {4, 8}(< 12 但放不下记录头)⇒ 那 4/8 个字节被丢弃,该页实得 4076/4072 而非 4080。
因此「每页 payload 恒 4080」在一阶成立(不存在整条级别的零头浪费),在二阶不成立(存在 ≤ 11 B/页的页尾弃置)。
表中与本节出现的「模拟/逐页模拟」一律读作 读码推定(逐页模拟)、「闭式」一律读作 读码推定(闭式):
两者均未上板、未编译,是对代码的理解而非观测(都不是实测);在不致歧义处方简写为"模拟/闭式"。
—— 本轮据此下调了第一轮那句过强的表述(「页内零头不会被浪费、每页 payload 恒 4080」),留痕不改数据;
一阶结论(slack ≡ 0、不存在记录级零头浪费)仍然成立且未被推翻,被推翻的是它的二阶版本。
S | N_board逐页模拟(闭式) | accepted | accepted×S(B) | 页尾弃置(B) | P@accepted逐页模拟(闭式) | R = 16×P + W | bytes 预测 = accepted×S + R |
|---|---|---|---|---|---|---|---|
| 44 | 58,843(58,881) | 58,840 | 2,588,960 | 1,688 † | 635(635) | 11,848 | 2,600,808 |
| 48 | 53,975(53,975) | 53,968 | 2,590,464 | 0 | 635(635) | 10,160 | 2,600,624 |
| 60 | 43,180(43,180) | 43,176 | 2,590,560 | 0 ✓ | 635(635) | 10,160 | 2,600,720 |
| 64 | 40,481(40,481) | 40,480 | 2,590,720 | 0 | 635(635) | 10,160 | 2,600,880 |
| 112 | 23,132(23,132) | 23,128 | 2,590,336 | 0 | 635(635) | 10,160 | 2,600,496 |
| 644 | 4,021(4,022) | 4,016 | 2,586,304 | 844 † | 635(634) | 11,004 | 2,597,308 |
† / ✓ 弃置标记:出处标签与自检适用性(frag-dev 2026-10-28 补,team-lead 派单)
† = 读码推定(逐页模拟),不是实测。本表 S=44 的 W = 1,688 B、S=644 的 W = 844 B
均未上板——#59b 尚未跑,这两档禁止标"实测",也禁止把这两档的 accepted / bytes 当实测引用。
✓ = S=60 的 W = 0 已由 #59 板端实测确认
【实测反解 · C:\emgw-build\_out59.txt:31/35-39】count=43,176 / bytes=2,600,720 / rec_pages_used=635
⇒ R = 10,160 = 16×635 恰等、无余量 ⇒ W = 0(同时 T = 0)。
这是 W = 0 这一档上唯一被实测确认的锚点——S=112 的 R 虽也是实测的 10,160,
但它的 W = 0 是「读码推定」(gcd(4080,112) = 16),不是实测,见上方「恒等式」段与抬头说明 ⑤。
⚠ 自检适用性(逐档,勿跨档套用):有弃置的两档恒等式是 R = 16×P + W
⇒ R/16 == P 在 S=44 与 S=644 不适用:
S=44:11,848 / 16 = 740.5 ≠ 635;S=644:11,004 / 16 = 687.75 ≠ 635。
其余四档 W = 0 ⇒ R/16 == P 适用(S=60 已实测验证)。
⇒ 有弃置的两档一律用「条数」裁决,不要用「字节」裁决:S=44 上甲/乙的 bytes 预测只差 72 B,而 accepted 差 40 条(5 个批量)——称字节等于零信噪比。
★ 分歧落点与量级(务必看清再引用):
六档里只有 S=44 与 S=644 会产生页尾弃置。
失效条件是解析式,不必靠穷举背:弃置发生 ⇔ gcd(4080, S) ∈ {4, 8}
(推导:设 r = 4080 mod S,第 n 页末余量 a_n,劈分后下一页 a_{n+1} = (a_n + r) mod S
⇒ a_n 的可达值恰为 gcd(4080, S) 的全体倍数 ⇒ 4 或 8 可达 ⇔ gcd | 8;又因 S ≡ 0 (mod 4) 且 4080 ≡ 0 (mod 16) ⇒ g ≥ 4 ⇒ 判据化为 g ∈ {4, 8}。
151 个可达 S 全部命中,弃置者恰好 59 个;本表六档里 48/60/64/112 的 gcd 分别为 48/60/16/16 ⇒ 无弃置,S=60 差值为 0 ⇒ #59 的预测不受影响)。
影响量级:S=44 条数 −38 条(0.065%,58,881→58,843、accepted 58,880→58,840),P 不变;
S=644 条数 −1 条(4,022→4,021,accepted 仍为 4,016),P@accepted 由闭式 634 变成 635。
连带修正(2026-10-28 晚):有弃置时 R = 16×P + W 而非 16×P ⇒
S=44 的 R = 11,848、bytes 2,600,808;S=644 的 R = 11,004、bytes 2,597,308。
(闭式对应:S=44 R = 10,160 / bytes 2,600,880;S=644 P=634 ⇒ R = 10,144 / bytes 2,596,448。)
⇒ 注意一个反直觉点:S=44 上两法的 bytes 预测只差 72 B(2,600,808 vs 2,600,880),而 accepted 差 40 条 ——
这一档要用"数条数"裁决,不要用"称字节"裁决。
运行前置(2026-10-28 状态,照抄前先确认是否已解除):
测量脚本 em_measure_cap.py 目前对 gcd(4080, S) ∈ {4, 8} 的 S 加了前置断言、整跑拒绝(rc=4)——
而 S=44 的 gcd 恰为 4 ⇒ 当前这一档跑不起来。已提出改为"照跑 + 明确禁用 R/P 自检"(约 3 行),
状态更新(2026-10-28 20:36):守卫已按 team-lead 三条硬条件改造完成(em_measure_cap.py):
① 新增显式开关 --allow-waste-s,不给开关照旧拒绝(rc=4);
② 给开关后照跑,并打醒目警告横幅到 stdout 与结果文件两处,写明"本轮不产出任何 R/P 自检结论(⑥⑧⑨ 一律不判、不给数),唯一裁决量是条数";
③ ⑥⑧⑨ 已用 rp_selftest_allowed(S) 显式门控;条数核对仍走已存在的 probe_frag_list()(frag.list 从介质重新点一遍,无条件调用)。
⇒ S=44 现在可以跑,但必须显式带 --allow-waste-s,且只认条数结论。
(另:该档另有 accepted_max = 8×⌊⌊C/S⌋/8⌋ 的批截断,见抬头说明 ⑥。)
(方法论一条,留这免得下次重犯:该守卫当初实现成"拒绝整个运行",理由是"不产出误导性的红";
而 #59b 选 S=44 的目的恰恰是绕开 R/P 自检不可用的档位、纯靠条数裁决
⇒ "拒绝整个运行"把被测对象一起挡掉了。守卫粒度必须比它保护的东西更细,否则会把要测的东西一起屏蔽。)
推论:所谓「S=644 上 P_full 与 P@accepted 不等(635 vs 634)」是闭式近似的产物,逐页模拟下两者同为 635。
两算法谁对,不由文档裁定:判据跑一次即可(见下「自检式」),#59 未跑前并列列出。
自检式(2026-10-28 晚修正,上一版写错了;再修:由「区间夹」改为精确恒等式):
★ 恒等式:R ≡ bytes − count×S 必须精确等于 16×P + W(S),
其中 P = P@accepted、bytes 取板端 em.stat.bytes、
W(S) = 页尾弃置累计数(本表第 5 列),其取值由判据 弃置发生 ⇔ gcd(4080, S) ∈ {4, 8} 决定:
gcd(4080, S) ∈ {4, 8} ⇒ W(S) > 0;否则 ⇒ W(S) = 0。
这是等式,不是区间夹——两侧必须逐字节相等,差 1 B 即为模型失效;
弃置那几字节不进记录也不进页头,只能落在 R 里 ⇒ 有弃置时 R 必然大于 16×P,且多出来的部分恰等于 W(S)。
两档对照(恒等式的两个检验点):
· S=60:gcd(4080, 60) = 60 ∉ {4, 8} ⇒ W = 0 ⇒ R 必须恰好 10,160;
实测 2,600,720 − 43,176×60 = 10,160 ✓(#59,_out59.txt:31/35-39)。
· S=112:gcd(4080, 112) = 16 ∉ {4, 8} ⇒ W = 0 ⇒ R 必须恰好 10,160;
实测 2,600,944 − 23,132×112 = 10,160 ✓。
⚠⚠ S=112 的 W = 0 是「读码推定」(由 gcd 判据推得,#59b 未跑),不是实测,不许顺手标成实测;
S=60 的 W = 0 才是实测确认(下方 ✓ 标记)。两档虽同为 W=0,标签不同,引用时必须分开写。
⛔ 区间夹(R ∈ [16P, 16P + 16 + 上界) 那类写法)—— 已于 2026-10-28 由 team-lead 裁定:该上界给不出出处 ⇒ 一律删除,数字亦不得留在文件里:
① 本文件当前没有这种写法(2026-10-28 全文件 grep:零命中)⇒ 无需删除;
② 该上界的出处经 team-lead 本人复核:推不出来(既非源码常量、也非任何观测量、也非某个式子的输出)⇒
按「出处可指律」:给不出出处就删,不留"降到次位"这个选项,也不留数字(留着等于默认它可以被引用);
③ 替代它的是上面的精确恒等式:R ≡ bytes − count×S 必须精确等于 16×P + W(S)——
点值断言比区间夹强得多:S 选错一个 lane,R 立刻偏移 100 量级,一眼可见;
而一个宽数百 B 的区间会让 W 的任何推算值都"落在区间内",等于不检验。
实测正例(2026-10-28,#59):S=60 实测 R = 10,160 ⇒ R/16 = 635 = P@accepted ✓(且等于板端 rec_pages_used = 635)——
这是该式在 W = 0 档上的实测通过实例;
反例仍是 S=44(R = 11,848 ⇒ R/16 = 740.5 ≠ 635)——正例反例成对,正好说明"该式只在 W=0 档成立"不是纸上推演。
(发现人 frag-dev;本文档作者已用模拟器独立验证:六档的写指针位置与 accepted×S + 16P + W 逐格相等,见下校验行。)
两种正确写法,二选一但必须写明用的是哪种:
(R − W) / 16 == P@accepted,且 P@accepted 必须等于板端 em.stat.rec_pages_used;代价是 W 本身来自读码推定(不是实测),等于在自检里引入了第二个模型;R/16 == P@accepted 的适用范围限定在 gcd(4080, S) ∉ {4, 8} 的 S 上(共 92 个,含本表 48/60/64/112,也含 #59 要测的 S=60 ⇒ #59 不受影响)——建议用这条:它不把推定值写进判据。校验行(可直接复算):
S=44:58,840×44 + 16×635 + 1,688 = 2,588,960 + 10,160 + 1,688 = 2,600,808;
S=644:4,016×644 + 16×635 + 844 = 2,586,304 + 10,160 + 844 = 2,597,308;
S=60:43,176×60 + 16×635 + 0 = 2,600,720。三者与模拟器逐步搬运出的写指针位置完全相等。
禁止硬编码:不许写 == 635,也不许把某一档的 P / R / bytes 抄成常数——S=644 在两算法下 P 就不同(634/635),S=44 的 R 还会随 W 变;硬编码会在换算法当天变成假红。
S=44 这档的可用性限制(板端实证):工具报 S=44 反解出 text_len=0:本包 text 全空——
因为 S = 12 + rec_len、rec_len 最小 32 B 全是头域,留不出 text。
结论:S=44 能测容量,但抽样查回(按 text 定位)失效。本档保留在表中仅供容量下界参考,任何"可回查"用例不得使用 S=44。
但这档的交叉验证仍有一条可用路径,且只有这一条:
❌ em.query 按 text 抽样查回 —— 失效(所有记录 text 均为空串);
✅ frag.list —— 仍然有效:它走 emlite_frag_list()(fw_ascii 树:签名行 em_frag.c:292,开花括号 293)扫 flash 页、
逐条 CRC 校验、从记录体 offset 8 取 frag_id,完全不依赖 text,输出 count / bytes ⇒
是独立于 accepted 的第二路计数。⇒ S=44 上的核对请用 frag.list,不要用 em.query。
这条路已被实测坐实(#59,2026-10-28):frag.list 报 frag_id=9601、count = 43,176、bytes = 2,590,560,
与 accepted 完全一致,且 bytes = accepted × 60 精确相等(em_frag.c:357-358 累加的是含 12 B 私有头的落库尺寸)。
★ frag_id=9601 的归因(实测值不变,归因于 2026-10-28 由 team-lead 裁定改写):
它由 frag.begin 的 opt.frag_id 显式传入并生效(em_frag.c:219-232 第一优先级)——
本档那次灌库由 em_measure_cap.py 发起,它给 frag.begin 传的是 9600 + b(b = 批序号,第 2 批即 9601),
9600 只是该脚本自己的编号约定,不是固件常量;不传时的固件自动分配规则是 frag_max_id() + 1(em_frag.c:224-231)。
实证(#60,2026-10-28,COM3,实测):传 --frag-id 9777 ⇒ 落库 9777(显式值生效);
库内已有 9777 时不传 ⇒ 落库 9778(= frag_max_id() + 1)。
⛔ 「9600 + session」已被证伪,本文件不得再出现这种说法——9600 在固件里零命中;
把它当"固件的自动分配规则"转述出去,是本档已经犯过一次并被撤回的假绿(_run_fid_split.py,作者本人撤回留痕)。
⚠ 同名陷阱:同一份 em.stat 里的 frag_bytes = 0 与 frag.list 的 bytes 不是同一个量,
禁止拿 frag_bytes 当"碎片占了多少字节"。
(另注:frag.list 本身的口径边界见本条 ⑤——它不做墓碑回溯扣减,发生过删除的库其 count/bytes 可能偏高。)
N_sess 的来源(诚实版,勿夸大为独立印证):表中 N_sess 由 em_emf.py:587 按 (2,097,152 − 32)/(S − 12) 下取整算出;
本轮核过 fw_ascii/main/include/emlite_bridge.h(注意路径是 main/include/):第 229 行确有容量推算,但写的是 18,600,不是 20,971——
18,600 / 18,724 = 2,097,152 / 112,用的是落库 112 B 这一侧(口径错);
而 20,971 = (2,097,152 − 32)/100 是同一条公式把口径改成线上 rec_len = 100 之后的更正值(由 frag-dev 提出、并已由其本人撤回"独立"属性)。
⇒ 两个数出自同一条公式,只是一前一后改了口径 ⇒ 同一条路径,不是独立印证。当前真正站得住的、与线上/落库换算链路相互独立的佐证只有一条板端锚点:
S=112 实测 em.stat count = 23,132,恰等于 ⌊C/S⌋ = 23,132
(注意该锚点本身来自 em.bulk 脚手架与 .emf 通道两条路径,脚手架 ≠ 产品通道,勿混引)。
⇒ 在出现结构上独立的第二来源之前,本表不许用"交叉印证 / 独立印证"描述 N_sess。
⚠ 上文那个 S=112 板端锚点不能充当这"第二来源:它锚的是 C 与 ⌊C/S⌋(N_board 列),
与 N_sess 不在同一条公式链上,也不验证 2 MB 会话上限(它走 em.bulk、且是旧固件)——
拿它去印证 N_sess 就是跨量借锚点,属假绿;此处引用它只能用于 C 的口径;
先前流传的一句「S=112 → 20,971 与 main/include/emlite_bridge.h 的独立推算一致」,
经全树 grep 该文件内并无此值、该数值在全树亦零命中 ⇒ 该印证暂无出处,待核(2026-10-28 由提出方本人撤回,详见下方「口径自查留痕」)。
⑥ 点值式(U=4080)的定性 —— 自洽检查,不是独立印证:
⑥ 是从同一次观测解出的点值:设每页可用为 U,则 R = bytes − accepted×S = 16 + (P−1)×(4096 − U) ⇒
U = 4096 − (R−16)/(P−1)(代入 S=60 预测值得 U = 4080)。
用 P 做自洽检查(R/16 必须等于板端 rec_pages_used),不成立则模型失效。
这不是独立印证:区间式与点值式共用 bytes 与 accepted,⑥ 唯一的额外信息是 P,
因此它能抓算术错与数据不自洽,抓不到模型错——若"劈分吃满、页头 16"这个前提本身错(例如页尾弃置被忽略),两式会一起错且不报警。
⑥ 也依赖 W = 0 这个前提:把正确关系 R = 16P + W 代入 U = 4096 − (R−16)/(P−1),得
U = 4080 − W/(P−1) ⇒ 有弃置时它会把页尾丢弃误算成"每页 payload 变小":
S=44 会解出 U = 4080 − 1,688/634 ≈ 4077.34,S=644 会解出 ≈ 4078.67——一个数被压低,却不会有任何报警。
这正是"自洽检查抓不到模型错"的活样本:⑥ 只有一个观测量入口(bytes),页头开销与页尾弃置在它眼里是同一个东西。
另注意措辞边界:⑥ 直接测的是每页非记录开销 O = 4096 − U = 16 B/页,U = 4080 是由 4096 − O 推出来的,不是对着介质读出来的数。
来源标注(照抄前必读):
N_board / accepted / P / R / bytes 预测五列,
其绝对值依赖 C 的两个因子:rec_pages = 635(板端实证)与 4080 B/页(引擎常量推算、板端未验);
N_sess 依赖会话常量(frag.begin 的 EM_E_NOMEM 已实测锚定)。
算法乙为读码推定(不是实测:逐行复刻 mem_append_raw() 分支,未上板、未编译),与算法甲孰优由一次灌库测量裁决。
把 4080 由「推算」升为「板端实测」、并同时裁决两算法,靠编号 #59 的灌库测量(固定尺寸包灌满 → 读 em.stat 的 bytes / rec_pages_used),尚未执行。
在此之前,本表所有数值一律标推算/读码推定(不是实测),禁止当作实测引用。
⚠ 本条已撤回三次错误,留痕(不要照抄旧版本):
第一次(作废):曾写「分界点 ≈ 90.7 B/条」。错因:把「Dev-A 板容 23,132 条」当成尺寸无关常量去比「2 MB 装多少条」,两列用了不同的 s。
第二次(作废):曾写「2 MB 不过配,对所有记录尺寸成立」。错因:把"线上尺寸"与"落库尺寸"当成同一个量推导,于是 s 被约掉。两者差 12 B,约不掉。正确结论是上表的区间结论。
第三次(作废):2026-10-28,团队负责人提出含 slack 的修正式 R = 16 + (P−1)×(16+slack)
(slack = 4080 − floor(4080/s)×s),假设记录不跨页、页内零头被浪费;
经 frag-dev 引证 components/edgemem/emlite/em_mem.c:155-164(记录跨页劈两段写,零头被跨页记录吃掉 ⇒ slack ≡ 0)
后被撤回。正确式为 R = 16 × P。
第三次错误性质与前两次不同:前两次是算术/量纲错,这次是未读代码即凭想当然的物理模型发修正式。
工程教训:参数无关的错与参数相关的错
第三次那个含 slack 的修正式之所以必须留痕,不是因为它错,而是因为它错得隐蔽。它建立在一个想当然的物理模型上:
「记录不跨页 ⇒ 每页只能装 floor(4080/s) 条 ⇒ 页尾余零 slack = 4080 − floor(4080/s)×s 被浪费 ⇒ 每页实摊 16 + slack」。
引擎实际会把放不下的记录劈成两段跨页写,零头被下一条跨页记录吃掉,slack ≡ 0,修正式退化为 R = 16 × P。
关键事实:这个错在 s = 60 上完全隐形。
4080 mod 60 = 0 ⇒ slack = 0,错式与正确式在我们实际在用的一组参数上给出同一个答案
(两边都是 R = 10,160、P = 635),无论怎么对拍都对不出来;
它只在 s = 112 时才暴露(4080 mod 112 = 48 ≠ 0:在 P = 635 前提下错式给
R = 16 + 634×64 = 40,592,实际 10,160,差 4 倍;
若连 P 也按错模型重算——每页 36 条、ceil(23,128/36) = 643 页——则更远,为 41,104。
这类错最危险的不是它错,而是它在你正在用的参数上恰好对。
它若没被抓出来,会一直潜伏到某天有人拿它去判 S = 112 的一次测量,
把一个本来正确的测量判废——错误一旦被写成公式沉淀下来,杀伤半径就不再限于当初那个场景。
纪律 · 退化点证伪律(可执行):
若某公式在参数点 A 上恰好退化成正确式(本例中 4080 mod 60 = 0 ⇒ slack 项消失 ⇒ 错式与正确式在 S=60 上给出完全相同的答案),
则在 A 上通过不构成对该公式的验证。必须另选一个「不退化」的参数点 B 再做一次;只有 A、B 同时通过才算验证过。
选 B 的判据就是"使退化条件不成立"——不是随便挑两个 S 各跑一遍就算数,
B 必须由「让退化式失效」这一条选出来,写文档时要连同选 B 的理由一起写(否则这条纪律会被敷衍成走过场)。
本例:S=60 是退化点,S=112 是不退化点(4080 mod 112 = 48 ≠ 0,错式给 40,592、正确式给 10,160,差 4 倍)——这正是本次错误能在 112 上被抓出来、却在 60 上完全隐形的原因。
★ 同一条纪律,本轮就又抓到了一个:闭式 P = ⌈accepted·S/4080⌉。
这个式的隐性前提是「每页 payload 恒 4080」。它在 S = 48/60/64/112 上恰好成立(这四档的相位周期里不出现页尾弃置),
却在 S = 44 与 S = 644 上失效——那里 avail 会落到 4/8 B 而被丢弃(详上方 note 块)。
它在 S=60(我们正在用量、也是 #59 唯一要测的量)上完全隐形,换到 44 / 644 才露出来。
教训的二阶版本:一个公式"在你测的那个点上对",既可能因为它对,也可能因为那个点恰好让它退化。
⇒ 文档里任何公式必须标注它的失效条件(本式:S ∈ {44, 52, 56, …, 644} 共 59 个可达取值会触发页尾弃置),
读者才知道去哪些点上信它、去哪些点上别信;只标注成立点的公式,等于把一半信息藏起来。
⚠ 口径自查留痕(2026-10-28,团队负责人自查,留痕不删):
① 同一处被纠正两次,且第二次发生在"自己已经算对之后"(禁止美化为笔误):
曾对外写「43,176 × 60 = 2,590,560,/4080 = 635.0 正好整除」。
真相是 2,590,560 / 4080 = 634.9412,余 3,840,不整除;
真正整除的是未经批量回滚的 43,180 × 60 = 2,590,800 = 635 × 4080。
特别记录:提出方在发出该口径之前已自行用 Python 验算过这一行,输出即为 634.9411764705883,看到后仍照写"整除"。
算对了却不认账,比不会算严重得多——不要写成"笔误",第二次纠正的记录要跟原文留在一起。
(结论 P = 635 仍成立,靠的是向上取整,不是整除。)
② 一个不存在的印证被用来给一张表背书:
曾写「N_sess 得到 emlite_bridge.h 独立推算的交叉印证(20,971 / 43,690)」。
经核,该文件内没有这两个值,20,971 字面值在全树零命中;唯一的算式是 em_emf.py:587 的同一套 (2,097,152 − 32)/(S − 12) ⇒
同一条路径,不是独立印证。该说法系转述他人报告而未核实即当成独立证据外传,
而它恰恰被用来支撑"线上/落库换算链路是通的"这一结论。状态:该印证暂无出处,待核。
工程教训:阶次陷阱(2026-10-28,本轮真正的产出)
验证一个模型时,必须分清你验的是它的一阶结论还是高阶结构。 大量参数点上的一阶吻合,对高阶结构零证据力——因为高阶误差往往只在特定相位 / 累积条件下才显形。 判断一个公式够不够用,先问"我要的结论落在第几阶"。
为什么退化点证伪律装不下这件事:那一条管的是「错式在某个参数点上恰好退化成正确式」。
而本页这次不是退化——S=60 上闭式与逐页模拟本来就一致,它们在任何参数上一阶都一致。
失效发生在二阶:一阶命题「记录级零头不浪费」在全部 151 个可达 S 上全对;
二阶命题「每页 payload 恒 4080」在其中 59 个 S 上错。
⇒ 在一阶上验过 151 个点,一点都不能说明二阶是对的。换 1000 个参数点也一样。
完整教训链(两次栽在同一个地方:拿结构假设当公式用,而不问它在第几阶成立):
floor(4080/S) 条"——一阶就错,被 frag-dev 引 em_mem.c:155-164(劈分跨页写)否证(第三次撤回,见上)。avail 落到 4/8 B 而被丢弃。这一步由两次彼此独立的逐页复现(负责人与 spec-cap-table 各自照三条分支重写,1,688 B / 844 B 两处分毫不差)共同确认。落地要求:凡从"某个结构事实"推出来的公式,写进文档时必须同时标注它成立到第几阶、二阶失效条件是什么
(本式的失效条件:S ∈ [44,644] 步长 4 的 151 个可达值中有 59 个触发页尾弃置,含本表的 S=44 与 S=644)。
只标成立点不标失效条件的公式,等于把一半信息藏起来。
引用纪律(三条):
①「能开 2 MB 会话」不是「能灌进 2 MB」的证据,中间隔着一整个板容量;
② 凡出现条数,必须同时标出当时的 s——s=44 与 s=644 差 14.6 倍,脱离 s 的条数是无效数字;
③ C 的页数因子已板端实证、页内可用字节因子仍为推算;权威值需一次「清库 → 固定尺寸包灌满 → 清库」的独占时段实测(C ≈ accepted × s,s 固定)。在此之前 C 标推算,且必须注明是哪一半在推算;
④ 量 C 的不确定度粒度是 8 × s 而非连续量——分块导入按 EM_FRAG_BATCH=8 成批提交且整批回滚,故 accepted 必为 8 的倍数,末批残留最多浪费 8 × s;
⑤ 表述截止线时不得写「s ≥ 64 就量不出 C」——那句话将来会被一句「换 s=48 就量得出」直接证伪。正确写法是绑死几何:「在 Dev-A 8MB / 992 页几何下,单会话可灌上限为 s ≤ 60,C 的可测域是 s ∈ {44, 48, …, 60}」。
⑤ frag.list 的口径边界:只统计记录区存活记录,
不做墓碑回溯扣减。发生过 em_mem_del 的库,其 count / bytes
可能偏高,不能当净条目账本用——要对账请另走删除计数。
(当前三场景实测场景无删除,判据干净;此边界在引入删除后必须重新核对。)
edgehippom 分区(Dev-B 11.875 MB / Dev-A 3.875 MB) ┌────────────────────────────────────────────────────────┐ │ Superblock A 4 KB seq + 各区指针 + CRC32 │ 双份 A/B 交替写 │ Superblock B 4 KB │ 启动时取 seq 大且 CRC 有效者 ├────────────────────────────────────────────────────────┤ │ Region Table 4 KB 区域类型 / 偏移 / 长度 / 水位 │ 支持「分区内动态比例」(W1-16) ├────────────────────────────────────────────────────────┤ │ WAL 512 KB 环形,tx_id + 页号 + 旧值 + CRC32 │ 提交/回滚依据 ├────────────────────────────────────────────────────────┤ │ 记忆区 │ │ ├─ 记录区(append-only 页链,4 KB 页) │ 主体 │ └─ ANN 索引区(IVF 簇表 + graph 邻接) │ 与记录同事务提交 ├────────────────────────────────────────────────────────┤ │ 时序区 │ │ ├─ L1 列式聚合(每流每分钟一行) │ 预算 1.375 MB,待实测回收 │ └─ 取证区(异常原始窗口,L0 转存) │ 定长环形,满则覆盖最旧 ├────────────────────────────────────────────────────────┤ │ 空闲区(compact 可用) │ └────────────────────────────────────────────────────────┘
| 现有 | 去向 | 说明 |
|---|---|---|
em_store.c 的 append-only 记录日志 | 改造为记录区底座 | 已有 em_kb_add/del/get/clear/compact/count/bytes_used/next_id/foreach |
em_kb_search() | 保留为 v1 文本检索;v2 走 em_query | 无向量能力,是 .emf 分代的原因 |
em_kb_partition_{read,write,erase} | 收敛进 em_flash_io | PC 仿真实现替换即可上 PC 单测 |
em_crc32 / em_utf8_cut / store.map | 直接复用 | — |
kb_static 分区 | 并入 edgehippom,由 Region Table 管理 | 改分区表后原内容作废,改前先 eme kb export --backup |
store.map 同源输出(产品定规)store.map 必须同源输出以下全部信息,不得分散到多个命令:flash 分区占用 / KB 占用 / NVS / RAM / 账本 / 多模态快照。这是 Studio 与 eme 的唯一容量视图。
协议:单行 JSON {"id":n,"cmd":"name","args":{}}。串口 / USB CDC / BLE 共用同一套命令分发。
| 命令 | 用途 |
|---|---|
sys.ping sys.info sys.caps sys.bootloader sys.form sys.set_form sys.led sys.reboot sys.factory_reset | 设备信息与控制。sys.info 的 build_time 是验「是否上新板」的唯一依据。sys.form / sys.set_form 返回与设置形态标识(gamma-card / usb-dongle);旧名 sys.role / sys.set_role 保留为兼容别名(ADR-018) |
store.map | 容量同源视图(6.2 节) |
em.provision em.stats | 配置与统计 |
kb.img | KB 镜像导入状态 |
data.query dev.id dev.autoid | 数据查询与设备标识 |
link.scan link.connect link.status link.sync | EdgeMesh 链路(03 号分册) |
hub.discover hub.join hub.leave hub.set hub.state hub.status | EdgeMesh 中转(Hub)能力——协议层角色,与板级形态无关(ADR-018) |
mesh.query mesh.ledger | 语义改为碎片接力(ADR-011) |
ota.status ota.reboot ota.set_token | OTA。token emgw-ota-demo |
wifi.scan wifi.connect wifi.disconnect wifi.forget wifi.state wifi.status | Wi-Fi |
ble.init ble.adv ble.scan ble.conn ble.status ble.stop | BLE |
| 命令 | args | 返回 | 对应 HAL |
|---|---|---|---|
mem.put | {text, meta, prio, ttl_h, vec_b64?} | {id} | em_mem_put |
mem.get | {id} | {rec} | em_mem_get |
mem.del | {id} | {ok} | em_mem_del |
mem.query | {vec_b64?, k, filter, flags, min_score_q8?} | {hits:[{id,frag_id,ts_ms,score,matched_by,text,src}], reject}。n=0 时必带 reject(ADR-014) | em_query |
mem.stat | — | {count,bytes,frag_rate,wal} | em_stat |
ts.inject | {stream, ts_us, sample_b64} | {ok} | em_ts_append |
ts.script | {model, seed, dur_s} | {ok} | 合成数据源(第 9 章) |
ts.query | {stream, t0, t1} | {rows} | em_ts_window 【ADR-019】区间左闭右开 [t0, t1):含 t0 所属分钟、不含 t1 所属分钟;逐分钟查询请写 t1 = 下一分钟起点 |
ts.agg | {stream, t0, t1, op} | {value} | em_ts_agg 【ADR-019】与 ts.query 口径逐字一致的左闭右开 [t0, t1);min_score 类语义不适用,窗口为空返回 EM_OK + n = 0(非错误) |
ts.level | {from, to} | {ok} | em_ts_downsample |
frag.import | {chunk, off, total, crc} | {ok, off} | em_frag_import |
frag.export | {frag_id} | {chunk, off, total} | em_frag_export |
frag.list | — | {frags:[...]} | em_frag_list |
frag.verify | {frag_id} | {ok, bad_recs} | TC-F-* |
frag.del | {frag_id} | {ok} | — |
edgehippom.compact | — | {freed_bytes, ms} | 维护窗口 ≤ 30 s |
cam.*(11 个)、mic.*(7 个)、gps.*(2 个)、env.*(5 个)、photo.*、snap.*、ow.scan、sensor.*。
裁剪后 main/ 从 14 个 .c 减到约 11 个(另加 em_runtime.c / em_ts.c / em_usb.c)。实测裁剪仅省 ≈120 KB flash(esp32-camera 组件 83.7 KB + I2S 驱动 31.1 KB + 三个自有模块 8.4 KB),真正的大头是 Wi-Fi / BLE / mbedTLS,砍不掉。裁剪的真实收益是并发与内存峰值压力的下降,不是 flash。
cmd_console_write + esp_log_set_vprintf)。
fetch("http://192.168.1.46/api/...") 会被同源策略拦死。板端必须返回 Access-Control-Allow-Origin: *、Access-Control-Allow-Methods: GET,POST,OPTIONS、Access-Control-Allow-Headers: Content-Type, X-EM-Token,并对 OPTIONS 直接返回 204。| 端点 | 方法 | 说明 |
|---|---|---|
/ | GET | 控制台首页(h_index,HTML 约 24 KB) |
/api/status | GET | 设备状态 |
/api/config | GET/POST | 配置读写 |
/api/reboot | POST | 重启 |
/api/ota/status /api/ota/token | GET/POST | OTA 状态与 token(emgw-ota-demo) |
/ota | POST | 固件 OTA(只写 app 分区,不碰 edgehippom) |
/kbota | POST | .emf 镜像 OTA(?dry=1 预检) |
/kb/export | GET | 导出知识库 |
/api/kb/img | POST | KB 镜像导入 |
/api/mesh/{query,bid,request,result,award} | POST | 语义改为碎片接力(ADR-011,见 03 号分册) |
/snapshot.jpg | GET | MVP 移除(摄像头裁剪) |
/api/mic/{level,record} | GET/POST | MVP 移除 |
| 端点 | 方法 | 请求体 | 说明 |
|---|---|---|---|
/api/mem/query | POST | {vec_b64?, k, filter, flags, min_score_q8?} | 混合查询。返回 {hits:[{id,frag_id,ts_ms,score,matched_by,text,src}], reject} |
/api/mem/put | POST | {text, meta, prio, ttl_h, vec_b64?} | 单条写入 |
/api/mem/del | POST | {id} | — |
/api/mem/stat | GET | — | 条数 / 字节 / 碎片率 / WAL |
/api/frag/import | POST | 二进制 .emf(4 KB 分块 + CRC32 + 断点续传) | 铁律:失败路径不得破坏已有库 |
/api/frag/list | GET | — | 碎片清单 |
/api/frag/export | GET | ?frag_id= | 导出碎片 |
/api/frag/verify | POST | {frag_id} | 自检(对应打进包里的 20 条验收用例) |
/api/frag/del | POST | {frag_id} | — |
/api/ts/query | GET | ?stream=&t0=&t1= | 时序窗口 |
/api/ts/agg | GET | ?stream=&t0=&t1=&op= | 聚合 |
/api/ts/inject | POST | {stream, ts_us, sample} | 注入(含合成源触发) |
/api/store/map | GET | — | 同源容量视图(6.2 节) |
/api/edgehippom/stat | GET | — | 引擎层指标:malloc 计数、abort P50/P99、索引参数 |
/api/mesh/sync | POST | 见 03 号分册 | 碎片同步 |
send;② httpd 的 open_fn 中设置 TCP_NODELAY。
| 步 | 环节 | 产物 | 算力来源 | 落点 |
|---|---|---|---|---|
| 1 | 采集 | 多速率原始样本 | 板内(合成源) | L0 PSRAM 环 |
| 2 | 窗口特征化 | 时域 + 频域特征(RMS / 峭度 / 包络谱峰值) | 板上 | L1 分钟聚合 |
| 3 | 特征向量化 | 特征 → 向量 | 板上(确定性变换,非模型) | → 检索 |
| 4 | ANN 检索 + 混合过滤 | top-k 及分数 | 板上 | — |
| 5 | 回写 | 新 Pattern 入库(或合并到已有) | 板上 | L2 记忆区 |
MVP 不接任何传感器。时序数据走板内确定性合成源:CWRU 风格参数化模型——基频 + 谐波 + BPFO/BPFI/BSF 冲击串 + 噪声,seed 可控、逐字节可复现。
| 参数 | 取值 | 说明 |
|---|---|---|
| 采样率 | 4 kHz(振动 3 轴) | 另有 1 Hz 温度、10 Hz 电流 |
| 基频 / 谐波 | 可配 | 模拟转频及其倍频 |
| 故障特征频率 | BPFO / BPFI / BSF | 对应外圈 / 内圈 / 滚动体故障 |
| 噪声 | 高斯,σ 可配 | — |
| seed | u32 | 同 seed 必须逐字节可复现,否则测试不可信 |
| 注入接口 | ts.script {model, seed, dur_s} | — |
frag.export 中可见,可被接力给另一块板并命中# PowerShell —— 每次开新终端都要设 $env:IDF_PATH = "C:\Espressif\v6.1\esp-idf" $env:IDF_PYTHON_ENV_PATH = "C:\Espressif\...\idf6.1_py3.12_env" $env:IDF_PYTHON_CHECK_CONSTRAINTS = "0" # 并把工具链加入 PATH # 构建唯一入口 .\build_board.ps1 -Board xiao|devkit|all [-Flash] [-Port COMx] # 日常增量 ./inc_build.sh # 板位与形态的对应(由 boards/sdkconfig.<board_id> 固化,见 §1.2): # xiao → CONFIG_EMGW_FORM_GAMMA_CARD (运行时 form = gamma-card) # devkit→ CONFIG_EMGW_FORM_USB_DONGLE (运行时 form = usb-dongle)
main/version_stamp.cmake,勿手改 emgw_version.h)。例 v1.20261015-213249sys.info 的 build_time,不要靠 idf.py build 的退出码(idf.py build | while read 会吞退出码)flash-id(不是 v4 的 flash-info)| 序 | 方式 | 适用 | 说明 |
|---|---|---|---|
| 1 | OTA(POST /ota) | 日常固件升级 | token emgw-ota-demo。双槽 + rollback + 启动自证。只写 app 分区,不碰 edgehippom |
| 2 | KB-OTA(POST /kbota) | 知识库更新 | ?dry=1 预检;改前必 --backup |
| 3 | USB MSC | 离线灌库 / β 形态主路径 | 拖 .emf 进虚拟 U 盘 |
| 4 | USB CDC(eme / Studio) | 在线灌库与调试 | 行协议 |
| 5 | 串口刷写(esptool / esptool-js) | 首刷与救砖 | 唯一救砖通道,不可移除 |
| 现象 | 先查什么 |
|---|---|
| 周期自发重启 | 栈溢出:NimBLE host ≥ 8192;hub_task 6144 内不得放大缓冲;mic_mgr 类 char[4600] 改堆分配 |
| tlsf assert / double free | esp_partition_next() 末尾自 release,调用方不可再 release |
| WS2812 不亮 / 颜色错 | IDF v6.1 的 rmt_transmit() encoder 不可为 NULL(用 rmt_copy_encoder);config 必须 memset 不可 {0};WS2812 是 GRB 序 10 MHz |
| HTTP 响应慢(10 fps → 0.33 fps) | Nagle + 延迟 ACK:合并一次 send + open_fn 设 TCP_NODELAY |
| 串口日志串行 / 半行 | 多任务共串口须单锁整行原子写 |
| 中文乱码 | JSON 缓冲 ≥ 768 B |
nvs_stats_t 字段报错 | IDF v6 字段名为 used_entries / free_entries |
| JS 参数名遮蔽模块变量 | 例 protocol.js 中 connect(baud, port) → 改名 extPort;fire-and-forget async 须自带 try/catch |
| 引擎未就绪,日志却只有一句常量 | 取的是 err(常量)不是 detail(真因)——err or detail 的短路写法会把唯一有诊断价值的字段吞掉。详见下方 ★ |
★ 判据失败必须带出可归因的字段(本仓既有正确做法的归纳,不是新立的规矩)
em.stat 在引擎未就绪时返回【读码推定 · fw_ascii/main/cli_json.c:2362-2366】:
{"ok":0, "err":"emlite 未就绪", "detail": emlite_last_error()}
· :2364 err = 常量字符串;· :2365 detail = emlite_last_error() = 真正的失败原因
⇒ 取 err or detail 的短路写法会静默丢掉 :2365 这个唯一有诊断价值的字段——
日志里只剩一句常量,零可归因信息。
⚠ 两个命令的 err 文案不同,引用必须钉命令名:
em.stat = 「emlite 未就绪」(:2364);em.query = 「emlite 引擎未就绪」(:1544);
其余命令走信封层 resp_err(id, "emlite 引擎未就绪")。
只写"emlite 未就绪"四个字会被后人张冠李戴——这与 check_anchors() / check_real_board()
两个"锚点"同名不同物,是同一类毛病(两个名字差几个字,结论就串了)。
核对用 grep -n "未就绪" fw_ascii/main/cli_json.c(行号会漂移,以 grep 为准)。
⚠ 顺带一条"取层"坑:em.stat 的失败写在 data 里(ok:0),
外层信封走 resp_ok_raw(:2383)⇒ 信封层 ok 恒为 true,
拿 r["ok"] 判 em.stat 是永远假绿;其余命令走 resp_err、失败在信封层。
两者取层不同,别混用。
本仓既有正确做法(3 处):em_powercut_l3.py:1681/1795 用 detail 优先、
:1377 两者都留;_fid_empty_probe.py:262 / _fid_hdr_probe.py:307 同时打印两者。
⇒ 我们是把不一致的 2 处对齐到这既有的 3 处,不是发明新要求。
曾不一致的 2 处:emb.py 的 wait_ready、与 _boot_ready_probe.py(G7 专用启动就绪探针)
—— 均已修(2026-10-28,team-lead 自修):一律改 detail 优先,
后者另加 flush 与"每轮就绪上限"参数。
行号会漂移,以 grep -n 'get("detail")' emb.py _boot_ready_probe.py 为准。