v1.2.7
This commit is contained in:
2
app.yaml
2
app.yaml
@@ -1,6 +1,6 @@
|
|||||||
id: t11
|
id: t11
|
||||||
name: t11
|
name: t11
|
||||||
version: 1.2.3
|
version: 1.2.7
|
||||||
author: t11
|
author: t11
|
||||||
icon: ''
|
icon: ''
|
||||||
desc: t11
|
desc: t11
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ REG_CALIBRATION = 0x05
|
|||||||
CALIBRATION_VALUE = 0x1400
|
CALIBRATION_VALUE = 0x1400
|
||||||
|
|
||||||
# ==================== 空气传感器配置 ====================
|
# ==================== 空气传感器配置 ====================
|
||||||
ADC_TRIGGER_THRESHOLD = 50000 # TODO:只是用于测试,最终需要改为正常值
|
ADC_TRIGGER_THRESHOLD = 2500 # TODO:只是用于测试,最终需要改为正常值
|
||||||
AIR_PRESSURE_lOG = False # TODO: 在正式环境中关闭
|
AIR_PRESSURE_lOG = False # TODO: 在正式环境中关闭
|
||||||
|
|
||||||
# ADC配置
|
# ADC配置
|
||||||
|
|||||||
2
main.py
2
main.py
@@ -452,7 +452,7 @@ def cmd_str():
|
|||||||
logger = logger_manager.logger
|
logger = logger_manager.logger
|
||||||
if logger:
|
if logger:
|
||||||
logger.error(f"[MAIN] 显示异常: {e}")
|
logger.error(f"[MAIN] 显示异常: {e}")
|
||||||
time.sleep_ms(1)
|
time.sleep_ms(10)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 主循环的顶层异常捕获,防止程序静默退出
|
# 主循环的顶层异常捕获,防止程序静默退出
|
||||||
|
|||||||
391
network.py
391
network.py
@@ -225,56 +225,132 @@ class NetworkManager:
|
|||||||
self._wifi_connected = False
|
self._wifi_connected = False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def connect_wifi(self, ssid, password):
|
def connect_wifi(self, ssid, password, verify_host=None, verify_port=None, persist=True, timeout_s=20):
|
||||||
"""
|
"""
|
||||||
连接 Wi-Fi 并将凭证持久化保存到 /boot/ 目录
|
连接 Wi-Fi(先用新凭证尝试连接并验证可用性;失败自动回滚;成功后再决定是否落盘)
|
||||||
|
|
||||||
|
重要:系统的 /etc/init.d/S30wifi 通常会读取 /boot/wifi.ssid 与 /boot/wifi.pass 来连接 WiFi。
|
||||||
|
因此要“真正尝试连接新 WiFi”,必须临时写入 /boot/ 触发重启;若失败则把旧值写回去(回滚)。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(ip, error): IP地址和错误信息(成功时error为None)
|
(ip, error): IP地址和错误信息(成功时error为None)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 配置文件路径定义
|
# 配置文件路径定义
|
||||||
conf_path = "/etc/wpa_supplicant.conf" # wpa_supplicant配置文件路径
|
conf_path = "/etc/wpa_supplicant.conf"
|
||||||
ssid_file = "/boot/wifi.ssid" # 用于保存SSID的文件路径
|
ssid_file = "/boot/wifi.ssid"
|
||||||
pass_file = "/boot/wifi.pass" # 用于保存密码的文件路径
|
pass_file = "/boot/wifi.pass"
|
||||||
|
|
||||||
|
def _read_text(path: str):
|
||||||
|
try:
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _write_text(path: str, content: str):
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
def _restore_boot(old_ssid: str | None, old_pass: str | None):
|
||||||
|
# 还原 /boot 凭证:原来没有就删除,原来有就写回
|
||||||
|
try:
|
||||||
|
if old_ssid is None:
|
||||||
|
if os.path.exists(ssid_file):
|
||||||
|
os.remove(ssid_file)
|
||||||
|
else:
|
||||||
|
_write_text(ssid_file, old_ssid)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if old_pass is None:
|
||||||
|
if os.path.exists(pass_file):
|
||||||
|
os.remove(pass_file)
|
||||||
|
else:
|
||||||
|
_write_text(pass_file, old_pass)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
old_conf = _read_text(conf_path)
|
||||||
|
old_boot_ssid = _read_text(ssid_file)
|
||||||
|
old_boot_pass = _read_text(pass_file)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 生成 wpa_supplicant 配置
|
# 生成 wpa_supplicant 配置(写 /etc 作为辅助,具体是否生效取决于 S30wifi 脚本)
|
||||||
net_conf = os.popen(f'wpa_passphrase "{ssid}" "{password}"').read() # 调用系统命令生成配置
|
net_conf = os.popen(f'wpa_passphrase "{ssid}" "{password}"').read()
|
||||||
if "network={" not in net_conf: # 检查配置是否生成成功
|
if "network={" not in net_conf:
|
||||||
return None, "Failed to generate wpa config"
|
raise RuntimeError("Failed to generate wpa config")
|
||||||
|
|
||||||
# 写入运行时配置
|
try:
|
||||||
with open(conf_path, "w") as f: # 打开配置文件准备写入
|
_write_text(
|
||||||
f.write("ctrl_interface=/var/run/wpa_supplicant\n") # 设置控制接口路径
|
conf_path,
|
||||||
f.write("update_config=1\n\n") # 允许更新配置
|
"ctrl_interface=/var/run/wpa_supplicant\n"
|
||||||
f.write(net_conf) # 写入网络配置
|
"update_config=1\n\n"
|
||||||
|
+ net_conf,
|
||||||
# 持久化保存 SSID/PASS
|
)
|
||||||
with open(ssid_file, "w") as f: # 打开SSID文件准备写入
|
except Exception:
|
||||||
f.write(ssid.strip()) # 写入SSID(去除首尾空格)
|
# 不强制要求写 /etc 成功(某些系统只用 /boot)
|
||||||
with open(pass_file, "w") as f: # 打开密码文件准备写入
|
pass
|
||||||
f.write(password.strip()) # 写入密码(去除首尾空格)
|
|
||||||
|
# ====== 临时写入 /boot 凭证,触发 WiFi 服务真正尝试连接新 SSID ======
|
||||||
|
_write_text(ssid_file, ssid.strip())
|
||||||
|
_write_text(pass_file, password.strip())
|
||||||
|
|
||||||
# 重启 Wi-Fi 服务
|
# 重启 Wi-Fi 服务
|
||||||
os.system("/etc/init.d/S30wifi restart") # 执行WiFi服务重启命令
|
os.system("/etc/init.d/S30wifi restart")
|
||||||
|
|
||||||
# 等待获取 IP
|
# 等待获取 IP
|
||||||
import time as std_time # 导入time模块并重命名为std_time
|
import time as std_time
|
||||||
for _ in range(50): # 最多等待50秒
|
wait_s = int(timeout_s) if timeout_s and timeout_s > 0 else 20
|
||||||
ip = os.popen("ifconfig wlan0 2>/dev/null | grep 'inet ' | awk '{print $2}'").read().strip() # 获取wlan0的IP地址
|
wait_s = min(max(wait_s, 5), 60)
|
||||||
if ip: # 如果获取到IP地址
|
for _ in range(wait_s):
|
||||||
self._wifi_connected = True # 设置WiFi连接状态为已连接
|
ip = os.popen("ifconfig wlan0 2>/dev/null | grep 'inet ' | awk '{print $2}'").read().strip()
|
||||||
self._wifi_ip = ip # 保存IP地址
|
if ip:
|
||||||
self.logger.info(f"[WIFI] 已连接,IP: {ip}") # 记录连接成功日志
|
# 拿到 IP 不代表可上网/可访问目标;继续做可达性验证
|
||||||
|
self._wifi_connected = True
|
||||||
|
self._wifi_ip = ip
|
||||||
|
self.logger.info(f"[WIFI] 已连接,IP: {ip},开始验证网络可用性...")
|
||||||
|
|
||||||
|
# 验证能访问指定目标(默认使用 TCP 服务器)
|
||||||
|
v_host = verify_host if verify_host is not None else self._server_ip
|
||||||
|
v_port = int(verify_port) if verify_port is not None else int(self._server_port)
|
||||||
|
if v_host and v_port:
|
||||||
|
if not self.is_server_reachable(v_host, v_port, timeout=5):
|
||||||
|
raise RuntimeError(f"Target unreachable ({v_host}:{v_port})")
|
||||||
|
|
||||||
|
# ====== 验证通过 ======
|
||||||
|
if not persist:
|
||||||
|
# 不持久化:把 /boot 恢复成旧值(不重启,当前连接保持不变)
|
||||||
|
_restore_boot(old_boot_ssid, old_boot_pass)
|
||||||
|
self.logger.info("[WIFI] 网络验证通过,但按 persist=False 回滚 /boot 凭证(不重启)")
|
||||||
|
else:
|
||||||
|
self.logger.info("[WIFI] 网络验证通过,/boot 凭证已保留(持久化)")
|
||||||
|
|
||||||
return ip, None
|
return ip, None
|
||||||
std_time.sleep(1) # 每次循环等待1秒
|
|
||||||
|
std_time.sleep(1)
|
||||||
return None, "Timeout: No IP obtained" # 超时未获取到IP
|
|
||||||
|
raise RuntimeError("Timeout: No IP obtained")
|
||||||
except Exception as e: # 捕获所有异常
|
|
||||||
self.logger.error(f"[WIFI] 连接失败: {e}") # 记录错误日志
|
except Exception as e:
|
||||||
return None, f"Exception: {str(e)}" # 返回异常信息
|
# 失败:回滚 /boot 和 /etc,重启 WiFi 恢复旧网络
|
||||||
|
_restore_boot(old_boot_ssid, old_boot_pass)
|
||||||
|
try:
|
||||||
|
if old_conf is not None:
|
||||||
|
_write_text(conf_path, old_conf)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.system("/etc/init.d/S30wifi restart")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._wifi_connected = False
|
||||||
|
self._wifi_ip = None
|
||||||
|
self.logger.error(f"[WIFI] 连接/验证失败,已回滚: {e}")
|
||||||
|
return None, str(e)
|
||||||
|
|
||||||
def is_server_reachable(self, host, port=80, timeout=5):
|
def is_server_reachable(self, host, port=80, timeout=5):
|
||||||
"""检查目标主机端口是否可达(用于网络检测)"""
|
"""检查目标主机端口是否可达(用于网络检测)"""
|
||||||
@@ -480,6 +556,43 @@ class NetworkManager:
|
|||||||
err = getattr(e, "errno", None)
|
err = getattr(e, "errno", None)
|
||||||
if err in (11, 35, 10035): # EAGAIN/EWOULDBLOCK on linux/mac/win
|
if err in (11, 35, 10035): # EAGAIN/EWOULDBLOCK on linux/mac/win
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# 某些平台会把“无数据可读/超时”抛成 socket.timeout / TimeoutError,errno 可能为 None,
|
||||||
|
# 这不代表断线:视为 benign,交给真正的 send/recv 去判定断线。
|
||||||
|
if (err is None) and (("timed out" in str(e).lower()) or isinstance(e, (TimeoutError, socket.timeout))):
|
||||||
|
try:
|
||||||
|
self.logger.warning(f"[WIFI-TCP] conncheck timeout treated as benign: {e}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 某些嵌入式 socket 实现可能不支持 MSG_PEEK/MSG_DONTWAIT,或返回 EINVAL/ENOTSUP。
|
||||||
|
# 这种情况不代表断线:选择“无法检测但不判死”,交给真正的 send/recv 去触发断线处理。
|
||||||
|
# 常见:EINVAL(22), ENOTSUP(95), EOPNOTSUPP(95), EINTR(4)
|
||||||
|
if err in (4, 22, 95):
|
||||||
|
try:
|
||||||
|
self.logger.warning(f"[WIFI-TCP] conncheck unsupported/benign errno={err}: {e}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 记录真实错误,便于定位
|
||||||
|
try:
|
||||||
|
self.logger.error(f"[WIFI-TCP] conncheck failed errno={err}: {e}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 明确的“连接不可用”错误才判定断线并清理
|
||||||
|
# 常见:ENOTCONN(107), ECONNRESET(104), EPIPE(32), EBADF(9)
|
||||||
|
if err in (9, 32, 104, 107, 10054, 10057):
|
||||||
|
try:
|
||||||
|
self._wifi_socket.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self._wifi_socket = None
|
||||||
|
self._tcp_connected = False
|
||||||
|
return False
|
||||||
|
|
||||||
# socket已断开或不可用,清理
|
# socket已断开或不可用,清理
|
||||||
try:
|
try:
|
||||||
self._wifi_socket.close()
|
self._wifi_socket.close()
|
||||||
@@ -544,6 +657,7 @@ class NetworkManager:
|
|||||||
if self._network_type == "wifi":
|
if self._network_type == "wifi":
|
||||||
# 先快速校验 WiFi socket 是否仍有效,避免卡在半开连接上
|
# 先快速校验 WiFi socket 是否仍有效,避免卡在半开连接上
|
||||||
if not self._check_wifi_connection():
|
if not self._check_wifi_connection():
|
||||||
|
print("_check_wifi_connection failed")
|
||||||
return False
|
return False
|
||||||
return self._tcp_send_raw_via_wifi(data, max_retries)
|
return self._tcp_send_raw_via_wifi(data, max_retries)
|
||||||
elif self._network_type == "4g":
|
elif self._network_type == "4g":
|
||||||
@@ -686,15 +800,13 @@ class NetworkManager:
|
|||||||
return b""
|
return b""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 设置接收超时
|
# 这里保持 socket 为非阻塞模式(连接时已 setblocking(False))。
|
||||||
self._wifi_socket.settimeout(timeout_ms / 1000.0)
|
# 不要反复 settimeout(),否则会把 socket 切回“阻塞+超时”,并导致 conncheck 误报 timed out。
|
||||||
|
data = self._wifi_socket.recv(4096) # 每次最多接收4KB(无数据会抛 BlockingIOError)
|
||||||
# 尝试接收数据
|
|
||||||
data = self._wifi_socket.recv(4096) # 每次最多接收4KB
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except socket.timeout:
|
except BlockingIOError:
|
||||||
# 超时是正常的,表示没有数据
|
# 无数据可读是正常的
|
||||||
return b""
|
return b""
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
# socket错误(连接断开等)
|
# socket错误(连接断开等)
|
||||||
@@ -713,13 +825,16 @@ class NetworkManager:
|
|||||||
self.logger.error(f"[WIFI-TCP] 接收数据异常: {e}")
|
self.logger.error(f"[WIFI-TCP] 接收数据异常: {e}")
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
def _upload_log_file(self, upload_url, wifi_ssid=None, wifi_password=None):
|
def _upload_log_file(self, upload_url, wifi_ssid=None, wifi_password=None, include_rotated=True, max_files=None, archive_format="tgz"):
|
||||||
"""上传日志文件到指定URL
|
"""上传日志文件到指定URL
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
upload_url: 上传目标URL,例如 "https://example.com/upload/"
|
upload_url: 上传目标URL,例如 "https://example.com/upload/"
|
||||||
wifi_ssid: WiFi SSID(可选,如果未连接WiFi则尝试连接)
|
wifi_ssid: WiFi SSID(可选,如果未连接WiFi则尝试连接)
|
||||||
wifi_password: WiFi 密码(可选)
|
wifi_password: WiFi 密码(可选)
|
||||||
|
include_rotated: 是否包含轮转日志(app.log.1 等)
|
||||||
|
max_files: 最多打包多少个日志文件(包含 app.log 本身),None=按 backupCount 自动推断
|
||||||
|
archive_format: 打包格式:tgz 或 zip
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
该功能仅在 WiFi 连接时可用,4G 网络暂不支持文件上传
|
该功能仅在 WiFi 连接时可用,4G 网络暂不支持文件上传
|
||||||
@@ -727,6 +842,7 @@ class NetworkManager:
|
|||||||
import requests
|
import requests
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import glob
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 检查 WiFi 连接状态,如果未连接则尝试连接
|
# 检查 WiFi 连接状态,如果未连接则尝试连接
|
||||||
@@ -735,7 +851,22 @@ class NetworkManager:
|
|||||||
self.logger.info(f"[LOG_UPLOAD] WiFi 未连接,尝试连接 WiFi: {wifi_ssid}")
|
self.logger.info(f"[LOG_UPLOAD] WiFi 未连接,尝试连接 WiFi: {wifi_ssid}")
|
||||||
self.safe_enqueue({"result": "log_upload_connecting_wifi", "ssid": wifi_ssid}, 2)
|
self.safe_enqueue({"result": "log_upload_connecting_wifi", "ssid": wifi_ssid}, 2)
|
||||||
|
|
||||||
ip, error = self.connect_wifi(wifi_ssid, wifi_password)
|
# 连接前先把“目标上传 URL”作为可达性验证目标,只有验证通过才落盘保存 SSID/PASS
|
||||||
|
try:
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
parsed = urlparse(upload_url)
|
||||||
|
v_host = parsed.hostname
|
||||||
|
v_port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
||||||
|
except Exception:
|
||||||
|
v_host, v_port = None, None
|
||||||
|
|
||||||
|
ip, error = self.connect_wifi(
|
||||||
|
wifi_ssid,
|
||||||
|
wifi_password,
|
||||||
|
verify_host=v_host,
|
||||||
|
verify_port=v_port,
|
||||||
|
persist=True,
|
||||||
|
)
|
||||||
if error:
|
if error:
|
||||||
self.logger.error(f"[LOG_UPLOAD] WiFi 连接失败: {error}")
|
self.logger.error(f"[LOG_UPLOAD] WiFi 连接失败: {error}")
|
||||||
self.safe_enqueue({
|
self.safe_enqueue({
|
||||||
@@ -767,26 +898,116 @@ class NetworkManager:
|
|||||||
self.safe_enqueue({"result": "log_upload_failed", "reason": "log_file_not_found"}, 2)
|
self.safe_enqueue({"result": "log_upload_failed", "reason": "log_file_not_found"}, 2)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 生成带时间戳的文件名
|
# 生成带时间戳的文件名(归档)
|
||||||
# 格式: app_20260131_143025_d19566161359c372.log
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
device_id = self._device_id or "unknown"
|
device_id = self._device_id or "unknown"
|
||||||
new_filename = f"app_{timestamp}_{device_id}.log"
|
base_name = f"logs_{timestamp}_{device_id}"
|
||||||
|
archive_format = (archive_format or "tgz").strip().lower()
|
||||||
# 创建临时文件(带时间戳的名字)
|
if archive_format not in ("tgz", "zip"):
|
||||||
temp_dir = "/tmp"
|
archive_format = "tgz"
|
||||||
temp_file_path = os.path.join(temp_dir, new_filename)
|
|
||||||
|
# 选择要打包的日志文件
|
||||||
# 先 sync,确保所有日志都已写入磁盘
|
candidates = [log_file_path]
|
||||||
|
if include_rotated:
|
||||||
|
candidates = sorted(set(glob.glob(log_file_path + "*")))
|
||||||
|
candidates = [p for p in candidates if os.path.isfile(p)]
|
||||||
|
|
||||||
|
# app.log 优先,其余按“数字后缀”排序(app.log.1, app.log.2...)
|
||||||
|
def _log_sort_key(p: str):
|
||||||
|
if p == log_file_path:
|
||||||
|
return (0, 0, p)
|
||||||
|
suffix = p[len(log_file_path):]
|
||||||
|
if suffix.startswith("."):
|
||||||
|
try:
|
||||||
|
return (1, int(suffix[1:]), p)
|
||||||
|
except:
|
||||||
|
return (2, 999999, p)
|
||||||
|
return (3, 999999, p)
|
||||||
|
|
||||||
|
candidates.sort(key=_log_sort_key)
|
||||||
|
|
||||||
|
# 限制最大文件数(默认:app.log + 轮转数量)
|
||||||
|
if max_files is None:
|
||||||
|
try:
|
||||||
|
max_files = 1 + int(getattr(config, "LOG_BACKUP_COUNT", 5))
|
||||||
|
except:
|
||||||
|
max_files = 6
|
||||||
|
try:
|
||||||
|
max_files = int(max_files)
|
||||||
|
except:
|
||||||
|
max_files = 6
|
||||||
|
max_files = max(1, min(max_files, 20))
|
||||||
|
selected = candidates[:max_files]
|
||||||
|
|
||||||
|
if not selected:
|
||||||
|
self.logger.error("[LOG_UPLOAD] 未找到可打包的日志文件")
|
||||||
|
self.safe_enqueue({"result": "log_upload_failed", "reason": "no_log_files"}, 2)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.info(f"[LOG_UPLOAD] 将打包日志文件数: {len(selected)}")
|
||||||
|
|
||||||
|
# 先 sync,尽量确保日志落盘;再复制快照到 /tmp,避免打包过程中日志被追加导致内容不一致
|
||||||
os.system("sync")
|
os.system("sync")
|
||||||
|
temp_dir = "/tmp"
|
||||||
# 复制日志文件到临时位置
|
staging_dir = os.path.join(temp_dir, f"log_upload_{base_name}")
|
||||||
shutil.copy2(log_file_path, temp_file_path)
|
os.makedirs(staging_dir, exist_ok=True)
|
||||||
self.logger.info(f"[LOG_UPLOAD] 日志文件已复制到: {temp_file_path}")
|
staged_paths = []
|
||||||
|
try:
|
||||||
# 使用 multipart/form-data 上传文件
|
for p in selected:
|
||||||
with open(temp_file_path, 'rb') as f:
|
dst = os.path.join(staging_dir, os.path.basename(p))
|
||||||
files = {'file': (new_filename, f, 'text/plain')}
|
shutil.copy2(p, dst)
|
||||||
|
staged_paths.append(dst)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"[LOG_UPLOAD] 复制日志快照失败: {e}")
|
||||||
|
self.safe_enqueue({"result": "log_upload_failed", "reason": "snapshot_failed", "detail": str(e)[:100]}, 2)
|
||||||
|
try:
|
||||||
|
shutil.rmtree(staging_dir)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
# 打包压缩
|
||||||
|
if archive_format == "zip":
|
||||||
|
archive_filename = f"{base_name}.zip"
|
||||||
|
else:
|
||||||
|
archive_filename = f"{base_name}.tar.gz"
|
||||||
|
archive_path = os.path.join(temp_dir, archive_filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if archive_format == "zip":
|
||||||
|
import zipfile
|
||||||
|
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for p in staged_paths:
|
||||||
|
zf.write(p, arcname=os.path.basename(p))
|
||||||
|
else:
|
||||||
|
import tarfile
|
||||||
|
with tarfile.open(archive_path, "w:gz") as tf:
|
||||||
|
for p in staged_paths:
|
||||||
|
tf.add(p, arcname=os.path.basename(p))
|
||||||
|
self.logger.info(f"[LOG_UPLOAD] 日志压缩包已生成: {archive_path}")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"[LOG_UPLOAD] 打包压缩失败: {e}")
|
||||||
|
self.safe_enqueue({"result": "log_upload_failed", "reason": "archive_failed", "detail": str(e)[:100]}, 2)
|
||||||
|
try:
|
||||||
|
shutil.rmtree(staging_dir)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if os.path.exists(archive_path):
|
||||||
|
os.remove(archive_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
shutil.rmtree(staging_dir)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 使用 multipart/form-data 上传压缩包
|
||||||
|
with open(archive_path, 'rb') as f:
|
||||||
|
mime = "application/gzip" if archive_format == "tgz" else "application/zip"
|
||||||
|
files = {'file': (archive_filename, f, mime)}
|
||||||
|
|
||||||
# 添加额外的头部信息
|
# 添加额外的头部信息
|
||||||
headers = {
|
headers = {
|
||||||
@@ -811,7 +1032,7 @@ class NetworkManager:
|
|||||||
self.logger.info(f"[LOG_UPLOAD] 上传成功! 状态码: {response.status_code}")
|
self.logger.info(f"[LOG_UPLOAD] 上传成功! 状态码: {response.status_code}")
|
||||||
self.safe_enqueue({
|
self.safe_enqueue({
|
||||||
"result": "log_upload_ok",
|
"result": "log_upload_ok",
|
||||||
"filename": new_filename,
|
"filename": archive_filename,
|
||||||
"status_code": response.status_code
|
"status_code": response.status_code
|
||||||
}, 2)
|
}, 2)
|
||||||
else:
|
else:
|
||||||
@@ -824,8 +1045,8 @@ class NetworkManager:
|
|||||||
|
|
||||||
# 清理临时文件
|
# 清理临时文件
|
||||||
try:
|
try:
|
||||||
os.remove(temp_file_path)
|
os.remove(archive_path)
|
||||||
self.logger.debug(f"[LOG_UPLOAD] 临时文件已删除: {temp_file_path}")
|
self.logger.debug(f"[LOG_UPLOAD] 临时文件已删除: {archive_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"[LOG_UPLOAD] 删除临时文件失败: {e}")
|
self.logger.warning(f"[LOG_UPLOAD] 删除临时文件失败: {e}")
|
||||||
|
|
||||||
@@ -1140,11 +1361,14 @@ class NetworkManager:
|
|||||||
os.system("poweroff")
|
os.system("poweroff")
|
||||||
return
|
return
|
||||||
elif inner_cmd == 43: # 上传日志命令
|
elif inner_cmd == 43: # 上传日志命令
|
||||||
# 格式: {"cmd":43, "data":{"ssid":"xxx","password":"xxx","url":"xxx"}}
|
# 格式: {"cmd":43, "data":{"ssid":"xxx","password":"xxx","url":"xxx", ...}}
|
||||||
inner_data = data_obj.get("data", {})
|
inner_data = data_obj.get("data", {})
|
||||||
upload_url = inner_data.get("url")
|
upload_url = inner_data.get("url")
|
||||||
wifi_ssid = inner_data.get("ssid")
|
wifi_ssid = inner_data.get("ssid")
|
||||||
wifi_password = inner_data.get("password")
|
wifi_password = inner_data.get("password")
|
||||||
|
include_rotated = inner_data.get("include_rotated", True)
|
||||||
|
max_files = inner_data.get("max_files")
|
||||||
|
archive_format = inner_data.get("archive", "tgz") # tgz 或 zip
|
||||||
|
|
||||||
if not upload_url:
|
if not upload_url:
|
||||||
self.logger.error("[LOG_UPLOAD] 缺少 url 参数")
|
self.logger.error("[LOG_UPLOAD] 缺少 url 参数")
|
||||||
@@ -1153,7 +1377,10 @@ class NetworkManager:
|
|||||||
self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令,目标URL: {upload_url}")
|
self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令,目标URL: {upload_url}")
|
||||||
# 在新线程中执行上传,避免阻塞主循环
|
# 在新线程中执行上传,避免阻塞主循环
|
||||||
import _thread
|
import _thread
|
||||||
_thread.start_new_thread(self._upload_log_file, (upload_url, wifi_ssid, wifi_password))
|
_thread.start_new_thread(
|
||||||
|
self._upload_log_file,
|
||||||
|
(upload_url, wifi_ssid, wifi_password, include_rotated, max_files, archive_format)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
time.sleep_ms(5)
|
time.sleep_ms(5)
|
||||||
|
|
||||||
@@ -1201,15 +1428,21 @@ class NetworkManager:
|
|||||||
vol_val = get_bus_voltage()
|
vol_val = get_bus_voltage()
|
||||||
if not self.tcp_send_raw(self._netcore.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})):
|
if not self.tcp_send_raw(self._netcore.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})):
|
||||||
# if not self.tcp_send_raw(self.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})):
|
# if not self.tcp_send_raw(self.make_packet(4, {"vol": vol_val, "vol_per": voltage_to_percent(vol_val)})):
|
||||||
# 心跳失败说明链路不可用:立刻触发重连,避免长时间卡住
|
|
||||||
self.logger.error("心跳发送失败,准备重连")
|
|
||||||
send_hartbeat_fail_count += 1
|
send_hartbeat_fail_count += 1
|
||||||
self._tcp_connected = False
|
# 短暂波动可能导致一次发送失败:连续失败达到阈值才重连,避免重连风暴
|
||||||
try:
|
self.logger.error(f"心跳发送失败({send_hartbeat_fail_count}/3),准备重试")
|
||||||
self.disconnect_server()
|
if send_hartbeat_fail_count >= 3:
|
||||||
except:
|
self.logger.error("心跳连续失败>=3,准备重连")
|
||||||
pass
|
self._tcp_connected = False
|
||||||
break
|
try:
|
||||||
|
self.disconnect_server()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 不立即断开,让下一轮心跳再试;同时缩短一点等待,提升恢复速度
|
||||||
|
time.sleep_ms(200)
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
send_hartbeat_fail_count = 0
|
send_hartbeat_fail_count = 0
|
||||||
last_heartbeat_send_time = current_time
|
last_heartbeat_send_time = current_time
|
||||||
|
|||||||
@@ -1195,12 +1195,6 @@ class OTAManager:
|
|||||||
try:
|
try:
|
||||||
# 与 4G 一致:OTA 期间暂停主循环 / 心跳等
|
# 与 4G 一致:OTA 期间暂停主循环 / 心跳等
|
||||||
self._begin_ota()
|
self._begin_ota()
|
||||||
ip, error = network_manager.connect_wifi(ssid, password)
|
|
||||||
if error:
|
|
||||||
safe_enqueue({"result": "wifi_failed", "error": error}, 2)
|
|
||||||
return
|
|
||||||
safe_enqueue({"result": "wifi_connected", "ip": ip}, 2)
|
|
||||||
|
|
||||||
if not ota_url:
|
if not ota_url:
|
||||||
safe_enqueue({"result": "ota_failed", "reason": "missing_url"}, 2)
|
safe_enqueue({"result": "ota_failed", "reason": "missing_url"}, 2)
|
||||||
return
|
return
|
||||||
@@ -1209,11 +1203,19 @@ class OTAManager:
|
|||||||
host = parsed_url.hostname
|
host = parsed_url.hostname
|
||||||
port = parsed_url.port or (443 if parsed_url.scheme == 'https' else 80)
|
port = parsed_url.port or (443 if parsed_url.scheme == 'https' else 80)
|
||||||
|
|
||||||
if not network_manager.is_server_reachable(host, port, timeout=8):
|
# 先连接 WiFi,并把 OTA host:port 作为“可达性验证目标”
|
||||||
err_msg = f"网络不通:无法连接 {host}:{port}"
|
# 只有连接成功 + 可访问 OTA 地址,才会把 SSID/PASS 落盘到 /boot/
|
||||||
safe_enqueue({"result": err_msg}, 2)
|
ip, error = network_manager.connect_wifi(
|
||||||
self.logger.error(err_msg)
|
ssid,
|
||||||
|
password,
|
||||||
|
verify_host=host,
|
||||||
|
verify_port=port,
|
||||||
|
persist=True,
|
||||||
|
)
|
||||||
|
if error:
|
||||||
|
safe_enqueue({"result": "wifi_failed", "error": error}, 2)
|
||||||
return
|
return
|
||||||
|
safe_enqueue({"result": "wifi_connected", "ip": ip}, 2)
|
||||||
|
|
||||||
downloaded_filename = self.get_filename_from_url(ota_url, default_name="main_tmp")
|
downloaded_filename = self.get_filename_from_url(ota_url, default_name="main_tmp")
|
||||||
self.logger.info(f"[OTA] 下载文件将保存为: {downloaded_filename}")
|
self.logger.info(f"[OTA] 下载文件将保存为: {downloaded_filename}")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
应用版本号
|
应用版本号
|
||||||
每次 OTA 更新时,只需要更新这个文件中的版本号
|
每次 OTA 更新时,只需要更新这个文件中的版本号
|
||||||
"""
|
"""
|
||||||
VERSION = '1.2.4'
|
VERSION = '1.2.7'
|
||||||
|
|
||||||
# 1.2.0 开始使用C++编译成.so,替换部分代码
|
# 1.2.0 开始使用C++编译成.so,替换部分代码
|
||||||
# 1.2.1 ota使用加密包
|
# 1.2.1 ota使用加密包
|
||||||
@@ -12,6 +12,8 @@ VERSION = '1.2.4'
|
|||||||
# 1.2.3 修改ADC_TRIGGER_THRESHOLD 为2300,支持上传日志到服务器
|
# 1.2.3 修改ADC_TRIGGER_THRESHOLD 为2300,支持上传日志到服务器
|
||||||
# 1.2.4 修改ADC_TRIGGER_THRESHOLD 为3000,并默认关闭摄像头的显示,并把ADC的采样间隔从50ms降低到10ms
|
# 1.2.4 修改ADC_TRIGGER_THRESHOLD 为3000,并默认关闭摄像头的显示,并把ADC的采样间隔从50ms降低到10ms
|
||||||
# 1.2.5 支持空气传感器采样,并默认关闭日志。优化断网时的发送队列丢消息问题,解决 WiFi 断线检测不可靠问题。
|
# 1.2.5 支持空气传感器采样,并默认关闭日志。优化断网时的发送队列丢消息问题,解决 WiFi 断线检测不可靠问题。
|
||||||
|
# 1.2.6 在链接 wifi 前先判断 wifi 的可用性,假如不可用,则不落盘。增加日志批量压缩上传功能
|
||||||
|
# 1.2.7 修复OTA失败的bug, 空气压力传感器的阈值是2500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user