diff --git a/network.py b/network.py index 2d3372c..ea46c61 100644 --- a/network.py +++ b/network.py @@ -679,6 +679,128 @@ class NetworkManager: self.logger.error(f"[WIFI-TCP] 接收数据异常: {e}") return b"" + def _upload_log_file(self, upload_url, wifi_ssid=None, wifi_password=None): + """上传日志文件到指定URL + + Args: + upload_url: 上传目标URL,例如 "https://example.com/upload/" + wifi_ssid: WiFi SSID(可选,如果未连接WiFi则尝试连接) + wifi_password: WiFi 密码(可选) + + Note: + 该功能仅在 WiFi 连接时可用,4G 网络暂不支持文件上传 + """ + import requests + import shutil + from datetime import datetime + + try: + # 检查 WiFi 连接状态,如果未连接则尝试连接 + if not self.is_wifi_connected(): + if wifi_ssid and wifi_password: + self.logger.info(f"[LOG_UPLOAD] WiFi 未连接,尝试连接 WiFi: {wifi_ssid}") + self.safe_enqueue({"result": "log_upload_connecting_wifi", "ssid": wifi_ssid}, 2) + + ip, error = self.connect_wifi(wifi_ssid, wifi_password) + if error: + self.logger.error(f"[LOG_UPLOAD] WiFi 连接失败: {error}") + self.safe_enqueue({ + "result": "log_upload_failed", + "reason": "wifi_connect_failed", + "detail": error + }, 2) + return + + self.logger.info(f"[LOG_UPLOAD] WiFi 连接成功,IP: {ip}") + else: + self.logger.warning("[LOG_UPLOAD] WiFi 未连接且未提供 WiFi 凭证,无法上传日志") + self.safe_enqueue({ + "result": "log_upload_failed", + "reason": "wifi_not_connected", + "detail": "WiFi not connected and no credentials provided" + }, 2) + return + else: + self.logger.info("[LOG_UPLOAD] WiFi 已连接,跳过连接步骤") + + self.logger.info(f"[LOG_UPLOAD] 开始上传日志文件...") + + # 获取日志文件路径 + log_file_path = config.LOG_FILE # /maixapp/apps/t11/app.log + + if not os.path.exists(log_file_path): + self.logger.error(f"[LOG_UPLOAD] 日志文件不存在: {log_file_path}") + self.safe_enqueue({"result": "log_upload_failed", "reason": "log_file_not_found"}, 2) + return + + # 生成带时间戳的文件名 + # 格式: app_20260131_143025_d19566161359c372.log + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + device_id = self._device_id or "unknown" + new_filename = f"app_{timestamp}_{device_id}.log" + + # 创建临时文件(带时间戳的名字) + temp_dir = "/tmp" + temp_file_path = os.path.join(temp_dir, new_filename) + + # 复制日志文件到临时位置 + shutil.copy2(log_file_path, temp_file_path) + self.logger.info(f"[LOG_UPLOAD] 日志文件已复制到: {temp_file_path}") + + # 使用 multipart/form-data 上传文件 + with open(temp_file_path, 'rb') as f: + files = {'file': (new_filename, f, 'text/plain')} + + # 添加额外的头部信息 + headers = { + 'User-Agent': 'Archery-Device/1.0', + 'X-Device-ID': device_id, + } + + # 如果是 ngrok-free.dev,添加绕过警告页面的头 + if 'ngrok-free.dev' in upload_url or 'ngrok.io' in upload_url: + headers['ngrok-skip-browser-warning'] = 'true' + + self.logger.info(f"[LOG_UPLOAD] 正在上传到: {upload_url}") + + # 禁用 SSL 警告(用于自签名证书或 SSL 兼容性问题) + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + # 发送请求,verify=False 跳过 SSL 证书验证(解决 MaixCAM SSL 兼容性问题) + response = requests.post(upload_url, files=files, headers=headers, timeout=60, verify=False) + + if response.status_code in (200, 201, 204): + self.logger.info(f"[LOG_UPLOAD] 上传成功! 状态码: {response.status_code}") + self.safe_enqueue({ + "result": "log_upload_ok", + "filename": new_filename, + "status_code": response.status_code + }, 2) + else: + self.logger.error(f"[LOG_UPLOAD] 上传失败! 状态码: {response.status_code}, 响应: {response.text[:200]}") + self.safe_enqueue({ + "result": "log_upload_failed", + "reason": f"http_{response.status_code}", + "detail": response.text[:100] + }, 2) + + # 清理临时文件 + try: + os.remove(temp_file_path) + self.logger.debug(f"[LOG_UPLOAD] 临时文件已删除: {temp_file_path}") + except Exception as e: + self.logger.warning(f"[LOG_UPLOAD] 删除临时文件失败: {e}") + + except requests.exceptions.Timeout: + self.logger.error("[LOG_UPLOAD] 上传超时") + self.safe_enqueue({"result": "log_upload_failed", "reason": "timeout"}, 2) + except requests.exceptions.ConnectionError as e: + self.logger.error(f"[LOG_UPLOAD] 连接失败: {e}") + self.safe_enqueue({"result": "log_upload_failed", "reason": "connection_error"}, 2) + except Exception as e: + self.logger.error(f"[LOG_UPLOAD] 上传异常: {e}") + self.safe_enqueue({"result": "log_upload_failed", "reason": str(e)[:100]}, 2) def generate_token(self, device_id): """生成用于 HTTP 接口鉴权的 Token(HMAC-SHA256)""" @@ -970,6 +1092,21 @@ class NetworkManager: time.sleep_ms(2000) os.system("poweroff") return + elif inner_cmd == 43: # 上传日志命令 + # 格式: {"cmd":43, "data":{"ssid":"xxx","password":"xxx","url":"xxx"}} + inner_data = data_obj.get("data", {}) + upload_url = inner_data.get("url") + wifi_ssid = inner_data.get("ssid") + wifi_password = inner_data.get("password") + + if not upload_url: + self.logger.error("[LOG_UPLOAD] 缺少 url 参数") + self.safe_enqueue({"result": "log_upload_failed", "reason": "missing_url"}, 2) + else: + self.logger.info(f"[LOG_UPLOAD] 收到日志上传命令,目标URL: {upload_url}") + # 在新线程中执行上传,避免阻塞主循环 + import _thread + _thread.start_new_thread(self._upload_log_file, (upload_url, wifi_ssid, wifi_password)) else: time.sleep_ms(5) diff --git a/version.py b/version.py index 26d787c..8defd66 100644 --- a/version.py +++ b/version.py @@ -4,11 +4,14 @@ 应用版本号 每次 OTA 更新时,只需要更新这个文件中的版本号 """ -VERSION = '1.2.1' +VERSION = '1.2.3' # 1.2.0 开始使用C++编译成.so,替换部分代码 # 1.2.1 ota使用加密包 # 1.2.2 支持wifi ota,并且设定时区,并使用单独线程保存图片 +# 1.2.3 修改ADC_TRIGGER_THRESHOLD 为2300,支持上传日志到服务器 + +