# weather-python **Repository Path**: wangdong_cmcc/weather-python ## Basic Information - **Project Name**: weather-python - **Description**: 这是一个在 PC端或者RK3588 开发板上可独立运行的 Python编写的 网络天气站程序。能实时获取并显示天气信息、提供三日预报,并通过音箱进行语音播报。此外,它还支持 MQTT 数据上报,让你能在手机上订阅查看天气数据,并能在有显示器时自动切换图形界面或无显示器时使用终端界面。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-14 - **Last Updated**: 2026-05-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RK3588 网络天气站——Python 版设计文档 ![天气图标](./ScreenShot.png) ## 1. 项目概述 | 项目 | 说明 | |---|---| | **项目名称** | RK3588 Weather Station (Python) | | **硬件平台** | RK3588 开发板 + HDMI/MIPI 显示器 + 音箱 | | **操作系统** | Ubuntu 22.04.5 LTS,内核 5.10.160 | | **开发语言** | Python 3.8+ | | **GUI 框架** | Tkinter(Python 标准库,有显示器时) | | **TUI 框架** | curses(Python 标准库,无显示器时) | | **核心功能** | 实时天气显示、3日预报、语音播报、自动播报、MQTT 数据上报、开机自启 | --- ## 2. 系统架构 ``` ┌──────────────────────────────────────────────────────────────┐ │ RK3588 开发板 (Ubuntu 22.04) │ ├────────────┬──────────────┬───────────────┬──────────────────┤ │ │ │ │ │ │ [requests] │ [json] │ [tkinter / │ [paho-mqtt] │ │ HTTP 请求 │ JSON 解析 │ curses TUI] │ MQTT 上报 │ │ ↓ │ ↓ │ ↓ │ ↓ │ │ 和风天气API │ 数据转换 │ 屏幕显示 │ 手机端订阅 │ │ │ │ ↓ │ │ │ │ │ [espeak-ng │ │ │ │ │ / espeak] │ │ │ │ │ 语音播报 │ │ └────────────┴──────────────┴───────────────┴──────────────────┘ ↑ ↑ 网络请求 音频输出 / MQTT 发布 ``` ### 2.1 数据流 ``` [定时器触发 / 用户点击刷新] │ ▼ [WeatherFetcher] ──HTTP GET──▶ 和风天气 API │ │ JSON 字符串 ▼ [WeatherParser] │ │ CurrentWeather / DailyForecast dataclass ▼ [WeatherBackend] ──→ 多回调分发 ──→ tkinter / curses UI 更新 ──→ MqttPublisher 上报 │ ▼ [TTSSpeaker] ──subprocess──▶ espeak-ng / espeak 语音输出 ``` ### 2.2 运行模式 | 模式 | 入口 | 条件 | 触发方式 | |---|---|---|---| | **GUI (tkinter)** | `gui.py` | `$DISPLAY` 已设置 + tkinter 可用 | `python3 main.py` 或 `--gui` | | **终端 (curses)** | `curses_ui.py` | 无 `$DISPLAY` 或 `--tui` | `python3 main.py --tui` | ### 2.3 组件职责 | 组件 | 文件 | 职责 | 输入 | 输出 | |---|---|---|---|---| | **WeatherFetcher** | `weather_fetcher.py` | 异步 HTTP 请求,每请求独立线程,支持 gzip | 城市 Location ID | JSON 字符串(回调) | | **WeatherParser** | `weather_parser.py` | JSON 解析与字段校验,字符串→数值类型转换 | JSON 字符串 | CurrentWeather / DailyForecast dataclass | | **WeatherBackend** | `weather_backend.py` | 协调层:fetch→parse→多回调分发,维护城市名映射 | 用户操作(刷新/播报) | 多个回调(UI + MQTT) | | **TTSSpeaker** | `tts_speaker.py` | 天气数据拼接为中文语句,调用 espeak-ng/espeak 合成语音 | CurrentWeather dataclass | 音频输出(非阻塞) | | **GUI** | `gui.py` | tkinter 深色主题界面:当前天气、3日预报卡片、操作按钮、播报状态 | dict + list | 图形界面 | | **CursesApp** | `curses_ui.py` | curses 终端界面:键盘控制,无 X 依赖 | dict + list | 终端界面 | | **WeatherIcon** | `weather_icon.py` | 根据天气文字返回对应 emoji 图标 | 天气描述字符串 | emoji 字符 | | **MqttPublisher** | `mqtt_client.py` | 发布天气数据到 MQTT Broker,支持 retained 消息和遗嘱 | dict + list | MQTT 报文 | --- ## 3. 目录结构 ``` weather_python/ ├── main.py # 入口:自动检测 GUI/TUI,支持 --gui/--tui/--no-mqtt ├── config.py # API 配置 + 城市映射 + 主题常量 + MQTT 配置 ├── models.py # CurrentWeather / DailyForecast 数据类 ├── weather_fetcher.py # 异步 HTTP 请求(requests + daemon 线程) ├── weather_parser.py # JSON 解析 + 字段校验 + 类型转换 ├── weather_backend.py # 协调层:多回调分发(UI + MQTT 并行通知) ├── tts_speaker.py # TTS 语音播报(espeak-ng 优先,espeak 回退) ├── weather_icon.py # 天气图标 emoji 映射 ├── gui.py # tkinter 暗色主题 GUI(带语音播报状态反馈) ├── curses_ui.py # curses 终端界面(无需 X,带状态提示 + 键盘快捷键) ├── mqtt_client.py # MQTT 发布者(retained + 遗嘱消息) ├── 设计文档.md # 本文档 ├── requirements.txt # requests, paho-mqtt └── weather.service # systemd 开机自启 ``` --- ## 4. 接口设计 ### 4.1 和风天气 API | 项目 | 内容 | |---|---| | **Host** | `mr5egn8gqa.re.qweatherapi.com`(CDN 加速域名) | | **认证** | URL 参数 `key={YOUR_API_KEY}` | | **实时天气** | `GET /v7/weather/now?location={id}&key={k}` | | **3日预报** | `GET /v7/weather/3d?location={id}&key={k}` | | **Location ID** | 9位数字,如 `101190401`(苏州) | **实时天气响应关键字段**: | JSON 路径 | 类型 | 说明 | |---|---|---| | `code` | string | `"200"` 表示成功 | | `updateTime` | string | 更新时间 ISO8601 | | `now.text` | string | 天气描述(多云/晴/阴…) | | `now.temp` | string | 温度,需转 float | | `now.humidity` | string | 湿度百分比,需转 int | | `now.windSpeed` | string | 风速 km/h,需转 float | **3日预报响应关键字段**: | JSON 路径 | 类型 | 说明 | |---|---|---| | `daily[].fxDate` | string | 日期 yyyy-MM-dd | | `daily[].textDay` | string | 白天天气 | | `daily[].tempMax` | string | 最高温 | | `daily[].tempMin` | string | 最低温 | ### 4.2 内部数据结构 ```python # models.py @dataclass class CurrentWeather: city: str = "" # 城市英文名(从 fxLink 提取) text: str = "" # 天气描述:"多云"、"晴"... temp: float = 0.0 # 当前温度 °C humidity: int = 0 # 湿度百分比 wind_speed: float = 0.0 # 风速 km/h last_update: str = "" # 数据更新时间 @dataclass class DailyForecast: date: str = "" # "2026-05-14" text_day: str = "" # 白天天气 temp_max: float = 0.0 # 最高温 temp_min: float = 0.0 # 最低温 ``` ### 4.3 WeatherFetcher 接口 ```python # weather_fetcher.py Callback = Callable[[bool, str], None] class WeatherFetcher: def __init__(self): """初始化,存储 API Key""" def fetch_weather(self, location_id: str, callback: Callback): """在 daemon 线程中 GET 实时天气,通过 callback(success, body) 返回""" def fetch_forecast(self, location_id: str, callback: Callback): """同上,URL 替换为 3 日预报接口""" ``` **设计要点**:每请求使用 `threading.Thread(daemon=True)` 启动独立线程,调用 `requests.get()` 时设置 `timeout=10`、`verify=False`、`Accept-Encoding: gzip`。线程为 daemon 模式,主线程退出时自动回收,无需手动 join。 与原 C++ 版的关键差异: | C++ | Python | |---|---| | `std::thread::detach()` | `threading.Thread(daemon=True).start()` | | 每线程新建 `CURL*` 句柄 | 每线程独立 `requests.get()` 调用(requests 内部管理连接) | | `curl_easy_setopt` 逐项配置 | `requests.get()` 关键字参数 | | `CURLOPT_SSL_VERIFYPEER = 0` | `verify=False` | ### 4.4 WeatherParser 接口 ```python # weather_parser.py class WeatherParser: @staticmethod def parse_current(json_data: str) -> Optional[CurrentWeather]: """ 解析实时天气 JSON。 成功返回 CurrentWeather,失败返回 None。 - 检查 code == "200" - now.text / now.temp / now.humidity / now.windSpeed 字符串→数值 - 从 fxLink 提取城市英文名(如 "suzhou") """ @staticmethod def parse_forecast(json_data: str) -> list[DailyForecast]: """ 解析 3 日预报 JSON。 成功返回 DailyForecast 列表,失败返回 []。 - 检查 code == "200" 且 daily 数组存在 - 遍历 daily[],提取 fxDate / textDay / tempMax / tempMin - tempMax / tempMin 字符串→float """ ``` **设计要点**:API 中 `temp`、`humidity`、`windSpeed`、`tempMax`、`tempMin` 均为字符串类型,必须做 `string→数值` 转换。API 不返回中文城市名,城市名通过 Location ID 映射表获取。 ### 4.5 WeatherBackend(协调层) ```python # weather_backend.py UpdateCallback = Callable[[dict, list], None] class WeatherBackend: def __init__(self, location_id: str = "101190401"): """创建 WeatherFetcher 实例,初始化空缓存""" def on_update(self, callback: UpdateCallback): """ 注册数据更新回调(可多次调用,注册多个回调)。 UI 和 MQTT 各自注册自己的回调,互不影响。 """ @property def location_id(self) -> str: ... @property def location_name(self) -> str: ... @staticmethod def available_cities() -> list[tuple[str, str]]: """返回可切换的城市列表 [(id, name), ...]""" def set_location(self, location_id: str): """切换城市,清空缓存并立即刷新数据""" def refresh(self): """ 并行发起两个异步请求: fetcher.fetch_weather() → 解析 → 更新 _current → _notify() fetcher.fetch_forecast() → 解析 → 更新 _forecast → _notify() _notify() 遍历所有已注册回调,各自独立调用。 两个请求各自独立回调,不互相等待——当前天气先返回就先通知。 """ @property def current(self) -> dict: """返回最新天气数据缓存(供 TTS 使用)""" @property def forecast(self) -> list[dict]: """返回最新预报数据缓存""" # 内部维护: # _fetcher: WeatherFetcher 实例 # _current: dict (最新天气缓存,key 为 city/text/temp/humidity/windSpeed/lastUpdate) # _forecast: list[dict] (最新预报缓存,每项含 date/textDay/tempMax/tempMin) # _callbacks: list[UpdateCallback] (回调列表,UI + MQTT 等) # _location_id: str ``` **设计要点**: - 多回调模式:`on_update()` 可多次调用,`_notify()` 遍历回调列表逐个调用,单个回调异常不影响后续回调 - 两个 API 请求各自独立完成即回调,不等待对方——当前天气先返回就先显示,预报到了再追加 - 与原 C++ 版的信号/槽机制不同,Python 版使用回调函数模式 ### 4.6 GUI 界面结构(tkinter) ``` Tk 窗口 (800×480, 不可调整大小, 背景色 #1a1a2e) ├── after(): 每30分钟 → weatherBackend.refresh() ├── Frame (主内容区, 紧凑布局): │ ├── Label: 城市名 (28px, 白色 #ffffff) │ ├── Frame (行容器): │ │ ├── Label: 大号温度 (56px bold, 金色 #ffd700) │ │ └── Frame (列容器): │ │ ├── Label: 天气描述 (22px, #cccccc) │ │ ├── Label: 湿度 (12px, #999999) │ │ ├── Label: 风速 (12px, #999999) │ │ └── Label: 更新时间 (9px, #666666) │ ├── Label: 天气 emoji 图标 (40px) │ ├── Label: "未来天气预报" 标题 (14px, #888888) │ └── Frame (预报卡片容器, 横向排列): │ └── 每项 Frame (95×78, 背景 #2a2a4e): │ ├── Label: 日期 MM-DD (11px, 白色) │ ├── Label: 天气 (13px, 金色) │ └── Label: 最高/最低温 (10px, #aaaaaa) └── Frame (底部固定, pack side=bottom): ├── Label: 语音播报状态提示 (10px, 金色) ← 点击播报后显示内容预览 └── Frame (按钮行): ├── OptionMenu: 城市选择下拉菜单 → set_location() + 重置自动播报 ├── Button: "刷新天气" → backend.refresh() ├── Button: "语音播报" → 提取当前数据 → TTSSpeaker + 状态提示 └── Checkbutton: "自动播报" → 首次加载 / 刷新后自动播报 ``` **设计要点**: - 窗口固定 800×480,不可 resize,匹配嵌入式显示器分辨率 - **按钮栏使用 `pack(side="bottom")` 固定在窗口底部,不会被上方内容挤出可视区** - 字体和间距均收缩(温度 72→56px,卡片 95→78px 等),确保 480px 高度内所有元素可见 - 使用 `after(0, callback)` 将网络线程回调调度到 tkinter 主线程,保证线程安全 - 预报卡片每次重建(先 `destroy()` 再创建),确保与数据严格同步 - 颜色主题集中定义在 `config.py`,便于统一调整 - **语音播报有视觉反馈**:按钮上方显示"正在播报: …"状态文字,5 秒后自动消失 - **自动播报**:首次成功加载天气数据后自动播报一次(`AUTO_SPEAK` 配置项控制,GUI 有复选框可开关) ### 4.7 Curses 终端界面 ``` 终端窗口 (自适应大小) ├── 标题栏: "RK3588 天气站" (反色) ├── 城市名 (加粗白色) ├── 温度 + emoji 行 (加粗黄色) ├── 天气描述 (青色) ├── 湿度 | 风速 (白色) ├── 更新时间 (暗色) ├── 分隔线 ├── "未来天气预报" 标题 ├── 预报卡片 (蓝底白字横向排列): │ ├── 日期 MM-DD │ ├── 天气 (黄色) │ └── 最高/最低温 ├── 状态提示行 (黄色) ← 播报/错误提示,超时自动消失 └── 底部按键栏 (反色): " [R]刷新天气 [S]语音播报 [A]自动播报 [Q]退出 " ``` **键盘快捷键**: | 按键 | 功能 | |---|---| | `R` | 刷新天气数据 | | `S` | 语音播报当前天气 | | `A` | 开关自动播报(状态提示 2 秒) | | `C` | 切换到下一个城市(循环) | | `Q` / `ESC` | 退出程序 | **设计要点**: - 线程安全:网络回调通过 `queue.Queue` → 主循环 `_drain_queue()` 取出更新 → `_draw()` 绘制 - `_drain_queue()` 中检测首次数据到达 + `_auto_speak` 开关,自动触发语音播报 - 使用 `curses.use_default_colors()` 保持终端默认底色 - `stdscr.nodelay(True)` 非阻塞输入,`time.sleep(0.1)` 防止 CPU 空转 - 状态提示带过期时间(`_status_until`),到期自动清除 ### 4.8 TTSSpeaker 接口(多引擎架构) ```python # tts_speaker.py # 内部引擎基类 class _TTSEngine: def speak(self, text: str) -> bool: ... @property def name(self) -> str: ... # Edge TTS —— 微软免费高音质引擎(主力) class EdgeTTSEngine(_TTSEngine): """ 步骤: 1. edge-tts --voice zh-CN-XiaoxiaoNeural --text "..." --write-media /tmp/tts.mp3 2. paplay /tmp/tts.mp3 (后台非阻塞) 3. 延迟清理临时文件 (按文本长度估算播放时长) """ name = "Edge TTS (zh-CN-XiaoxiaoNeural)" # espeak —— 离线回退方案 class EspeakEngine(_TTSEngine): """espeak-ng 优先,espeak 回退""" def speak(self, text): subprocess.Popen([bin, "-v", "zh", text], ...) # 引擎自动选择 def _select_engine() -> _TTSEngine: """Edge TTS (需 edge-tts + paplay/ffplay/mpv) → espeak-ng → espeak → 不可用""" # 公开接口 class TTSSpeaker: @staticmethod def speak_current(weather: CurrentWeather): """拼接中文语句并调用当前最优引擎播报""" @staticmethod def is_available() -> bool: ... @staticmethod def engine_name() -> str: """返回当前引擎名称,如 "Edge TTS (zh-CN-XiaoxiaoNeural)" """ ``` **引擎优先级与对比**: | 引擎 | 音质 | 联网 | 费用 | 依赖 | |---|---|---|---|---| | **Edge TTS** | 极好,自然流畅 | 需要 | 免费 | `pip install edge-tts` + paplay/ffplay | | espeak-ng | 机械僵硬 | 离线 | 免费 | `apt install espeak-ng` | | espeak | 机械僵硬 | 离线 | 免费 | `apt install espeak` | **设计要点**: - **自动选择最优引擎**:启动时按 Edge TTS → espeak-ng → espeak 顺序探测,取第一个可用的 - Edge TTS 合成到临时 MP3 → 播放 → 延迟清理文件(`threading.Timer`,按文本长度估算) - 语音播报全程非阻塞:`subprocess.Popen` 后台播放,Edge TTS 合成用 `subprocess.run` 等待但时长很短 - Edge TTS 失败自动回退(如无网络),下次仍会重试 与原 C++ 版的差异: | C++ | Python | |---|---| | `system("espeak ... &")` | 多引擎架构,Edge TTS 为主力 | | 仅 espeak | Edge TTS (自然语音) + espeak-ng + espeak 三级回退 | | 无临时文件 | Edge TTS 需临时 MP3,自动清理 | ### 4.9 WeatherIcon 逻辑 ```python # weather_icon.py def get_weather_emoji(weather_text: str) -> str: """根据天气文字返回对应 emoji""" if "晴" in weather_text: return "☀️" if "多云" in weather_text: return "⛅" if "阴" in weather_text: return "☁️" if "雨" in weather_text and "雷" in weather_text: return "⛈️" if "雨" in weather_text: return "🌧️" if "雪" in weather_text: return "❄️" if "风" in weather_text: return "💨" if "雾" in weather_text or "霾" in weather_text: return "🌫️" return "🌤️" # 默认 ``` 匹配顺序:先精确匹配"多云",再处理组合条件(雷+雨),最后是单字匹配。检查"雷"需同时包含"雨"。 ### 4.10 MqttPublisher 接口 ```python # mqtt_client.py class MqttPublisher: def __init__(self, broker: str, port: int, client_id: str): """ 创建 MQTT 客户端并连接 Broker。 - 设置遗嘱消息: weather/status = "offline" (retained) - 启动 paho-mqtt 后台网络循环 (loop_start) - 连接失败不抛异常,仅打日志,上报功能降级 """ def publish_weather(self, current: dict, forecast: list[dict]): """ 发布天气数据到 MQTT: - weather/current ← 当前天气 JSON (retained) - weather/forecast ← 3日预报 JSON (retained) 未连接时静默跳过 """ def is_connected(self) -> bool: """返回当前连接状态""" def stop(self): """发布 offline 状态并断开连接""" ``` **MQTT 主题定义**: | 主题 | 内容 | QoS | retained | 说明 | |---|---|---|---|---| | `weather/current` | `{"city":"苏州","text":"多云","temp":25.0,...}` | 0 | 是 | 新订阅者立即可得最新数据 | | `weather/forecast` | `[{"date":"2026-05-14",...}, ...]` | 0 | 是 | 3日预报数组 | | `weather/status` | `"online"` / `"offline"` | 0 | 是 | 遗嘱自动切换 offline | **设计要点**: - 使用 `paho-mqtt` 2.x,通过 `CallbackAPIVersion.VERSION1` 保持回调兼容(抑制 DeprecationWarning) - Retained 消息确保手机 App 新订阅时立即可得最新天气,无需等待下一次刷新周期 - 遗嘱消息(`will_set`):进程异常崩溃时 Broker 自动发布 `offline`,手机端可感知设备离线 - MQTT 连接失败不影响核心天气显示功能(降级而非崩溃) - 默认 Broker: `broker.emqx.io:1883`(免费公共 Broker,生产环境改为私有) **手机端订阅示例**: ```bash # 命令行测试 mosquitto_sub -h broker.emqx.io -t "weather/current" -t "weather/forecast" -t "weather/status" -v # 手机 App: MQTT Dash / IoT MQTT Panel # 连接 broker.emqx.io:1883,订阅以上 3 个主题 ``` ### 4.11 入口与命令行参数 ```python # main.py 用法: python3 main.py # 自动检测(有 DISPLAY + tkinter → GUI,否则 → curses) python3 main.py --tui # 强制使用终端 curses 模式 python3 main.py --gui # 强制使用 tkinter GUI 模式 python3 main.py --no-mqtt # 禁用 MQTT 上报 ``` **启动流程**: 1. 解析命令行参数,确定运行模式 2. 日志初始化(`logging.basicConfig`,格式:`HH:MM:SS [LEVEL] message`) 3. MQTT 初始化(除非 `--no-mqtt`):创建 `MqttPublisher` 实例 4. 创建 UI 实例(`WeatherApp` 或 `CursesApp`) 5. 将 MQTT 回调注册到 `WeatherBackend.on_update()` 6. 进入 UI 主循环 **自动检测逻辑**: ```python def _can_use_gui() -> bool: """检查 $DISPLAY 是否存在且 tkinter 是否可导入""" if not os.environ.get("DISPLAY"): return False try: import tkinter return True except ImportError: return False ``` ### 4.12 配置与常量 ```python # config.py # ── 和风天气 API ── API_HOST = "mr5egn8gqa.re.qweatherapi.com" API_KEY = "02b6b7e586274e6f85dc1e1c1490616e" NOW_API_URL = f"https://{API_HOST}/v7/weather/now" FORECAST_API_URL = f"https://{API_HOST}/v7/weather/3d" DEFAULT_LOCATION = "101190401" # 苏州 # ── 请求超时 ── REQUEST_TIMEOUT = 10 # 秒 # ── 自动刷新 ── AUTO_REFRESH_MS = 30 * 60 * 1000 # 30 分钟 # ── 自动语音播报 ── AUTO_SPEAK = True # 首次成功加载数据后自动播报 # ── 窗口 ── WINDOW_WIDTH = 800 WINDOW_HEIGHT = 480 # ── 主题色 ── COLOR_BG = "#1a1a2e" # 主背景 COLOR_CARD_BG = "#2a2a4e" # 卡片背景 COLOR_TEXT_PRIMARY = "#ffffff" COLOR_TEXT_SECONDARY = "#cccccc" COLOR_TEXT_MUTED = "#999999" COLOR_TEXT_DIM = "#666666" COLOR_ACCENT = "#ffd700" # 强调色(温度、预报天气) COLOR_SECTION_TITLE = "#888888" # ── MQTT ── MQTT_BROKER = "broker.emqx.io" MQTT_PORT = 1883 MQTT_CLIENT_ID = "rk3588-weather-station" MQTT_TOPIC_CURRENT = "weather/current" MQTT_TOPIC_FORECAST = "weather/forecast" MQTT_TOPIC_STATUS = "weather/status" # ── 城市映射表 ── CITY_NAMES = { "101010100": "北京", "101020100": "上海", "101280101": "广州", "101280601": "深圳", "101210101": "杭州", "101190101": "南京", "101190401": "苏州", "101200101": "武汉", "101220101": "合肥", "101230101": "福州", "101110101": "西安", "101270101": "成都", "101040100": "重庆", } def city_name_for(location_id: str) -> str: return CITY_NAMES.get(location_id, location_id) ``` --- ## 5. 环境搭建 ### 5.1 依赖项 | 依赖 | 用途 | 安装 | |---|---|---| | Python 3.8+ | 运行环境 | 系统自带或 `apt install python3` | | python3-tk | tkinter GUI(仅 GUI 模式需要) | `apt install python3-tk` | | requests | HTTP 请求 | `pip install requests` | | paho-mqtt | MQTT 上报 | `pip install paho-mqtt` | | edge-tts | Edge TTS 语音合成(主力,自然流畅) | `pip install edge-tts` | | espeak-ng | 语音合成(离线回退方案) | `apt install espeak-ng` | | espeak | 语音合成(最后回退) | `apt install espeak` | ### 5.2 运行 ```bash # 安装 Python 依赖 cd weather_python pip install -r requirements.txt # GUI 模式(有显示器) DISPLAY=:0 python3 main.py # 终端模式(无显示器 / SSH) python3 main.py --tui # 禁用 MQTT 上报 python3 main.py --no-mqtt # 无 X 环境测试 GUI xvfb-run -s "-screen 0 800x480x24" python3 main.py ``` ### 5.3 构建配置要点 - Python 版本:3.8+(使用 `from __future__ import annotations` 兼容 3.8 的类型注解语法) - GUI:tkinter(Python 标准库,`apt install python3-tk` 安装) - TUI:curses(Python 标准库,无需额外安装) - 线程:`threading.Thread(daemon=True)`,不阻塞 UI,进程退出时自动回收 - UI 线程安全: - GUI: 网络回调通过 `root.after(0, callback)` 调度到 tkinter 主线程 - curses: 网络回调通过 `queue.Queue` → 主循环 `_drain_queue()` 取出 - MQTT 线程安全:paho-mqtt 的 `loop_start()` 在内部线程运行,`publish()` 线程安全 - TTS 非阻塞:`subprocess.Popen` 后台运行,不等待进程结束 - 所有网络/MQTT 异常均不崩溃,降级处理 --- ## 6. 开机自启(Linux) ```ini # weather.service [Unit] Description=RK3588 Weather Station Display (Python) After=network-online.target graphical.target Wants=network-online.target [Service] Type=simple User=root WorkingDirectory=/home/ubuntu/weather_python ExecStart=/usr/bin/python3 /home/ubuntu/weather_python/main.py Environment=DISPLAY=:0 Restart=always RestartSec=10 [Install] WantedBy=multi-user.target ``` 启用: ```bash sudo cp weather.service /etc/systemd/system/ sudo systemctl enable weather.service sudo systemctl start weather.service ``` --- ## 7. 关键技术决策 | 决策点 | 选择 | 原因 | |---|---|---| | 语言 | Python 3.8+ | 无需编译,开发效率高,RK3588 自带 Python | | HTTP 库 | requests | Python 生态最成熟的 HTTP 库,API 简洁,支持 gzip | | JSON 库 | json(标准库) | 无需额外依赖,性能足够 | | GUI 框架 | tkinter | Python 标准库自带,无需安装 Qt;800×480 界面足够 | | TUI 框架 | curses | Python 标准库自带,无需 X 显示,SSH 可用 | | 运行模式自动检测 | 检查 `$DISPLAY` + tkinter 可用性 | SSH 和桌面环境自动适配,无需手动切换 | | 线程模型 | `threading.Thread(daemon=True)` | 网络请求不阻塞 UI,进程退出时自动回收,语义等价于原 C++ 的 `std::thread::detach()` | | GUI 线程安全 | `root.after(0, fn)` 调度到主线程 | tkinter 非线程安全,所有 UI 操作必须在主线程执行 | | curses 线程安全 | `queue.Queue` + 主循环轮询 | curses 非线程安全,通过队列解耦网络线程和绘制线程 | | 多回调分发 | `WeatherBackend.on_update()` 列表 | 支持 UI、MQTT 等多个消费者同时接收天气更新,各自独立 | | 语音方案 | 多引擎:Edge TTS 主力,espeak-ng/espeak 回退 | Edge TTS 免费且音质极好(微软神经网络语音);离线时自动回退本地引擎 | | 城市切换 | OptionMenu 下拉 / `C` 键循环 | 内置 13 城市映射,切换后清缓存并立即刷新,自动播报重置 | | 自动播报 | 首次加载数据后自动播报,可开关 | 嵌入式屏幕常亮,用户靠近即可听到,无需手动操作 | | 语音反馈 | 界面显示播报内容预览 | 用户可确认播报已触发及具体内容,无需依赖听到声音 | | MQTT | paho-mqtt + retained 消息 + 遗嘱消息 | 手机端新订阅立即可得最新数据;设备离线自动感知 | | MQTT 降级 | 连接失败不崩溃,仅打日志 | MQTT 是辅助功能,不应影响核心天气显示 | | 城市名获取 | Location ID 静态映射表 | API 不返回中文城市名,映射最可靠 | | UI 主题 | 纯代码绘制的暗色主题 | tkinter 无 QML 级样式系统,通过 `bg`/`fg` 逐组件配色实现 | | 类型注解 | `from __future__ import annotations` | 兼容 Python 3.8,同时使用 `list[X]` / `dict[K, V]` / `X \| Y` 现代语法 | --- ## 8. 调试指南 | 问题 | 检查方法 | |---|---| | 网络不通 | `curl -I https://mr5egn8gqa.re.qweatherapi.com` | | API 返回非 200 | 检查 `config.py` 中 API_KEY 是否有效、Location ID 格式是否正确 | | 无 GUI 显示 | `echo $DISPLAY`,确认 `python3-tk` 已安装,尝试 `DISPLAY=:0` | | tkinter 报错 | `python3 -c "import tkinter"` 验证 tkinter 可用 | | curses 无法启动 | 确认在真实终端中运行(非 IDE 内嵌终端),`echo $TERM` 非空 | | 语音无声音 | `edge-tts --voice zh-CN-XiaoxiaoNeural --text "测试" --write-media /tmp/t.mp3 && paplay /tmp/t.mp3` 检查 Edge TTS;`espeak-ng -v zh "测试"` 检查回退引擎 | | Edge TTS 不可用 | 检查 `edge-tts` 是否已安装:`pip install edge-tts`;检查网络是否可达微软 TTS 服务 | | 语音播报无反馈 | 界面应显示"正在播报: …"状态文字。如无,检查 TTS 引擎是否安装 | | 自动播报未触发 | 检查 `config.py` 中 `AUTO_SPEAK = True`,GUI 确认"自动播报"复选框已勾选 | | 按钮被遮挡 | GUI 按钮栏使用 `pack(side="bottom")` 固定在底部,不应被遮挡。如仍看不到,检查分辨率是否为 800×480 | | MQTT 未连接 | `mosquitto_sub -h broker.emqx.io -t "weather/status" -C 1` 检查 Broker 可达性 | | MQTT 消息不更新 | 检查 `--no-mqtt` 是否传入;查看日志有无 "MQTT 连接失败" | | UI 卡顿 | 检查网络回调是否通过 `after(0, ...)` 或 Queue 调度到主线程 | | requests SSL 错误 | `config.py` 中可临时设置 `verify=False`(生产环境应配置证书) | | 程序启动报错 | `python3 main.py --tui --no-mqtt` 最小化启动,排除网络/MQTT 干扰 | --- ## 9. 与原 C++ 版的对照 | 维度 | C++ 原版 | Python 版 | |---|---|---| | **语言** | C++17 | Python 3.8+ | | **GUI 框架** | Qt 5.14+ / QML | tkinter(标准库)+ curses TUI(终端) | | **运行模式** | 仅 GUI | GUI / 终端自动检测 | | **HTTP** | libcurl,每请求独立 CURL* | requests,每请求独立线程 | | **JSON** | nlohmann/json 单头文件 | json 标准库 | | **TTS** | espeak + system("... &") | 多引擎:Edge TTS(主力)+ espeak-ng/espeak(回退) | | **TTS 反馈** | 无 | 界面状态文字预览播报内容 + 引擎名称 | | **城市切换** | 硬编码单城市 | GUI 下拉 + curses `C` 键,13 城市循环 | | **MQTT** | 无 | paho-mqtt,retained + 遗嘱消息,手机端可订阅 | | **线程** | std::thread::detach() | threading.Thread(daemon=True) | | **UI 更新** | Qt signal/slot 自动线程安全 | GUI: root.after(0, fn);curses: queue.Queue | | **回调分发** | 单 signal → 多 slot | 多回调列表模式 | | **构建** | cmake + g++ | 无需编译,直接运行 | | **依赖安装** | apt install qt6-* libcurl-* nlohmann-* espeak | apt install python3-tk espeak-ng + pip install requests paho-mqtt | | **可执行文件** | build/WeatherStation(编译产物) | main.py(源码直接执行) | | **跨平台** | Qt5/Qt6 双兼容 CMake | Python 跨平台,Windows 需额外处理 espeak/TTS 降级 | --- ## 10. 扩展方向 - **空气质量 AQI**:增加和风天气 `/v7/air/now` 接口,界面增加 AQI 显示行 - **摄像头实景**:OpenCV 采集 V4L2,GUI 角落嵌入实时画面(tkinter + PIL) - **MQTT 增强**:支持 TLS 加密连接、自定义 Broker 认证、多城市数据上报 - **屏幕保护**:长时间无操作降低亮度或休眠背光(需硬件支持) - ~~多城市切换~~ **(已实现)**:GUI 下拉菜单 + curses `C` 键循环切换,13 城市内置 - **日志记录**:增加 `logging` 模块,记录 API 调用失败和异常堆栈(已部分实现) - **配置热加载**:通过 MQTT 订阅配置主题,远程修改 API Key / 城市 / 刷新间隔