news 2026/9/19 2:01:15

Packet Tracer 8.2物联网实战:MQTT智能家居原型搭建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Packet Tracer 8.2物联网实战:MQTT智能家居原型搭建

1. 项目缘起与整体设计思路

1.1 为什么选Packet Tracer 8.2做物联网原型

很多人第一次接触物联网开发,脑子里冒出来的第一反应是买一块ESP8266或者STM32开发板,焊上传感器,连上Wi-Fi模块,然后开始调代码。这条路没错,但成本在于:硬件采购周期、接线出错、烧录失败、串口驱动不兼容,随便一个环节卡住就是半天。如果你只是想验证一个智能家居控制逻辑——比如“温度超过阈值自动开风扇,手机端能远程开关灯”——其实完全没必要一上来就动烙铁。

Packet Tracer 8.2是思科官方模拟器里第一个把物联网设备做完整支持的版本。它内置了MCU-PT(可编程微控制器)、各类传感器(温度、湿度、光照、运动)、执行器(风扇、灯、门锁),还支持Python脚本直接跑在MCU上。更关键的是,它原生支持MQTT协议的模拟通信。这意味着你可以在纯软件环境里,把“传感器采集→MQTT发布→Broker转发→客户端订阅→执行器动作”这条完整链路跑通,不需要任何物理硬件。

我选8.2而不是7.x或者9.x,原因很实际:8.2的IoT设备库最稳定,MCU-PT的Python API文档齐全,而且对MQTT的Topic层级支持没有9.x那么多限制。9.0.1虽然界面更现代,但部分IoT设备的行为逻辑有改动,网上教程大多基于8.x,踩坑成本更低。

1.2 智能家居原型的核心架构拆解

这个项目的本质是一个发布/订阅模型的最小闭环。我把它拆成四层:

  • 感知层:温度传感器、光照传感器、运动传感器,挂在MCU-PT的模拟引脚上
  • 控制层:MCU-PT运行Python脚本,负责读取传感器值、判断阈值、发布MQTT消息、订阅控制指令
  • 传输层:Packet Tracer内置的MQTT Broker(也可以理解为模拟的服务器),负责Topic路由
  • 应用层:另一台MCU-PT或者PC端的MQTT客户端,作为“手机App”的角色,订阅状态、发布控制命令

为什么用MQTT而不是HTTP?因为智能家居场景里,设备需要低功耗、长连接、异步推送。HTTP是请求-响应模式,设备要不停轮询服务器,耗电且延迟高。MQTT的发布/订阅模式天然适合“一个传感器变化,多个订阅者同时收到”的场景。比如温度传感器发布到home/livingroom/temperature,空调控制器和手机App同时订阅这个Topic,谁都不需要知道对方的存在。

1.3 整体数据流与Topic设计

Topic设计是MQTT项目里最容易埋坑的地方。我见过有人把所有消息都发到一个Topic里,结果订阅端要写一堆if-else去解析。正确的做法是按层级+功能划分:

Topic层级示例方向说明
home/[房间]/[设备]/statehome/livingroom/light/state设备→App设备状态上报
home/[房间]/[设备]/cmdhome/livingroom/light/cmdApp→设备控制命令下发
home/[房间]/[传感器]/valuehome/livingroom/temp/value传感器→App传感器数值
home/[房间]/[设备]/onlinehome/livingroom/fan/online设备→App在线状态(遗嘱消息)

这个设计的好处是:订阅端可以用通配符home/livingroom/#一次性订阅客厅所有消息,也可以用home/+/light/cmd订阅所有房间的灯控命令。+匹配单层,#匹配多层,这是MQTT协议里非常实用的特性。

注意:Packet Tracer 8.2的MQTT Broker对#通配符的支持有限,实测订阅home/#有时收不到消息,建议用home/+/+这种精确到层级的写法。

2. 环境准备与核心细节解析

2.1 Packet Tracer 8.2的安装与IoT设备库确认

Packet Tracer 8.2的安装包在思科官网注册后可以下载。安装过程没什么好说的,一路Next。但有一个坑:安装完成后必须登录思科账号才能解锁完整设备库。如果你跳过登录,IoT设备面板里只有寥寥几个设备,MCU-PT根本找不到。

登录之后,在设备选择面板左下角找到“End Devices”,再往下拉,会看到“IoT”分类。里面有几个关键设备:

  • MCU-PT:可编程微控制器,支持Python和JavaScript,这是我们项目的主角
  • SBC-PT:单板计算机,性能更强但配置复杂,初期不建议用
  • Thing:通用物联网设备,可以自定义传感器和执行器
  • Home Gateway:家庭网关,用于连接IoT网络和传统网络

我建议先用MCU-PT,因为它简单直接,Python API就那几个函数,半小时能上手。

2.2 MCU-PT的Python API速查

MCU-PT的Python环境是精简版,不支持pip安装第三方库。但内置了mqtt模块和gpio模块,够用了。核心API如下:

# 导入模块 from mqtt import MQTTClient from gpio import * from time import * # GPIO操作 pinMode(0, INPUT) # 设置引脚0为输入 pinMode(1, OUTPUT) # 设置引脚1为输出 value = analogRead(0) # 读取模拟值(0-1023) digitalWrite(1, HIGH) # 输出高电平 digitalWrite(1, LOW) # 输出低电平 # MQTT操作 client = MQTTClient("device_id", "broker_ip", 1883) client.connect() client.publish("topic", "payload") client.subscribe("topic") client.on_message = callback_function client.loop_forever() # 阻塞式循环

这里有个细节:analogRead返回的是0-1023的整数,对应0-5V电压。温度传感器在Packet Tracer里的映射关系是:0-1023线性对应-100°C到100°C。所以温度计算公式是:

temperature = (analogRead(pin) / 1023.0) * 200.0 - 100.0

这个公式我实测过,和Packet Tracer内部显示的温度值误差在±0.5°C以内,够用了。

2.3 MQTT Broker的配置与网络拓扑

Packet Tracer 8.2里没有独立的“MQTT Broker”设备,但你可以用一台普通服务器(Server-PT)来充当。配置步骤:

  1. 拖一台Server-PT到拓扑里,命名为“MQTT-Broker”
  2. 双击进入配置界面,在“Services”标签页找到“IoT”服务
  3. 勾选“MQTT Broker”,端口保持1883
  4. 记下服务器的IP地址,比如192.168.1.100

网络拓扑建议这样搭:

  • 一台无线路由器(WRT300N)作为家庭网关,IP设为192.168.1.1
  • MQTT Broker服务器用网线连到路由器的LAN口,IP设为192.168.1.100
  • MCU-PT设备通过无线网卡连接到路由器,IP由DHCP分配,比如192.168.1.101192.168.1.102
  • 一台PC作为“手机App”的模拟端,也连到同一个路由器

提示:MCU-PT默认没有无线网卡,需要双击设备,在“Physical”标签页里关掉电源,拖入WMP300N无线网卡,再开机。这个操作和真机装网卡一样,很多人第一次找不到。

2.4 为什么用Python而不是JavaScript

MCU-PT支持Python和JavaScript两种脚本。我选Python的原因:

  • Python的MQTT库API更直观,client.publish()client.subscribe()一看就懂
  • JavaScript在Packet Tracer里的异步回调处理比较绕,容易踩坑
  • 网上Python的MQTT示例代码多,遇到问题好搜

但Python也有缺点:MCU-PT的Python是单线程的,loop_forever()会阻塞整个脚本。如果你既要读传感器又要处理MQTT消息,必须用client.loop()非阻塞模式,或者把传感器读取放在回调里。这个后面会详细讲。

3. 实操过程与核心环节实现

3.1 搭建最小可运行拓扑

先别急着写完整代码,第一步是验证“MCU能连上Broker并发布消息”。拓扑如下:

  • 1台WRT300N路由器
  • 1台Server-PT(MQTT Broker)
  • 1台MCU-PT(发布者)
  • 1台PC(订阅者,用MQTT客户端软件)

MCU-PT的配置:

  1. 双击MCU-PT,进入“Config”标签页
  2. 在“Wireless0”里设置SSID为路由器的SSID,密码一致
  3. 确认获取到IP地址,比如192.168.1.101
  4. 进入“Programming”标签页,选择“Python”,开始写代码

最小发布代码:

from mqtt import MQTTClient from time import * client = MQTTClient("mcu_pub_01", "192.168.1.100", 1883) client.connect() while True: client.publish("home/test/hello", "Hello from MCU") sleep(2)

PC端的验证:用MQTTX或者Mosquitto客户端,订阅home/test/hello,应该每2秒收到一条消息。如果收不到,检查三件事:Broker的IoT服务是否开启、MCU的IP是否和Broker在同一网段、Topic名称是否完全一致(MQTT区分大小写)。

3.2 温度传感器接入与数据发布

验证通信没问题后,把温度传感器加上。在Packet Tracer里,温度传感器叫“Temp Sensor”,拖一个到MCU-PT旁边,用“IoT Custom Cable”连接。连接时注意引脚对应:

  • 传感器VCC → MCU的3.3V
  • 传感器GND → MCU的GND
  • 传感器OUT → MCU的A0(模拟引脚0)

然后在Python代码里读取:

from mqtt import MQTTClient from gpio import * from time import * client = MQTTClient("mcu_temp_01", "192.168.1.100", 1883) client.connect() pinMode(0, INPUT) while True: raw = analogRead(0) temp = (raw / 1023.0) * 200.0 - 100.0 client.publish("home/livingroom/temp/value", str(round(temp, 1))) sleep(5)

这里有个实操心得:analogRead在Packet Tracer里有时候会返回抖动值,比如连续两次读到512和518。解决办法是取多次平均

def read_temp(): total = 0 for i in range(5): total += analogRead(0) sleep(0.1) raw = total / 5.0 return (raw / 1023.0) * 200.0 - 100.0

这样读出来的温度稳定得多,发布到MQTT的数值不会跳来跳去。

3.3 订阅控制命令并驱动执行器

现在让MCU同时具备“发布传感器数据”和“订阅控制命令”的能力。加一个LED灯作为执行器,连接到MCU的D1引脚。

关键点:MCU-PT的Python是单线程的,不能用loop_forever(),否则传感器读取会被阻塞。正确做法是用client.loop()非阻塞轮询:

from mqtt import MQTTClient from gpio import * from time import * client = MQTTClient("mcu_ctrl_01", "192.168.1.100", 1883) client.connect() pinMode(0, INPUT) # 温度传感器 pinMode(1, OUTPUT) # LED灯 def on_message(topic, payload): if topic == "home/livingroom/light/cmd": if payload == "ON": digitalWrite(1, HIGH) client.publish("home/livingroom/light/state", "ON") elif payload == "OFF": digitalWrite(1, LOW) client.publish("home/livingroom/light/state", "OFF") client.on_message = on_message client.subscribe("home/livingroom/light/cmd") last_temp_time = time() while True: client.loop() # 非阻塞处理MQTT消息 now = time() if now - last_temp_time >= 5: raw = analogRead(0) temp = (raw / 1023.0) * 200.0 - 100.0 client.publish("home/livingroom/temp/value", str(round(temp, 1))) last_temp_time = now sleep(0.1)

这段代码的核心逻辑:主循环每0.1秒跑一次client.loop(),检查有没有新消息;同时每5秒读一次温度并发布。time()函数返回的是秒数(浮点数),所以用差值判断时间间隔。

注意:client.loop()必须频繁调用,否则MQTT的Keep Alive会超时,Broker会断开连接。0.1秒的间隔是安全的。

3.4 手机App模拟端:PC上的MQTT客户端

PC端我用Python写一个简单的控制脚本,模拟手机App的行为:

import paho.mqtt.client as mqtt import time def on_connect(client, userdata, flags, rc): print("Connected with result code " + str(rc)) client.subscribe("home/livingroom/#") def on_message(client, userdata, msg): print(f"[{msg.topic}] {msg.payload.decode()}") client = mqtt.Client() client.on_connect = on_connect client.on_message = on_message client.connect("192.168.1.100", 1883, 60) client.loop_start() while True: cmd = input("Enter command (ON/OFF/quit): ") if cmd == "quit": break client.publish("home/livingroom/light/cmd", cmd) time.sleep(1) client.loop_stop() client.disconnect()

这个脚本需要paho-mqtt库,用pip install paho-mqtt安装。运行后,输入ON或OFF,MCU端的LED就会响应,同时状态会发布回home/livingroom/light/state,PC端也能看到。

3.5 完整代码整合与自动化逻辑

把前面的片段整合成一个完整的智能家居控制脚本,加入自动化逻辑:温度超过30°C自动开风扇,低于25°C自动关风扇。

from mqtt import MQTTClient from gpio import * from time import * BROKER_IP = "192.168.1.100" CLIENT_ID = "mcu_home_01" client = MQTTClient(CLIENT_ID, BROKER_IP, 1883) client.connect() # 引脚定义 TEMP_PIN = 0 LIGHT_PIN = 1 FAN_PIN = 2 pinMode(TEMP_PIN, INPUT) pinMode(LIGHT_PIN, OUTPUT) pinMode(FAN_PIN, OUTPUT) fan_state = False light_state = False def on_message(topic, payload): global light_state if topic == "home/livingroom/light/cmd": if payload == "ON": digitalWrite(LIGHT_PIN, HIGH) light_state = True client.publish("home/livingroom/light/state", "ON") elif payload == "OFF": digitalWrite(LIGHT_PIN, LOW) light_state = False client.publish("home/livingroom/light/state", "OFF") client.on_message = on_message client.subscribe("home/livingroom/light/cmd") def read_temp(): total = 0 for i in range(5): total += analogRead(TEMP_PIN) sleep(0.05) raw = total / 5.0 return (raw / 1023.0) * 200.0 - 100.0 last_temp_time = time() last_fan_time = time() while True: client.loop() now = time() # 每5秒读温度并发布 if now - last_temp_time >= 5: temp = read_temp() client.publish("home/livingroom/temp/value", str(round(temp, 1))) # 自动化:温度>30开风扇,<25关风扇 if temp > 30 and not fan_state: digitalWrite(FAN_PIN, HIGH) fan_state = True client.publish("home/livingroom/fan/state", "ON") elif temp < 25 and fan_state: digitalWrite(FAN_PIN, LOW) fan_state = False client.publish("home/livingroom/fan/state", "OFF") last_temp_time = now sleep(0.1)

这段代码跑起来后,你可以手动改变温度传感器的值(双击传感器,拖动滑块),观察风扇是否自动开关。同时PC端订阅home/livingroom/#,能看到所有状态变化。

4. 常见问题与排查技巧实录

4.1 MQTT连接失败的五种原因

现象可能原因排查方法
connect()返回FalseBroker IP错误在PC上ping Broker的IP
连接后立即断开Client ID重复确保每个MCU的Client ID唯一
收不到订阅消息Topic不匹配检查大小写、通配符层级
发布成功但订阅端无消息Broker的IoT服务未启动双击Server-PT检查Services
间歇性断连Keep Alive超时确保client.loop()调用频率<1秒

我踩过最坑的一次是:MCU-PT的无线网卡没插好,IP地址是169.254.x.x的自动私有地址,根本连不上Broker。后来养成习惯,每次先看MCU的IP是不是192.168.1.x网段。

4.2 传感器读数不准的校准方法

Packet Tracer的温度传感器默认映射是线性的,但如果你发现读数和传感器面板显示的值差很多,可以手动校准。方法:把传感器滑块拖到0°C,记录analogRead的值(比如512),再拖到100°C,记录值(比如1023)。然后用两点法计算:

temp = (raw - raw_at_0) * 100.0 / (raw_at_100 - raw_at_0)

这个公式比默认的线性映射更准,因为Packet Tracer的传感器模拟有时候不是完美的0-1023对应-100到100。

4.3 Python脚本报错的快速定位

MCU-PT的Python报错信息很简略,只有一个红叉。我的排查顺序:

  1. 检查import语句,mqttgpio必须分开写,不能from mqtt import *
  2. 检查pinMode是否在analogRead之前调用
  3. 检查client.connect()是否返回True,如果False后面全错
  4. 检查on_message回调的参数个数,必须是(topic, payload)两个

提示:MCU-PT的Python不支持try-except,所以任何异常都会直接终止脚本。写代码时要格外小心边界条件,比如analogRead的引脚号不能超过MCU的模拟引脚数量。

4.4 多设备Topic冲突的解决

当你有多个MCU时,Client ID和Topic都要唯一。我建议的命名规范:

  • Client ID:mcu_[房间]_[功能]_[编号],如mcu_livingroom_temp_01
  • Topic:home/[房间]/[设备]/[属性],如home/livingroom/temp/value

如果两个设备发布了相同Topic,订阅端会收到两条消息,容易混淆。用mosquitto_sub -t 'home/#' -v可以看到所有消息的Topic,方便调试。

4.5 性能优化:减少MQTT消息频率

MCU-PT的模拟性能有限,如果发布频率太高(比如每0.1秒一次),Packet Tracer会卡顿。我的经验值:

  • 温度、湿度等慢变量:5-10秒发布一次
  • 光照、运动等快变量:1-2秒发布一次
  • 控制命令:即时发布,不限制

另外,client.loop()的调用间隔不要小于0.05秒,否则CPU占用率飙升,模拟器会变慢。

5. 项目扩展与进阶玩法

5.1 加入Home Gateway实现跨网段通信

前面的拓扑里,所有设备都在同一个局域网。如果你想模拟“手机在外面通过互联网控制家里”的场景,可以加一台Home Gateway。配置方法:

  1. 拖一台Home Gateway到拓扑里
  2. 外网口连到一台ISP路由器,内网口连到家庭路由器
  3. 在Home Gateway上配置端口映射,把MQTT Broker的1883端口暴露出去
  4. PC端用ISP路由器的公网IP连接Broker

这个配置在Packet Tracer里完全可行,但要注意Home Gateway的NAT规则要写对,否则外网连不进来。

5.2 用SBC-PT跑Node-RED做可视化面板

SBC-PT是Packet Tracer里的单板计算机,支持跑Node-RED。你可以把SBC-PT连到家庭网络,在Node-RED里拖几个节点,做一个简单的Dashboard:温度曲线、灯控开关、风扇状态。这样整个项目就更接近真实的智能家居系统了。

不过SBC-PT的性能比MCU-PT强,但配置也复杂得多。建议先把MCU-PT的MQTT链路跑通,再折腾SBC-PT。

5.3 遗嘱消息与在线状态监测

MQTT的遗嘱消息(Last Will and Testament)是一个很实用的特性:设备连接时告诉Broker“如果我断线了,帮我发布这条消息”。在智能家居里,可以用来监测设备是否在线。

MCU-PT的MQTT库支持遗嘱消息吗?实测下来,8.2版本的MQTTClient构造函数不支持遗嘱参数。但你可以用变通方法:设备上线时发布home/livingroom/device/onlinetrue,然后定期发布心跳。App端如果超过一定时间没收到心跳,就认为设备离线。

# 上线时 client.publish("home/livingroom/mcu/online", "true") # 主循环里每30秒发一次心跳 if now - last_heartbeat >= 30: client.publish("home/livingroom/mcu/heartbeat", str(int(now))) last_heartbeat = now

App端记录最后一次心跳时间,超过60秒没更新就标记为离线。这个逻辑虽然不如遗嘱消息优雅,但在Packet Tracer里够用了。

5.4 从原型到真实硬件的迁移路径

Packet Tracer里跑通的逻辑,迁移到真实硬件时需要注意:

  • GPIO引脚号不同:MCU-PT的A0在真实ESP8266上是A0,但在STM32上可能是PA0,需要改代码
  • MQTT库不同:真实硬件上常用umqtt.simple(MicroPython)或PubSubClient(Arduino),API和Packet Tracer的不一样
  • 网络配置不同:真实硬件要配Wi-Fi SSID和密码,Packet Tracer里是模拟的
  • 电源管理:真实硬件要考虑功耗,Packet Tracer里不用管

Topic设计和业务逻辑可以完全复用。这也是为什么我建议先在Packet Tracer里把逻辑跑通——改硬件适配层比改业务逻辑容易得多。

6. 实操心得与避坑清单

6.1 我踩过的三个大坑

第一个坑:MCU-PT的Python脚本保存后不自动运行。你需要手动点击“Run”按钮,而且每次修改代码后都要重新Run。如果忘了,MCU还是跑旧代码,调试半天找不到问题。

第二个坑:无线网卡的SSID区分大小写。路由器的SSID是HomeNetwork,你在MCU里写成homenetwork,连不上。Packet Tracer不会提示密码错误,只是默默连不上。

第三个坑:MQTT Broker的IoT服务默认关闭。新拖出来的Server-PT,IoT服务是关的。你必须手动勾选“MQTT Broker”,否则MCU连接时直接超时。

6.2 调试利器:Packet Tracer的模拟模式

Packet Tracer右下角有个“Simulation”模式,可以逐包查看网络通信。调试MQTT时,切换到模拟模式,过滤MQTT协议,你能看到CONNECT、PUBLISH、SUBSCRIBE、PINGREQ这些报文的详细内容。这个功能对理解MQTT协议帮助极大,比看文档直观一百倍。

6.3 代码版本管理建议

MCU-PT的代码编辑器没有版本管理功能,改错了只能撤销。我的做法是:每跑通一个功能,就把代码复制到PC上的文本文件里,按版本命名,比如v1_publish_only.pyv2_subscribe_light.pyv3_auto_fan.py。这样出问题了可以快速回滚,也方便对比不同版本的差异。

6.4 性能瓶颈的预判

Packet Tracer是模拟器,不是真机。当你的拓扑里有超过5台MCU-PT同时跑Python脚本时,模拟器会明显变卡。解决办法:

  • 减少不必要的设备,只保留核心节点
  • 降低传感器读取频率
  • 关闭Simulation模式,用Realtime模式跑
  • 如果还是卡,把部分MCU的逻辑合并到一台设备上

我实测过,一台普通笔记本跑4台MCU-PT + 1台Server + 1台PC,Realtime模式下CPU占用约40%,还能接受。超过6台MCU就开始卡了。

6.5 从这个小项目能学到什么

这个项目表面上是“用MQTT控制灯和风扇”,但底层训练的是物联网系统的完整思维:感知→传输→处理→执行→反馈。你把MQTT换成CoAP、把MCU-PT换成ESP32、把Packet Tracer换成真实网络,这套思维框架是不变的。

另外,Topic设计、QoS选择、Keep Alive调优、遗嘱消息这些MQTT核心概念,在Packet Tracer里都能实践。虽然模拟器有局限性,但作为入门到进阶的跳板,它比直接啃协议文档高效得多。

最后分享一个我常用的调试技巧:在PC端用mosquitto_sub -t 'home/#' -v订阅所有消息,同时开一个mosquitto_pub手动发命令。这样你可以脱离MCU,单独测试Broker和Topic是否正常工作。排查问题时,先把MCU排除在外,确认Broker和PC端通信正常,再逐步加入MCU,定位效率会高很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 2:00:32

aarch64上Qt5.14.2静态编译实战:交叉编译与部署指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 1:59:14

从囚徒困境到古诺模型:纳什均衡的博弈论实战解析

简介&#xff1a;管理经济学课程配套的博弈论教学讲义PPT&#xff0c;面向经济学、管理学专业学生及需要掌握策略决策思维的管理者&#xff0c;系统讲解博弈论在寡头竞争、市场竞争等经济情境中的核心应用。资源共1个PPTX课件&#xff0c;约51页精炼内容&#xff0c;压缩包大小…

作者头像 李华
网站建设 2026/9/19 1:57:36

旅游资源学PDF考点解析与智能备考系统构建

简介&#xff1a;本资源是一份系统、精炼的《旅游资源学》课程复习资料&#xff0c;面向旅游管理、地理科学、文化产业管理等专业本科生及考研备考学生&#xff0c;聚焦核心概念辨析与高频考点梳理&#xff0c;助力高效掌握旅游资源分类、形成机理与文化内涵。资料以PDF格式单文…

作者头像 李华
网站建设 2026/9/19 1:56:17

试 Databricks Astra 高级设计,TaoToken 记录 Token 消耗

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 1:51:27

UPS选型与NUT联动关机:PVE/TrueNAS实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华