From b068b9cf49129e404b23fa2cb11ff37dad362cc8 Mon Sep 17 00:00:00 2001 From: kicer Date: Sun, 1 Feb 2026 18:52:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E7=A1=80=E7=89=88=E6=9C=ACui=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rom/app.py | 94 ++++--- src/rom/www/index.html | 549 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 614 insertions(+), 29 deletions(-) create mode 100644 src/rom/www/index.html diff --git a/src/rom/app.py b/src/rom/app.py index f1b0048..01f9de6 100644 --- a/src/rom/app.py +++ b/src/rom/app.py @@ -1,20 +1,22 @@ # ESP8266天气站主程序 # 首先尝试连接已保存的WiFi,失败则启动CaptivePortal进行配置 +import asyncio import gc import json import sys import time import machine -import asyncio from config import config from display import display # 导入液晶屏管理模块 from wifi_manager import wifi_manager + def uuid(): return str(machine.unique_id().hex()) + def parse_url_params(url): # 解析URL中的查询参数,返回参数字典 params = {} @@ -30,18 +32,23 @@ def parse_url_params(url): return params + async def post_parse(request): if request.method != "POST": raise Exception("invalid request") content_length = int(request.headers["Content-Length"]) return (await request.read(content_length)).decode() + async def json_response(request, data): await request.write("HTTP/1.1 200 OK\r\n") await request.write("Content-Type: application/json; charset=utf-8\r\n\r\n") await request.write(data) -CREDENTIALS = ('admin', config.get('web_password', 'admin')) + +CREDENTIALS = ("admin", config.get("web_password", "admin")) + + def authenticate(credentials): async def fail(request): await request.write("HTTP/1.1 401 Unauthorized\r\n") @@ -51,31 +58,37 @@ def authenticate(credentials): def decorator(func): async def wrapper(request): from ubinascii import a2b_base64 as base64_decode - header = request.headers.get('Authorization', None) + + header = request.headers.get("Authorization", None) if header is None: return await fail(request) # Authorization: Basic XXX - kind, authorization = header.strip().split(' ', 1) + kind, authorization = header.strip().split(" ", 1) if kind != "Basic": return await fail(request) - authorization = base64_decode(authorization.strip()) \ - .decode('ascii') \ - .split(':') + authorization = ( + base64_decode(authorization.strip()).decode("ascii").split(":") + ) if list(credentials) != list(authorization): return await fail(request) return await func(request) + return wrapper + return decorator + # /status: 获取系统状态 @authenticate(credentials=CREDENTIALS) async def sys_status(request): - await json_response(request, - json.dumps({ + await json_response( + request, + json.dumps( + { "time": "{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format( *time.localtime() ), @@ -84,28 +97,35 @@ async def sys_status(request): "uuid": uuid(), "platform": str(sys.platform), "version": str(sys.version), - }) + } + ), ) + # /lcd: 获取LCD状态 @authenticate(credentials=CREDENTIALS) async def lcd_status(request): # 返回LCD状态 - await json_response(request, - json.dumps({ - "ready": display.is_ready(), - "brightness": display.brightness(), - "ui_type": display.ui_type, - "data": display.ui_data, - }) + await json_response( + request, + json.dumps( + { + "ready": display.is_ready(), + "brightness": display.brightness(), + "ui_type": display.ui_type, + "data": display.ui_data, + } + ), ) + # /config: 获取当前配置 @authenticate(credentials=CREDENTIALS) async def config_get(request): # 返回所有配置项 await json_response(request, json.dumps(config.config_data)) + # /lcd/set: 设置LCD状态 @authenticate(credentials=CREDENTIALS) async def lcd_set(request): @@ -122,6 +142,7 @@ async def lcd_set(request): finally: await json_response(request, json.dumps(ack)) + # /config/set: 更新配置 # curl -H "Content-Type: application/json" -X POST -d '{"city":"xxx","who":"ami"}' 'http:///config/set' @authenticate(credentials=CREDENTIALS) @@ -139,6 +160,7 @@ async def config_update(request): finally: await json_response(request, json.dumps(ack)) + # /exec: 执行命令并返回 # {"cmd":"import network;R=network.WLAN().config(\"mac\").hex()", "token":"xxx"} @authenticate(credentials=CREDENTIALS) @@ -159,6 +181,8 @@ async def eval_cmd(request): finally: await json_response(request, json.dumps(ack)) + + # ntp时钟同步 def sync_ntp_time(): import ntptime @@ -187,7 +211,7 @@ async def fetch_weather_data(city=None): print(f"正在获取{city}天气数据...") # 从配置获取API基础URL,默认使用官方API url = config.get("weather_api_url", "http://esp.tangofu.com/api/ws2/") - params = {'uuid':uuid(), 'city':city} + params = {"uuid": uuid(), "city": city} # 发送GET请求 async with aiohttp.ClientSession() as session: @@ -211,8 +235,15 @@ async def fetch_weather_data(city=None): ip = wifi_manager.get_ip() - display.update_ui(city, weather, advice, aqi, lunar, ip, - envdat={'t':t,'rh':rh,'co2':co2,'pm':pm,'ap':ap}) + display.update_ui( + city, + weather, + advice, + aqi, + lunar, + ip, + envdat={"t": t, "rh": rh, "co2": co2, "pm": pm, "ap": ap}, + ) else: print(f"获取天气数据失败,状态码: {response.status}") @@ -282,7 +313,7 @@ async def ui_task(): t0 = time.ticks_ms() # 计算当前帧号(1-10),更新动画 - cframe = (F % 10) + cframe = F % 10 pic = f"/rom/images/T{cframe}.jpg" display.show_jpg(pic, 160, 160) @@ -297,7 +328,7 @@ async def ui_task(): # 控制帧率 now = time.ticks_ms() ts = time.ticks_diff(now, t0) - _sT = (100-ts) if ts<90 else 10 + _sT = (100 - ts) if ts < 90 else 10 await asyncio.sleep_ms(_sT) except Exception as e: @@ -308,28 +339,32 @@ async def ui_task(): except Exception as e: print(f"动画任务初始化失败: {e}") + def cb_progress(data): if isinstance(data, bytes): - if data == b'/': - display.portal_info('load iwconfig page ') - elif data == b'/login': - display.portal_info('WiFi connecting ... ') + if data == b"/": + display.portal_info("load iwconfig page ") + elif data == b"/login": + display.portal_info("WiFi connecting ... ") elif isinstance(data, str): display.message(data, 19, 204) - if data: print(f'progress: {str(data)}') + if data: + print(f"progress: {str(data)}") + def start(): # 初始化液晶屏 - display.init_display(config.get("bl_mode")=="pwm", 7000) + display.init_display(config.get("bl_mode") == "pwm", 7000) display.brightness(int(config.get("brightness", 10))) cb_progress("WiFi connect ...") if not wifi_manager.connect(cb_progress): gc.collect() from captive_portal import CaptivePortal + portal = CaptivePortal() - display.portal_win(portal.essid.decode('ascii')) + display.portal_win(portal.essid.decode("ascii")) portal.start(cb_progress) # just reboot machine.reset() @@ -343,6 +378,7 @@ def start(): naw = Nanoweb() # website top directory naw.STATIC_DIR = "/rom/www" + naw.INDEX_FILE = "/rom/www/index.html" # Declare route from a dict naw.routes = { diff --git a/src/rom/www/index.html b/src/rom/www/index.html new file mode 100644 index 0000000..fcdc160 --- /dev/null +++ b/src/rom/www/index.html @@ -0,0 +1,549 @@ + + + + + + WS2桌面气象站 + + + + +
+
+

WS2桌面气象站

+ +
+ + + + + + + +
+
+
+
+ +

系统信息

+
+
+ 时间: - +
+
+ 运行时间: + - +
+
+ 可用内存: + - +
+
+ UUID: - +
+
+ 平台: + - +
+
+ 版本: + - +
+
+ + +
+ + +
+

LCD状态

+
+
+
+
+ 就绪状态: + - + +
+
+ 当前亮度: + -% +
+
+ UI类型: + - +
+
+ 数据内容: + - +
+
+
+
+ +

显示设置

+
+ + +
当前值: -
+
+ +
+ + +
+ + +
+ + +
+

天气站配置

+
+ + + + 可输入城市名称或城市ID(查看城市ID列表) + +
+ +
+ + + 留空表示不自动熄屏 +
+ + + +

当前配置

+ + + + + +
配置项
+
+ + +
+

关于

+

+ WS2是一款基于ESP8266的桌面气象站,能够实时显示天气信息、环境数据和时间。 +

+ +
+
+

硬件规格

+
+
+ 硬件平台: ESP8266 + WiFi +
+
+ 显示屏: LCD 240x240 + 彩色 +
+
+ 环境参数: 温度、湿度、PM2.5、气压、AQI +
+
+
+
+

软件信息

+
+
+ 固件: + ws2-v1.3.0 + 开源 +
+
+ 协议: HTTP REST API +
+
+ 更新频率: 每小时 +
+
+
+
+ +

开放源码

+ Kicer@Github: ws2 + Kicer@foresh: ws2(国内访问) + +

软件许可

+

本项目采用MIT许可证开源,欢迎自由使用和修改。

+
+
+ + + + +