Skip to content

收束:把全栈画成一张图,把方法论带走

本节你将学到

  • 把 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 界面。然后带着这一路踩出来的判断,去造下一个。

继续学习

Built with VitePress