☰
29 嵌入式操作系统 | 网关维护状态表 + 每 2 秒上报(含设备在线判定)
2026/10/2 7:12:22 网站建设 项目流程

嵌入式操作系统 | 网关维护状态表 + 每 2 秒上报(含设备在线判定)

本课程开源地址(Gitee):https://gitee.com/fujianxinxi/qianrushixitongyingyongkaifa.git
课件、示例代码与验收脚本都在该仓库,可直接git clone或下载 ZIP 使用。

模块七 · 2 学时 · 环境:Ubuntu(gcc + libubox)+ socat 虚拟串口

一句话概述:网关不再"收到就转发",而是把设备状态记进一张表,然后每 2 秒自己组一帧0x81(帧尾多一个"在线"字节)UDP 上报给固定目标;设备超过 3 秒没上报就标成离线。下课时你能看到:灯 亮 | 风扇 2 档 | 25.0 ℃ | 在线→ 关掉设备 →离线→ 设备回来 →在线。

本节目标

  1. 会定义"设备状态表"结构体(灯/风扇/温度/最后收到时间/在线标志),并在收到帧时更新它
  2. 会用uloop_timeout做周期上报(回调里再排一次),会自己组帧(build_frame+ CRC16)
  3. 会做在线判定(用单调时间比较),并说清"为什么要把上报目标固定下来"(对比课时 27 的坑)
  4. 能复现全过程:上线 → 掉线 → 恢复

一、讲解内容

1.1 知识讲解

本节在课时 28 的基础上加什么。上一节的网关只是"把设备帧广播出去";这一节它开始记住设备的当前状态,并主动、定期地对外汇报:

设备面板 ──0x80 状态帧(每秒)──▶ 网关 ├── ① 更新"设备状态表"(灯/风扇/温度/最后收到时间) ├── ② 广播给已连接的 TCP 客户端(课时 28 已有) └── ③ 每 2 秒自己组一帧 0x81 ──UDP──▶ 监听端/界面/上位机 (表里的值 + online 标志)

为什么要"状态表"?三个现实原因,学生要能说出来:

原因说明
查询要快上位机问"现在什么状态",网关直接读表回答,不用现去问设备(设备可能正忙/掉线)
掉线要能判断表里记了"最后收到的时间",一比较就知道设备是不是失联了;掉线后最后已知值还能显示
上报要解耦上报节奏(2 秒)和设备的节奏(1 秒)不必绑定;设备换了、上报目标变了都不用动设备

帧格式加了一版(v1.1):网关对外发的是0x81,比设备的0x80多一个"在线"字节:

方向TYPELENPAYLOAD
设备 → 网关0x804灯(1) 风扇(1) 温度×10(2 大端)
网关 → 上位机0x815灯(1) 风扇(1) 温度×10(2 大端)在线(1)

在线判定怎么做?关键是用单调时间(CLOCK_MONOTONIC),不是墙上时钟(date那种会被手动/校时改动):

staticlongnow_ms(void)/* 单调时间的毫秒数 */{structtimespects;clock_gettime(CLOCK_MONOTONIC,&ts);return(long)ts.tv_sec*1000+(long)(ts.tv_nsec/1000000);}/* 收到设备状态帧时: */state.last_seen_ms=now_ms();/* 记下"最后一次见到" */state.online=1;/* 每 2 秒上报时先判一次: */if(state.online&&now_ms()-state.last_seen_ms>OFFLINE_MS){state.online=0;/* 3 秒没消息 → 掉线 */}

阈值怎么定?要比设备的上报周期宽 2~3 倍:设备 1 秒一帧 → 3 秒判掉线(本课)。太紧会"抖动"(设备只是慢了半拍就报掉线),太松则掉线发现慢。

"上报目标固定"是本节特意改的设计(对比课时 27):

课时 27(UDP 收命令)本节
上报发给谁“最近给我发过包的那个人”(peer)固定的127.0.0.1:9100(用./gateway_full /tmp/ttyV0 9000 127.0.0.1 9100指定)
风险任何程序发个包就把地址顶掉,真正的上位机收不到不受"谁说话"影响;目标可以写死、也可以(课时 31)从 uci 读
适合快速验证真实部署

1.2 操作步骤(一条命令一步,看现象)

第 0 步:环境检查(gcc、libubox、socat、pyserial;完整清单见课时 24 附录 F)

gcc--version|head-1ls/usr/local/include/libubox/uloop.hwhichsocat python3-c"import serial; print('pyserial OK')"

第 1 步:编译(本节的网关是从课时 28 复制过来改的)

cd~/lab/课时29-网关状态表与上报ls# 应看到 gateway_full.c、state_listener.pygcc-Wall-Wextra-ogateway_full gateway_full.c-lubox

现象(实测):[OK] gateway_full 编译无警告。

第 2 步:四个终端准备好(A 串口 / B 设备面板 / C 网关 / D 监听端)

# 终端 A:虚拟串口socat-d-dpty,raw,echo=0,link=/tmp/ttyV0 pty,raw,echo=0,link=/tmp/ttyV1# 终端 B:设备面板(课时 26 的程序)cd~/lab/课时26-设备面板接串口&&python3 device_panel_serial.py# 终端 C:网关(参数:串口 / TCP端口 / 上报目标IP / 上报端口)cd~/lab/课时29-网关状态表与上报 ./gateway_full /tmp/ttyV09000127.0.0.19100# 终端 D:监听上报(只看,不发命令)python3-ustate_listener.py9100

网关启动的现象(实测):

网关已启动:串口 /tmp/ttyV0 @ 9600 ⇄ TCP 0.0.0.0:9000(Ctrl+C 退出) 上位机发命令:先发 2 字节长度(大端),再发那一帧 设备状态:① 广播给已连接的 TCP 客户端;② 每 2000 毫秒 UDP 上报到 127.0.0.1:9100

终端 D 的现象(网关每 2 秒上报一次,注意最后那个"在线"):

监听 UDP 9100,等网关每 2 秒上报一次(Ctrl+C 退出) 收到 55 AA 81 05 00 00 00 F1 01 0E 0D → 灯 灭 | 风扇 0 档 | 24.1 ℃ | 在线 收到 55 AA 81 05 00 00 00 F3 01 0F 6D → 灯 灭 | 风扇 0 档 | 24.3 ℃ | 在线

帧里的81 05= TYPE 0x81、LEN 5,最后那个字节01就是"在线"。

第 3 步:用 TCP 命令控制设备,看上报跟着变(课时 28 的客户端就能用)

cd~/lab/课时29-网关状态表与上报 python3-u../课时28-网关TCP命令通道/tcp_client.py led on python3-u../课时28-网关TCP命令通道/tcp_client.py fan2

现象(实测,终端 D):

收到 55 AA 81 05 01 02 00 F2 01 32 85 → 灯 亮 | 风扇 2 档 | 24.2 ℃ | 在线 收到 55 AA 81 05 01 02 00 F4 01 31 25 → 灯 亮 | 风扇 2 档 | 24.4 ℃ | 在线

命令走 TCP(时长 28),状态走 UDP 上报(本节)——控制与数据分开,这也是真实工控系统的常见做法。

第 4 步:★掉线实验——把设备"拔掉"

pkill-f'device_panel_seria[l]'# 杀掉设备面板(相当于把设备断电/拔线)

等 5~6 秒,终端 D 的现象(实测,先保持"最后已知值",再把在线位变成 00):

收到 55 AA 81 05 01 02 00 F3 01 33 15 → 灯 亮 | 风扇 2 档 | 24.3 ℃ | 在线 收到 55 AA 81 05 01 02 00 F1 01 32 75 → 灯 亮 | 风扇 2 档 | 24.1 ℃ | 在线 收到 55 AA 81 05 01 02 00 F1 00 F3 B5 → 灯 亮 | 风扇 2 档 | 24.1 ℃ | 离线 ← online 变 0

网关那边同时打印(实测):

★ 设备掉线了(超过 3000 毫秒没收到状态帧) 网关上报状态(0x81)(11 字节):55 AA 81 05 01 02 00 F1 00 F3 B5 → 灯 亮 | 风扇 2 档 | 24.1 ℃ | 离线(第 7 次上报)

第 5 步:★设备回来了

cd~/lab/课时26-设备面板接串口 python3 device_panel_serial.py

现象(实测,几秒内自动恢复"在线"):

★ 设备上线了(灯 灭,风扇 0 档,24.9 ℃) ← 网关日志 收到 55 AA 81 05 00 00 00 F7 01 0D AD → 灯 灭 | 风扇 0 档 | 24.7 ℃ | 在线

第 6 步:停网关看统计

Ctrl+C停网关(终端 C),现象(实测):

网关退出。统计:串口收 18 帧、TCP 广播 4 次;TCP 收 2 帧、转给串口 2 帧;状态上报 11 次

三个数字对照着看:18 帧是从设备收到的、11 次是每 2 秒上报的次数、2 帧是客户端下发的命令。

第 7 步:收工。依次 Ctrl+C:监听端 → 网关 → 设备面板 → socat。

1.3 常见错误与排查

现象原因解决
编译报undefined reference to 'clock_gettime'老环境需要链接 rt加-lrt(Ubuntu 20.04 一般不需要)
监听端一直没输出① 网关没起来 ② 上报端口不对 ③ 被防火墙挡看网关日志有没有"网关上报状态(0x81)";确认监听端端口与网关第 4 个参数一致
监听端报Address already in use9100 被别的程序占着ss -lunp | grep 9100找出来杀掉,或两边都换端口(网关第 4 参数 + 监听端口)
状态一直是"离线"设备面板没跑 / 串口不对确认设备面板用/tmp/ttyV1、网关用/tmp/ttyV0;看设备面板日志有没有"发送 0x80"
掉线实验里"杀不掉设备"kill的是xvfb-run,里面 python 还活着用pkill -f 'device_panel_seria[l]'(按进程名杀);用pgrep -af device_panel确认
在线/离线来回跳判定阈值太小(比设备上报周期还短)阈值至少是设备上报周期的 2~3 倍(本课:设备 1 秒一帧 → 3 秒判掉线)
温度不变化设备面板停了(离线时表里是最后已知值)这是设计如此:离线后保留最后值,界面上要显示"离线+最后值"
上报帧 CRC 不对组帧时 CRC 覆盖范围写错范围是TYPE + LEN + PAYLOAD(不含 55 AA,也不含 CRC 本身)

二、学生练习

本节命令速查表:

命令作用示例
gcc -Wall -Wextra -o gateway_full gateway_full.c -lubox编译本节网关—
./gateway_full /tmp/ttyV0 9000 127.0.0.1 9100启动(串口/TCP端口/上报IP/上报端口)4 个参数都能省,用默认值
python3 -u state_listener.py 9100监听上报(看 在线/离线)Ctrl+C 退出
python3 -u ../课时28-网关TCP命令通道/tcp_client.py led on用 TCP 命令控制设备复用课时 28 的工具
pkill -f 'device_panel_seria[l]'模拟"设备掉线"看上报变 离线
ss -lunp | grep 9100看谁占着上报端口排查端口冲突
编号任务提示预期结果
L29.1编译 + 四个终端起来第 1、2 步监听端每 2 秒一行 `…
L29.2用 TCP 命令改状态第 3 步上报里的灯/风扇跟着变
L29.3★掉线实验第 4 步3 秒后 online 变 0,网关打印"设备掉线了"
L29.4设备恢复第 5 步网关打印"设备上线了",上报恢复"在线"
L29.5停网关看统计第 6 步三个数字能对上(收帧/上报/命令)
L29.6把OFFLINE_MS改成 1000 再试改#define OFFLINE_MS 1000掉线判得很快,但正常时也可能"误判"(体会阈值取舍)
L29.7把上报周期改成 500 毫秒改#define REPORT_MS 500监听端每 0.5 秒一行(网络和 CPU 占用也会上升)
L29.8让上报目标改成宿主机 IP第 4 个参数填宿主机 IP(192.168.31.x)宿主机上用 Python/工具收到上报(体会"上报目标可配置")

三、作业

  • 必做:交 3 张截图(监听端"在线"的一行 / 掉线后"离线"的一行 + 网关"设备掉线了" / 恢复后"在线"),并回答:① 网关为什么要维护"设备状态表"?不维护行不行?② 在线判定的阈值应该怎么定?定太小/太大分别会怎样?③ 为什么要用CLOCK_MONOTONIC(单调时间)而不是date那种时间?④ 本节把上报目标改成了"固定地址",比课时 27 的"谁说话听谁的"好在哪?
  • 选做:① 在0x81帧里再加一个"最后更新时间"(比如"距离上次收到设备帧多少秒",1 字节),监听端也翻译它;② 给网关加一条"设备连续离线超过 10 秒就打印一条告警"的日志(提示:在send_state_report里加个计数器)
  • 提交:截图 + 文字回答,命名课时29-作业截图.png

四、本节小结与下节预告

本节网关"长脑子"了:一张设备状态表(灯/风扇/温度/最后收到时间/在线)、每 2 秒自己组帧0x81上报(含在线字节)、超过 3 秒就判掉线,并且把上报目标固定下来(不再受"谁说话"影响)。你还亲手做了"在线 → 掉线 → 恢复"的全过程实验。

下节课(课时 30)做上位机界面:用 Flask 起一个网页,页面上显示灯/风扇/温度/在线(收网关的 UDP 上报),并放控制按钮(走 TCP 发命令)——做完之后,用鼠标就能控制那个"串口设备",而且在 Windows 宿主机浏览器里也能操作。


附录:怎么运行 + 完整代码

两个文件在代码/课时29-网关状态表与上报/:gateway_full.c(网关,从课时 28 复制后加功能)和state_listener.py(上报监听端)。完整代码以仓库文件为准,下面摘本节新增的代码。

附录 A|环境准备 + 怎么运行

# ===== ⓿ 环境准备(缺哪个装哪个;完整清单见课时 24 附录 F)=====sudoaptupdatesudoaptinstall-ybuild-essential cmake pkg-config socatsudoaptinstall-ypython3-pyqt5 python3-serial xvfb# ===== 终端 A:虚拟串口 =====socat-d-dpty,raw,echo=0,link=/tmp/ttyV0 pty,raw,echo=0,link=/tmp/ttyV1# ===== 终端 B:设备面板 =====cd~/lab/课时26-设备面板接串口&&python3 device_panel_serial.py# ===== 终端 C:本节网关(每 2 秒上报)=====cd~/lab/课时29-网关状态表与上报 gcc-Wall-Wextra-ogateway_full gateway_full.c-lubox./gateway_full /tmp/ttyV09000127.0.0.19100# ===== 终端 D:监听上报 =====python3-ustate_listener.py9100# ===== 掉线 / 恢复实验 =====pkill-f'device_panel_seria[l]'# 掉线:上报变"离线"cd~/lab/课时26-设备面板接串口&&python3 device_panel_serial.py# 恢复:"在线"

附录 B|本节新增的代码(从课时 28 复制后加的部分)

B-1 状态表 + 单调时间(文件开头)

/* ===== 课时 29 新增的常量 ===== */#defineT_STATUS0x80/* 设备 → 网关:状态上报(灯 / 风扇 / 温度) */#defineT_STATE_REPORT0x81/* 网关 → 上位机:网关状态(多一个"在线"字节) */#defineREPORT_MS2000/* 每 2 秒上报一次 */#defineOFFLINE_MS3000/* 超过 3 秒没收到设备状态帧 → 判定离线 */#defineSTATE_PORT_DEF9100/* 上报目标端口(课时 31 会改成从 uci 读) */#defineSTATE_HOST_DEF"127.0.0.1"/* ===== 课时 29 新增:设备状态表 + 上报定时器 ===== */structdevice_state{intled;/* 灯:0 灭 / 1 亮 */intfan;/* 风扇:0~3 档 */inttemp_x10;/* 温度 ×10(250 = 25.0 ℃) */longlast_seen_ms;/* 最近一次收到设备状态帧的时间(毫秒) */intonline;/* 1 在线 / 0 离线 */};staticstructdevice_statestate={0,0,250,0,0};/* 初值:灯灭、风扇停、25.0℃、离线 */staticstructuloop_timeoutt_report;/* 周期上报定时器 */staticintudp_fd=-1;/* 只用来发上报(sendto) */staticstructsockaddr_instate_peer;/* 上报目标(固定) */staticintreport_cnt;/* 上报了多少次 *//* 单调时间的毫秒数(用 clock_gettime,不受系统时间被改的影响) */staticlongnow_ms(void){structtimespects;clock_gettime(CLOCK_MONOTONIC,&ts);return(long)ts.tv_sec*1000+(long)(ts.tv_nsec/1000000);}

B-2 收到设备状态帧 → 记进状态表(顺便判"上线")

/* 收到设备的 0x80 帧:把它的值记到状态表里(这才是"网关"该干的事) */staticvoidupdate_state_from(constuint8_t*frame,inttotal){uint8_ttype=frame[2];intplen=frame[3];constuint8_t*payload=frame+4;(void)total;if(type!=T_STATUS||plen!=4){printf("(这一帧不是设备状态帧,不进状态表:TYPE=0x%02X LEN=%d)\n",type,plen);return;}state.led=payload[0];state.fan=payload[1];state.temp_x10=(payload[2]<<8)|payload[3];state.last_seen_ms=now_ms();if(!state.online){state.online=1;printf("★ 设备上线了(灯 %s,风扇 %d 档,%.1f ℃)\n",state.led?"亮":"灭",state.fan,state.temp_x10/10.0);fflush(stdout);}}

B-3 周期上报:先判在线 → 组 0x81 → sendto → 再排下一次

/* 每 2 秒:判在线 → 组一帧 0x81 → 发给固定上报目标 → 再排下一次 */staticvoidsend_state_report(structuloop_timeout*t){uint8_tframe[32];uint8_tpayload[5];intlen;/* ① 在线判定:太久没收到设备状态帧,就算离线(值保留,界面还能显示"最后已知值") */if(state.online&&state.last_seen_ms!=0&&now_ms()-state.last_seen_ms>OFFLINE_MS){printf("★ 设备掉线了(超过 %d 毫秒没收到状态帧)\n",OFFLINE_MS);fflush(stdout);state.online=0;}/* ② 组 0x81:灯 风扇 温度高 温度低 在线 */payload[0]=(uint8_t)state.led;payload[1]=(uint8_t)state.fan;payload[2]=(uint8_t)((state.temp_x10>>8)&0xFF);payload[3]=(uint8_t)(state.temp_x10&0xFF);payload[4]=(uint8_t)state.online;len=build_frame(frame,T_STATE_REPORT,payload,5);/* ③ 发出去 */if(sendto(udp_fd,frame,(size_t)len,0,(structsockaddr*)&state_peer,sizeof(state_peer))!=len){perror("sendto(state)");}else{report_cnt++;dump("网关上报状态(0x81)",frame,len);printf(" → 灯 %s | 风扇 %d 档 | %.1f ℃ | %s(第 %d 次上报)\n",state.led?"亮":"灭",state.fan,state.temp_x10/10.0,state.online?"在线":"离线",report_cnt);fflush(stdout);}uloop_timeout_set(t,REPORT_MS);/* 周期任务的写法:回调里再排一次 */}

B-4 main() 里注册定时器和"上报目标"

constchar*dev=(argc>=2)?argv[1]:DEV_DEFAULT;intport=(argc>=3)?atoi(argv[2]):TCP_PORT;constchar*state_host=(argc>=4)?argv[3]:STATE_HOST_DEF;/* 课时 29 新增 */intstate_port=(argc>=5)?atoi(argv[4]):STATE_PORT_DEF;.../* ===== 课时 29 新增:UDP socket(只用来上报)+ 上报定时器 ===== */udp_fd=socket(AF_INET,SOCK_DGRAM,0);if(udp_fd<0){perror("socket(udp)");close(ser_fd);close(listen_fd);uloop_done();return1;}memset(&state_peer,0,sizeof(state_peer));state_peer.sin_family=AF_INET;state_peer.sin_port=htons((uint16_t)state_port);inet_aton(state_host,&state_peer.sin_addr);t_report.cb=send_state_report;uloop_timeout_set(&t_report,REPORT_MS);/* 2 秒后开始第一次上报 */

另外,serial_handle_frames()里在广播那一行前面加了一句update_state_from(ser_rxbuf, total);——
设备帧既广播给 TCP 客户端,也记进状态表,两条路互不影响。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询