30 嵌入式操作系统 | 上位机界面:Flask 网页显示 + 控制闭环
发布时间:2026/10/2 21:04:59来源:尧图网络
嵌入式操作系统 | 上位机界面Flask 网页显示 控制闭环本课程开源地址Giteehttps://gitee.com/fujianxinxi/qianrushixitongyingyongkaifa.git课件、示例代码与验收脚本都在该仓库可直接git clone或下载 ZIP 使用。模块七 · 2 学时 · 环境UbuntuFlask 前面几节跑通的设备面板/网关 socat 虚拟串口一句话概述写网页上位机——Flask 起一个页面后台线程收网关的 UDP 上报灯/风扇/温度/在线按钮走 TCP 给网关发命令。做完这件事用鼠标就能控制那个串口设备而且 Windows 宿主机浏览器里也能操作。本节目标会写 Flask 应用app.route(/) 页面渲染知道网页就是一个返回 HTML 的函数会用后台线程收 UDP 上报更新共享状态并说清为什么共享数据要加锁会做零 JavaScript 的交互按钮用form、自动刷新用meta http-equivrefresh学生看得懂、命令行也能验证能跑通闭环点网页按钮 → 网关 → 串口设备动作 → 网关上报 → 页面显示新状态一、讲解内容1.1 知识讲解上位机在项目里的位置前面的三条链路本节把它们连成人能用的东西鼠标点按钮 TCP长度前缀 帧 串口 55 AA 帧 ┌──────────┐ ─────────────────────────▶ ┌────────┐ ─────────────────▶ ┌──────────┐ │ 浏览器 │ │ 网关 │ │ 设备面板 │ │ 网页界面 │ ◀───────────────────────── │(课时29) │ ◀───────────────── │(课时26) │ └──────────┘ UDP 0x81 每 2 秒 └────────┘ 0x80 每秒 └──────────┘ ▲ ▲ ▲ │ ┌─────┴──────┐ │ └──────────────┤ 本节app.py │────────────────┘ 页面每 2 秒刷新 │ ① 收 UDP │ │ ② 存状态 │ │ ③ 发 TCP 命令│ └────────────┘为什么用 Flask网页而不是 PyQt5 窗口回顾课时 24 的那张表PyQt5 窗口课时 25/26Flask 网页本节在哪看必须虚拟机桌面虚拟机浏览器 /Windows 宿主机/ VS Code 端口转发部署演示要开桌面一条python3 app.py别人的手机/电脑也能看需要学什么Qt 控件、信号槽HTML 表单 一点点 Flask本课连 JS 都不用三个知识点本课就这三个① Flask 路由 “哪个网址交给哪个函数”app.route(/)# 浏览器访问 http://IP:5000/ 时执行 index()defindex():returnrender_page()# 返回一段 HTML 字符串浏览器就显示成页面app.route(/cmd)# 点按钮时访问 /cmd?nameled_ondefcmd():...returnredirect(/)# 处理完回到首页PRG 模式见下② 后台线程 共享状态必须加锁.主线程在跑 Flask处理网页请求还要有另一个线程去收 UDP 上报state{led:0,fan:0,temp:25.0,online:0,updated:0.0,msg:...}lockthreading.Lock()# 两个线程都碰 state所以要有锁defudp_listener():# 后台线程收网关上报whileTrue:data,_s.recvfrom(2048)...withlock:# 改 state 前先拿锁state[led]payload[0]...threading.Thread(targetudp_listener,daemonTrue).start()# 启动时开线程daemonTrue主程序退出时这个线程跟着退出否则进程不肯结束。为什么要锁一个线程在读渲染页面、另一个在写收到上报不加锁可能读到改了一半的数据。③ 零 JavaScript 的交互本节刻意这么设计需求不用 JS 的办法按钮发命令form methodget action/cmdinput typehidden namename valueled_onbutton灯 开/button/form页面自动刷新meta http-equivrefresh content2一行每 2 秒刷一次点完按钮不要重复提交PRG 模式/cmd处理完redirect(/)浏览器地址栏变回/刷新不会重发命令这就是网页版上位机的最小闭环命令走 TCP可靠、状态走 UDP 上报轻量。想学 JavaScript 的同学可以用本课顺手提供的 **/api/stateJSON 接口**自己做定时 fetch 版选做。1.2 操作步骤一条命令一步看现象第 0 步环境检查Flask 装了吗前面几节的东西在吗python3-cimport flask; print(Flask, flask.__version__)# 应打印 Flask 1.1.1whichsocat# 虚拟串口python3-cimport serial; print(pyserial OK)# 设备面板要用ls~/lab/课时29-网关状态表与上报/gateway_full# 上一节的网关编译好的缺 Flask 就补完整清单见课时 24 附录 Fsudoaptupdatesudoaptinstall-ypython3-flask第 1 步先把设备面板 网关跑起来三个终端和上节课一样# 终端 A虚拟串口socat-d-dpty,raw,echo0,link/tmp/ttyV0 pty,raw,echo0,link/tmp/ttyV1# 终端 B设备面板cd~/lab/课时26-设备面板接串口python3 device_panel_serial.py# 终端 C网关每 2 秒上报到 127.0.0.1:9100cd~/lab/课时29-网关状态表与上报./gateway_full /tmp/ttyV09000127.0.0.19100第 2 步启动网页上位机终端 Dcd~/lab/课时30-上位机界面 python3 app.py现象实测四条启动信息网页上位机启动http://0.0.0.0:5000宿主机用 http://虚拟机IP:5000 命令发往网关TCP 127.0.0.1:9000状态来自网关UDP 9100 正在监听网关上报UDP 9100 * Running on http://0.0.0.0:5000/ (Press CTRLC to quit)注意UDP 9100 只允许一个程序监听——如果你上一节课的state_listener.py还开着这里会报Address already in use。第 3 步用浏览器打开三种方式任选在哪打开地址备注虚拟机里的浏览器http://127.0.0.1:5000最直接Windows 宿主机浏览器http://虚拟机IP:5000虚拟机里ip a查 IP本机实测是192.168.31.129VS Code Remote-SSH端口面板 → 5000 → “在浏览器中打开”自动转发最省事页面长这样左边一列是状态、下面一排按钮、最下面显示最近一次操作┌─ 设备监控Flask 网页上位机──────────────────────────────┐ │ LED 灯 ●(绿) 亮 ← 绿色圆点 亮样式表画的 │ │ 风扇 2 档 │ │ 温度 24.5 ℃ ← 红色大字 │ │ 设备状态 在线 │ │ 最后更新 05:39:460 秒前 │ │ ────────────────────────────────────────────────────────│ │ [灯 开] [灯 关] [风扇 0 档] [风扇 1 档] [风扇 2 档] ... │ │ │ │ 最近一次操作风扇 2 档 → 已发送 55 AA 02 01 02 50 51 │ └────────────────────────────────────────────────────────────┘第 4 步不开浏览器也能验证SSH 里很方便# ① 看状态JSON页面拿的就是它wget-qO- http://127.0.0.1:5000/api/state现象实测{age:0.014,fan:0,led:0,msg:页面已启动等待网关上报…,online:1,temp:24.7,updated:1790426382.2}# ② 用网页接口点灯等价于点了页面上的【灯 开】wget-qO-http://127.0.0.1:5000/cmd?nameled_on|head-3现象实测接口返回 302重定向回首页网页程序日志出现已发命令 55 AA 01 01 01 E0 50 127.0.0.1 - - [26/Sep/2026 05:39:42] GET /cmd?nameled_on HTTP/1.1 302 -再发一条风扇命令然后看状态就变了实测wget-qO-http://127.0.0.1:5000/cmd?namefan_2/dev/nullsleep4wget-qO- http://127.0.0.1:5000/api/state{age:0.111,fan:2,led:1,online:1,temp:24.5, msg:风扇 2 档 → 已发送 55 AA 02 01 02 50 51网关回了 00 0A 55 AA 80 04 01 02 00 F5 8E 60}第 5 步看页面渲染出来的内容本节闭环的证据wget-qO- http://127.0.0.1:5000/|grep-ELED 灯|温度|设备状态|最近一次操作现象实测trtdLED 灯/tdtdspan classdot on/span亮/td/tr trtd温度/tdtd classtemp24.5 ℃/td/tr trtd设备状态/tdtd class在线/td/tr b最近一次操作/bspan classmsg风扇 2 档 → 已发送 55 AA 02 01 02 50 51网关回了 …/span同时设备面板上的灯真的亮了、风扇开始转就是上面那张截图。第 6 步看设备掉线时页面什么表现pkill-fdevice_panel_seria[l]# 拔掉设备sleep6wget-qO- http://127.0.0.1:5000/api/state# 看 online现象online:0页面上设备状态会显示红色的离线设备没在报状态温度保留最后已知值。第 7 步收工。依次 CtrlC网页 → 网关 → 设备面板 → socat。1.3 常见错误与排查现象原因解决启动就报Address already in useUDP 9100上一节的state_listener.py或另一个 app.py 还开着ss -lunp | grep 9100找到后杀掉或改STATE_PORT启动就报Address already in useTCP 5000上次的 app.py 没退干净ss -ltnp | grep 5000杀掉或改WEB_PORT页面一直还没收到上报① 网关没跑 ② 网关的上报目标不是本机 9100 ③ 端口不一致看网关日志有没有网关上报状态(0x81)确认网关第 3/4 个参数是127.0.0.1 9100网页能开但点按钮连不上网关网关没跑 / 端口不是 9000看页面下方最近一次操作里的提示确认网关 TCP 端口宿主机打不开http://IP:5000① 程序只监听 127.0.0.1 ② IP 拿错 ③ VMware 网络模式本课代码是host0.0.0.0ip a查 IP网络用 NAT/桥接或加端口转发按钮点了页面没变化meta refresh是 2 秒一次等一下或redirect(/)被浏览器缓存手动CtrlR或把content2改小试试页面中文乱码少了meta charsetutf-8保留 HTML 里那一行掉线后温度不变设计如此离线保留最后已知值界面上同时显示离线别把老值当新值想改界面颜色/加按钮—直接改PAGE里的 HTML/CSS加按钮就在CMDS里加一条 页面加一个form二、学生练习本节命令速查表命令作用示例python3 app.py启动网页上位机0.0.0.0:5000CtrlC 退出wget -qO- http://127.0.0.1:5000/api/state看 JSON 状态页面拿的就是它wget -qO- http://127.0.0.1:5000/cmd?nameled_on命令行点按钮也可fan_2/status/resetwget -qO- http://127.0.0.1:5000/ | head -20看页面 HTML验证渲染结果ss -ltnp | grep 5000/ss -lunp | grep 9100看端口占用排查 Address already in useip a查虚拟机 IP宿主机访问用编号任务提示预期结果L30.1四个终端跑起来第 1、2 步页面能打开显示在线 温度L30.2在宿主机浏览器里打开并点按钮第 3 步点【灯 开】→ 设备面板灯亮L30.3命令行验证第 4、5 步/api/state的 JSON 里led:1、fan:2L30.4掉线时页面表现第 6 步online:0页面显示红色离线L30.5加一个按钮风扇 3 档CMDS加一条 页面加一个form点它风扇转更快L30.6把刷新周期改成 1 秒改content2→content1状态更新更快也更费流量L30.7选做用 JS 自动刷新用/api/statefetchsetInterval页面不闪、实时更新L30.8选做给离线加醒目样式渲染时判断online输出不同 class一掉线界面很显眼三、作业必做交 4 张截图宿主机浏览器里的页面 / 点按钮后设备面板的灯亮 //api/state的 JSON / 掉线时页面显示离线并回答① 网页和 PyQt5 窗口各适合什么场景本课程为什么上位机用网页② 为什么要用后台线程收 UDP为什么改state要加锁③ 什么是 PRG 模式不加redirect(/)会有什么问题④ 控制走 TCP、状态走 UDP 上报这样分工的好处是什么选做① 完成 L30.6、L30.7② 把页面改成两栏布局左边状态、右边按钮或给温度加一个超过 26.5℃ 变红加粗的样式提交截图 文字回答命名课时30-作业截图.png四、本节小结与下节预告本节把前四节串成了一个能演示的闭环网页上位机Flask→ TCP 命令 → 网关 → 串口 → 设备面板动作 → 状态经 0x80 上报 → 网关状态表 → 每 2 秒 0x81 UDP 上报 → 页面刷新显示。你还验证了设备掉线时界面显示离线并且在 Windows 宿主机浏览器里也能操作。下节课课时 31选做做产品化把网关里的串口号 / 波特率 / TCP 端口 / 上报目标从命令行参数改成从/etc/config/device读uci课时 19再用ubus暴露一个device.status课时 20这样改配置不用重新编译、状态也能被别的程序查询。附录怎么运行 完整代码文件在代码/课时30-上位机界面/app.py约 255 行含页面 HTML。运行前需要课时 26 的设备面板 课时 29 的网关。附录 A环境准备 怎么运行四个终端# ⓿ 环境准备缺哪个装哪个完整清单见课时 24 附录 Fsudoaptupdatesudoaptinstall-ypython3-flask python3-serial socatsudoaptinstall-ypython3-pyqt5 xvfb# 设备面板要用# 终端 A虚拟串口 socat-d-dpty,raw,echo0,link/tmp/ttyV0 pty,raw,echo0,link/tmp/ttyV1# 终端 B设备面板课时 26cd~/lab/课时26-设备面板接串口python3 device_panel_serial.py# 终端 C网关课时 29每 2 秒上报cd~/lab/课时29-网关状态表与上报./gateway_full /tmp/ttyV09000127.0.0.19100# 终端 D网页上位机本节cd~/lab/课时30-上位机界面python3 app.pyipa|grepinet# 查虚拟机 IP宿主机浏览器开 http://IP:5000# 命令行验证不开浏览器wget-qO- http://127.0.0.1:5000/api/statewget-qO-http://127.0.0.1:5000/cmd?nameled_on/dev/nullwget-qO-http://127.0.0.1:5000/cmd?namefan_2/dev/nullsleep4wget-qO- http://127.0.0.1:5000/api/state附录 Bapp.py关键片段B-1 协议部分和前面几节一字不差defcrc16_modbus(data:bytes)-int:crc0xFFFFforbindata:crc^bfor_inrange(8):crc(crc1)^0xA001ifcrc1elsecrc1returncrcdefbuild_frame(type_:int,payload:bytesb)-bytes:bodybytes([type_,len(payload)])payload crccrc16_modbus(body)returnb\x55\xaabodybytes([crc0xFF,crc8])# 按钮名 → 要发的帧CMDS{led_on:(灯 开,build_frame(0x01,b\x01)),led_off:(灯 关,build_frame(0x01,b\x00)),fan_0:(风扇 0 档,build_frame(0x02,b\x00)),fan_1:(风扇 1 档,build_frame(0x02,b\x01)),fan_2:(风扇 2 档,build_frame(0x02,b\x02)),fan_3:(风扇 3 档,build_frame(0x02,b\x03)),status:(查状态,build_frame(0x03)),reset:(复位,build_frame(0x04)),}B-2 后台线程收 UDP 上报0x81defudp_listener():ssocket.socket(socket.AF_INET,socket.SOCK_DGRAM)s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)s.bind((0.0.0.0,STATE_PORT))print(正在监听网关上报UDP %d%STATE_PORT)whileTrue:data,_s.recvfrom(2048)iflen(data)6ordata[0]!0x55ordata[1]!0xAA:continuetype_,lengthdata[2],data[3]payloaddata[4:4length]gotdata[4length]|(data[5length]8)ifcrc16_modbus(data[2:4length])!got:print(收到一帧上报但 CRC 不对丢弃)continueiftype_0x81andlength5:withlock:# 改共享状态要加锁state[led]payload[0]state[fan]payload[1]state[temp]((payload[2]8)|payload[3])/10.0state[online]payload[4]state[updated]time.time()B-3 三个路由页面 / 控制 / JSON 接口app.route(/)defindex():returnrender_page()app.route(/cmd)defcmd():namerequest.args.get(name,)ifnamenotinCMDS:withlock:state[msg]不认识的命令%s%namereturnredirect(/)label,frameCMDS[name]resultsend_command(frame)withlock:state[msg]%s → %s%(label,result)returnredirect(/)# PRG 模式重定向回首页刷新不会重发命令app.route(/api/state)defapi_state():给想用 JS 自动刷新的同学用选做withlock:datadict(state)data[age](time.time()-data[updated])ifdata[updated]elseNonereturnjsonify(data)B-4 控制命令怎么发长度前缀 帧复用课时 28 的约定defsend_command(frame:bytes)-str:pktbytes([len(frame)8,len(frame)0xFF])frame# 长度前缀 帧try:ssocket.create_connection((GATEWAY_HOST,GATEWAY_PORT),timeout2.0)s.sendall(pkt)s.settimeout(0.5)try:replys.recv(64)exceptsocket.timeout:replybs.close()exceptOSErrorasexc:print(连网关失败%s%exc)return连不上网关%s:%d先确认网关跑起来了%(GATEWAY_HOST,GATEWAY_PORT)print(已发命令 %s%frame_text(frame))ifreply:return已发送 %s网关回了 %s%(frame_text(frame),frame_text(reply))return已发送 %s%frame_text(frame)B-5 页面自动刷新就一行不用 JSmetahttp-equivrefreshcontent2!-- 每 2 秒自动刷新一次不用 JS --想换成 JS 版页面不闪在页面里加一句setInterval(() fetch(/api/state).then(r r.json()).then(s { /* 更新 DOM */ }), 1000);这就是 L30.7 那道选做题。
网站建设高端定制企业官网