收束:把全栈画成一张图,把方法论带走
本节你将学到
- 把 light-meter 这条从光子到屏幕像素的完整数据链路画成一张图,每个环节你能说出对应哪一章
- 回顾这一路踩到的那些工程判断,哪些是真值得记住的
- light-meter 这套 Sensor 契约 + Mock + 真机的架构,怎么扩成多传感器,衔接到更大的项目
前置知识
- 全系列第 01 到 10 章,这是收束章,不引入新代码
画一张图:从光子到屏幕像素
走完前面十章,你现在手里有了一整个能在板子上跑的产品。这一章我们退一步,把它的数据链路完整画出来,每个环节标上对应哪一章。说实话,这种"退一步看全貌"的事我前面一直在忍,十章都在低头赶路,这会儿才有底气把链路端到端摊开。这张图你能在一张纸上徒手画出来,就说明 light-meter 这套东西真吃透了。反过来,某个箭头你讲不出来,那就回去把对应那章再看一眼。
展开代码 (共 53 行)收起代码
物理世界
│ 环境光光子 / 手指接近
▼
┌───────────────┐
│ AP3216C 芯片 │ (硬件, Lite-On 三合一)
└───────────────┘
│ I2C1 总线
▼
┌───────────────┐
│ 内核驱动 │ driver/08 AP3216C I2C 驱动
│ ap3216c.ko │ (copy_to_user {ir,als,ps})
└───────────────┘
│ 暴露为字符设备节点
▼
/dev/ap3216c
│ POSIX open/read ← Ch.09
▼
┌───────────────┐
│ Ap3216cSensor │ Ch.09 真机后端
│ (用户态客户端) │ (als*lux_coeff → lux)
└───────────────┘
│ 实现 Sensor 契约 ← Ch.03
┌───────────────┴───────────────┐
│ unique_ptr<Sensor> │
│ (Ch.07 一行 CMake 在此切换) │
│ ▼ │
│ ┌─────────────────┐ │
│ │ MockedSensor │ Ch.04 桌面后端
│ │ (桌面,无硬件) │ (正弦 lux + 二值 ps)
│ └─────────────────┘ │
└───────────────┬─────────────────┘
│ query_once() 拉数据 ← Ch.05 onSampleTick
▼
┌───────────────┐
│ MainWindow │ Ch.05 三态状态机 + 定时器
│ (运行/告警/息屏) │ (processSample 分发数据)
└───────────────┘
┌───────┴───────┐
▼ ▼
┌──────────┐ ┌──────────────┐
│ ChartView │ │ 数字卡/CSV/ │
│ Ch.06 自绘 │ │ 息屏遮罩 │
│ (真环形) │ │ │
└──────────┘ └──────────────┘
│ QPainter
▼
┌──────────────┐
│ linuxfb QPA │ Ch.10 上板部署
│ /dev/fb0 │ (QT_QPA_PLATFORM)
└──────────────┘
│ blit 像素到显存
▼
LCD 屏幕从最上面的环境光光子打到 AP3216C 芯片,一路经过内核驱动、字符设备、用户态客户端、Sensor 抽象、状态机、自绘控件、linuxfb,最后变成屏幕上变化的像素,这条链路每一个环节你都能指认它对应哪一章、哪份代码。中间那个 unique_ptr<Sensor> 的分叉,就是第 07 章那"一行 CMake 开关"切换的地方,Mock 走左边、真机走右边,UI 那一侧完全一样。我们回头看,这整张图里真正难画的不是哪个节点,是节点之间的那根线。光知道"这里有个字符设备"没用,得知道它怎么跟用户态、跟 Sensor 抽象咬合上。
回头看这一路学到的
把图摊开后,顺着这条链路回顾一下,这一路哪些东西是值得记住的。下面这些不是 light-meter 这个产品特有的细节,换个项目它们还是成立,所以才值得专门拎出来念叨。
契约先行。第 03 章我们花一整章钉死 Sensor 抽象,UI 只跟基类指针打交道。这事当初写的时候觉得啰嗦,回报在第 07 章才显出来:翻一个 CMake 开关就能换后端。再往后想加新传感器,UI 一行不用改。先把接口钉死、再写任何实现,这套做法在有多后端、要测试的系统里基本是底层常识,只是很多人偷懒不这么做,真到要换实现的时候才补,代价大得多。
Mock-first。第 04 章那个假数据后端,让你在桌面把整条 UI 调到完美,不卡硬件、不烧镜像。说实话在硬件不到位、硬件贵、或者烧一次镜像要等十分钟的场景里,这种"先用假数据把上层调通"的做法就是救命稻草,嵌入式尤其。我们这趟没怎么被硬件阻塞,一半功劳在第 04 章那张 Mock 牌上。
编译期后端切换。第 07 章那个 option 加 #ifdef 的接缝,把"桌面开发态"和"板子部署态"用编译期开关隔开,host 和 target 各自最精简,运行时不背多余的依赖。同一套代码、多种部署形态(比如带模拟器的桌面版加嵌入式版)基本都可以套这个模式,看你怎么把接缝藏在编译期。
显式状态机。第 05 章那三个布尔加转移逻辑,看着土,但把运行、告警、息屏三态的转移条件写在了明面上,特别是 resetIdleCountdown 那个"定时器在跑就别重启"的细节,这种东西藏在隐式逻辑里调试时血压拉满。状态少的时候手搓清楚,状态多了再上框架。说实话这个取舍意识比死记 QStateMachine 的 API 有用得多,后者你忘了查文档就行,前者忘了你会写出一坨连自己都读不懂的 if-else。
无 GPU 自绘。第 06 章自己用 QPainter 画折线,不是因为 Qt Charts 跑不了,是因为对这个需求自绘更轻、更可控、更稳。库存在不等于该用,以及把高频刷新区域切成小控件,这两条在资源受限设备上反复会出现,记住省事。
驱动 ↔ 应用协议对齐。第 09 章那个 {ir,als,ps} 的顺序,是跨内核/用户态边界的二进制契约。两端必须共享同一份字节布局,这种意识你写对接自研驱动的应用、或者任何跨进程二进制接口时,都得刻在脑子里,字节顺序错一个就是 segfault 伺候。
按环境标定。第 08 章那个 lux_coeff,把传感器 raw 计数换成物理量,默认值只是占位,真值得按你那块板子所在的环境标。涉及物理量换算的产品,温湿度、气压、距离、电流,标定这一步谁也逃不掉。
最后还有两个横跨全程的 C++ 技巧,std::expected 的错误处理(第 02 章)、unique_ptr 配自定义 deleter 的轻量 pImpl(第 04 章)。这俩是现代 C++ 工程的高频件,你在 light-meter 里见过它们在真实代码里怎么落地,以后自己写 C++ 项目能直接抄走。
加第二颗传感器:契约层的可扩展性
light-meter 是一颗 AP3216C 单传感器,但你这套架构不是只能撑一颗。接下来问题来了,要加第二颗传感器(比如一颗温湿度 AHT10)得动哪些地方?我们顺手走一遍。你会发现,得益于契约先行,改动是局部的,不会牵一发动全身。
数据契约这一层有两个选择。要么给 SensorData 加温湿度两个字段,简单粗暴,但所有后端都得跟着填;要么把"传感器"这个概念再抽象一层,做成一个传感器注册表,每颗传感器独立一个契约,更灵活但要重构。light-meter 现在的规模,前者够用,等真的塞到四五颗再考虑后者不迟。
再往下是两个新后端。新增一个 Mock 后端,照着第 04 章的 MockedSensor 写个 MockedAht10,产假温湿度数据,桌面调试用。新增一个真机后端,照着第 09 章的 Ap3216cSensor 写个 Aht10Sensor,通过对应的字符设备节点(这里先验证一下,你得先有 AHT10 的驱动,那是 driver 系列另一个故事)读温湿度。它同样实现 Sensor 契约,接进来 UI 不用改。
UI 那一侧,ChartView 加一条温湿度折线(或者干脆新开一个显示区),状态机基本不动,因为它管的是 light-meter 自己的三态,温湿度只是多一路显示数据。
加一颗传感器的改动,集中在"加一个新后端 + UI 加一路显示"这两处。UI 的状态机骨架、Sensor 契约本身、自绘控件、部署这一整套基础设施,全都纹丝不动。真正的坑在后面:如果你想得太美,把这事儿当乘法做(每加一颗传感器就动一遍 UI 状态机),那架构就白搭了。契约先行的全部价值,就是让这件事变成加法而不是乘法。
下一站:PROJ-001 环境监测站
light-meter 这一套走到头,你已经掌握了 imx-forge 那个旗舰项目 PROJ-001 便携式环境监测站的大约八成技术底座。PROJ-001 在 document/todo/projects/proj-001-env-monitor.md 里有详细蓝图,它要做的事,本质就是 light-meter 的多路扩展再加上一层网络。
传感器层,PROJ-001 要把 AP3216C 这一课扩展到温湿度、气压、陀螺仪多颗传感器,每颗都走"驱动 + 字符设备 + Sensor 用户态客户端"这条你已经走通的路。这一路你刚在 light-meter 里练过一遍,PROJ-001 就是多练几遍,换个 I2C 地址、换份寄存器表而已。
应用层,Qt 的界面从单页折线扩展到多页面、多通道图表,Qt 的骨架还是你第 05、06 章学的那些:信号槽、QPainter、布局,只是控件更多、布局更密。
网络层是 PROJ-001 比 light-meter 多出来的部分,MQTT 把数据推到云端、Web 看板实时同步显示。这是 light-meter 整个系列没碰过的东西,你到了 PROJ-001 会新学一轮。
所以从 light-meter 到 PROJ-001,不是推倒重来,是"加几颗传感器后端、加几页 UI、加一层网络"。light-meter 这套 Sensor 契约加 Mock 加真机加自绘加部署的底座,PROJ-001 直接复用。这也是为什么我们花这么大力气把 light-meter 的方法论讲透——它不是个孤立的小摆件,它是通往旗舰项目的脚手架。
收尾
到这里,light-meter 这套教程就收工了。你从一个空的 CMakeLists.txt 开始,一路走到一个在 i.MX6ULL 板子上常驻运行、读真实环境光、会告警、会息屏、能导出 CSV 的产品,中间把 CMake、现代 C++、Qt、Mock 思想、驱动协议、部署标定都过了一遍。代码量不大,但每一处都是有讲究的,讲究的就是前面回顾的那套方法论。
完结撒花。给板子拍张照不过分,屏幕上亮着你自己造出来的 light-meter 界面。然后带着这一路踩出来的判断,去造下一个。