upload img to qiniu
This commit is contained in:
@@ -194,3 +194,31 @@ m/offset_method/distance_method:标记本次用的算法路径(triangle / ye
|
||||
你现在的“环数计算”实际依赖关系
|
||||
最好路径(快+稳):三角形 → dx,dy(单应性) + distance_m(PnP)
|
||||
兜底路径:圆/椭圆靶心 → dx,dy(基于黄心半径比例/透视校正) + distance_m(黄心半径估距)
|
||||
|
||||
12. 4g模块上传文件:
|
||||
|
||||
Upload images from MaixCam to Qiniu cloud via ML307R 4G module's AT commands. The HTTP body requires multipart/form-data with real CR/LF bytes (0x0D 0x0A) in boundaries.
|
||||
Methods Tried
|
||||
# Method AT Commands Result Root Cause
|
||||
1 Raw binary, no encoding MHTTPCONTENT with raw bytes + length param ERROR at first chunk CR/LF in binary data terminates AT command parser
|
||||
2 Encoding mode 2 (escape) MHTTPCFG="encoding",0,2 + \r\n escapes Server 400 Bad Request Module sends literal text \r\n to server, NOT actual 0x0D 0x0A bytes. Multipart body is garbled
|
||||
3 Encoding mode 1 (hex) MHTTPCFG="encoding",0,1 + hex-encoded data CME ERROR: 650/50 Firmware doesn't properly support hex mode for MHTTPCONTENT
|
||||
4 No chunked mode Skip MHTTPCFG="chunked" CME ERROR: 65 Module requires chunked mode to accept MHTTPCONTENT at all
|
||||
5 Single large MHTTPCONTENT All data in one command (2793 bytes) +MHTTPURC: "err",0,5 (timeout) Possible buffer limit; module hangs then times out
|
||||
6 Per-chunk HTTP instance (OTA style) CREATE→POST→DELETE per chunk Not feasible Each instance = separate HTTP request; Qiniu needs complete body in single POST
|
||||
Conclusion: AT HTTP layer (MHTTPCONTENT) is fundamentally broken for binary uploads.
|
||||
The Solution: Raw TCP Socket (MIPOPEN + MIPSEND)
|
||||
Bypass the AT HTTP layer entirely. Open a raw TCP connection and send a hand-crafted HTTP POST:
|
||||
plaintext
|
||||
AT+MIPCLOSE=3 // Clean up old socket
|
||||
AT+MIPOPEN=3,"TCP","upload.qiniup.com",80 // Raw TCP connection
|
||||
AT+MIPSEND=3,1024 → ">" → [raw bytes] → OK // Binary-safe!
|
||||
AT+MIPSEND=3,1024 → ">" → [raw bytes] → OK
|
||||
AT+MIPSEND=3,766 → ">" → [raw bytes] → OK
|
||||
// Response: +MIPURC: "rtcp",3,<len>,HTTP/1.1 200 OK...
|
||||
AT+MIPCLOSE=3
|
||||
Why it works:
|
||||
MIPSEND enters prompt mode (>) — after the >, the AT parser treats ALL bytes as data, including CR/LF
|
||||
We construct the complete HTTP request ourselves (headers + Content-Length + multipart body) with real CRLF bytes
|
||||
|
||||
Key bug found during integration: _send_chunk() wrapped calls in self.at._cmd_lock, but self.at.send() also acquires the same lock internally — threading.Lock() is not reentrant, causing deadlock. Fixed by removing the outer lock (the network_manager.get_uart_lock() already provides thread safety).Trade-off: UART is locked during the entire upload, so heartbeats pause. For small JPEG files (~2-80KB), this is 5-20 seconds — acceptable if server heartbeat timeout is generous
|
||||
@@ -24,8 +24,8 @@ r = hardware_manager.at_client.send(f'AT+MSSLCFG="auth",{ssl_id},0', "OK", 3000)
|
||||
这些在 config.py 是明文:
|
||||
SERVER_IP = "stcp.shelingxingqiu.com"SERVER_PORT = 50005HTTP_URL = "http://ws.shelingxingqiu.com"HTTP_API_PATH = "/home/shoot/device_fire/arrow/fire"
|
||||
注意:即使你把它们藏进 .so,攻击者仍可能通过抓包/观察 DNS/SNI/流量拿到域名或 IP,所以“隐藏”只能降低静态分析风险,不能替代鉴权/签名。
|
||||
2.2 WiFi 凭证落盘位置
|
||||
你会把 SSID/密码写到 /boot/wifi.ssid 和 /boot/wifi.pass(network.py/wifi.py 都有)。拿到设备存储就能读到明文密码,这属于设备侧安全问题。
|
||||
|
||||
|
||||
2.3 日志/调试信息泄露
|
||||
你仓库里 .cursor/debug.log* 已经记录了完整的 AT+MIPOPEN ... host:port 等信息;如果这些被打进包或被用户导出,也会泄露通信细节。
|
||||
你发布包里“真正会被带走的敏感内容”
|
||||
@@ -37,5 +37,9 @@ files: - app.yaml - at_client.py - camera_manager.py - config.py - hardware
|
||||
把“password=device_id+.” 改为真正的 per-device secret(设备出厂烧录,服务端保存;或服务端下发公钥,设备用私钥签名)
|
||||
所有上报/控制命令加签名 + nonce/timestamp + 服务端防重放(别人抓到一次包也不能复用)
|
||||
OTA 包必须做签名校验(设备端内置公钥,下载后验签通过才应用)
|
||||
TLS 必须做证书校验/最好做 pinning(至少别用 auth=0)
|
||||
如果你告诉我:你们服务端目前能不能改协议(例如新增签名字段、下发 challenge、做 OTA 签名),我可以按“最小改动但提升最大安全”的顺序,帮你规划一套从现状平滑升级的方案。
|
||||
|
||||
如果你告诉我:你们服务端目前能不能改协议(例如新增签名字段、下发 challenge、做 OTA 签名),我可以按“最小改动但提升最大安全”的顺序,帮你规划一套从现状平滑升级的方案。
|
||||
|
||||
|
||||
|
||||
https://wiki.sipeed.com/maixpy/doc/zh/pro/compile_os.html
|
||||
165
network.py
165
network.py
@@ -1406,6 +1406,112 @@ class NetworkManager:
|
||||
self.logger.error(f"[LOG_UPLOAD] 上传异常: {e}")
|
||||
self.safe_enqueue({"result": "log_upload_failed", "reason": str(e)[:100]}, 2)
|
||||
|
||||
def _upload_image_file(self, image_path, upload_url, upload_token, key, shoot_id, outlink):
|
||||
"""上传图片文件到指定URL(自动检测网络类型,WiFi使用requests,4G使用AT HTTP命令)
|
||||
|
||||
Args:
|
||||
image_path: 本地图片文件路径
|
||||
upload_url: 上传目标URL,例如 "https://upload.qiniup.com"
|
||||
upload_token: 上传token
|
||||
key: 文件key,例如 "shootPic/123456.bmp"
|
||||
shoot_id: 射击ID
|
||||
outlink: 外链域名(可选,用于构建访问URL)
|
||||
"""
|
||||
# 自动检测网络类型,选择上传路径
|
||||
if self._network_type == "wifi" and self.is_wifi_connected():
|
||||
mode = "wifi"
|
||||
else:
|
||||
mode = "4g"
|
||||
|
||||
self.logger.info(f"[IMAGE_UPLOAD] Using {mode} path, image: {image_path}")
|
||||
|
||||
try:
|
||||
if mode == "wifi":
|
||||
# ---- WiFi path: 使用 requests 库上传 ----
|
||||
import requests
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
with open(image_path, 'rb') as f:
|
||||
files = {'file': (os.path.basename(image_path), f, 'application/octet-stream')}
|
||||
data = {'token': upload_token, 'key': key}
|
||||
# 测试:将HTTPS转为HTTP
|
||||
wifi_upload_url = upload_url.replace('https://', 'http://', 1)
|
||||
self.logger.info(f"[IMAGE_UPLOAD] WiFi upload URL: {wifi_upload_url}")
|
||||
response = requests.post(wifi_upload_url, files=files, data=data, timeout=120, verify=False)
|
||||
response.raise_for_status()
|
||||
result_json = response.json()
|
||||
uploaded_key = result_json.get('key', key)
|
||||
|
||||
self.logger.info(f"[IMAGE_UPLOAD] WiFi upload ok: key={uploaded_key}")
|
||||
|
||||
access_url = None
|
||||
if outlink:
|
||||
access_url = f"https://{outlink}/{uploaded_key}"
|
||||
|
||||
response_data = {
|
||||
"result": "image_upload_ok",
|
||||
"shootId": shoot_id,
|
||||
"key": uploaded_key,
|
||||
"via": "wifi",
|
||||
}
|
||||
if access_url:
|
||||
response_data["url"] = access_url
|
||||
|
||||
self.safe_enqueue(response_data, 2)
|
||||
|
||||
else:
|
||||
# ---- 4G path: 使用 FourGUploadManager AT命令上传 ----
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"four_g_upload_manager",
|
||||
os.path.join(os.path.dirname(__file__), "4g_upload_manager.py")
|
||||
)
|
||||
upload_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(upload_module)
|
||||
FourGUploadManager = upload_module.FourGUploadManager
|
||||
|
||||
# 实例化4G上传管理器
|
||||
uploader = FourGUploadManager(hardware_manager.at_client)
|
||||
|
||||
# 执行上传
|
||||
result = uploader.upload_image(image_path, upload_url, upload_token, key)
|
||||
|
||||
if result.get("success"):
|
||||
uploaded_key = result.get("key", key)
|
||||
self.logger.info(f"[IMAGE_UPLOAD] 4G upload ok: key={uploaded_key}")
|
||||
|
||||
access_url = None
|
||||
if outlink:
|
||||
access_url = f"https://{outlink}/{uploaded_key}"
|
||||
|
||||
response_data = {
|
||||
"result": "image_upload_ok",
|
||||
"shootId": shoot_id,
|
||||
"key": uploaded_key,
|
||||
"via": "4g",
|
||||
}
|
||||
if access_url:
|
||||
response_data["url"] = access_url
|
||||
|
||||
self.safe_enqueue(response_data, 2)
|
||||
else:
|
||||
error_msg = result.get("error", "unknown_error")
|
||||
self.logger.error(f"[IMAGE_UPLOAD] 4G upload failed: {error_msg}")
|
||||
self.safe_enqueue({
|
||||
"result": "image_upload_failed",
|
||||
"shootId": shoot_id,
|
||||
"reason": error_msg[:100]
|
||||
}, 2)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"[IMAGE_UPLOAD] upload exception ({mode}): {e}")
|
||||
self.safe_enqueue({
|
||||
"result": "image_upload_failed",
|
||||
"shootId": shoot_id,
|
||||
"reason": str(e)[:100]
|
||||
}, 2)
|
||||
|
||||
def generate_token(self, device_id):
|
||||
"""生成用于 HTTP 接口鉴权的 Token(HMAC-SHA256)"""
|
||||
SALT = "shootMessageFire"
|
||||
@@ -1592,6 +1698,64 @@ class NetworkManager:
|
||||
self.safe_enqueue({'data':{'l': len(self._raw_line_data), 'v': v}, 'cmd': 41})
|
||||
self.logger.info(f"已下载{len(self._raw_line_data)} 全部:{t} 版本:{v}")
|
||||
|
||||
elif logged_in and msg_type == 100:
|
||||
self.logger.info(f"[IMAGE_UPLOAD] 收到图片上传命令 {body}")
|
||||
if isinstance(body, dict):
|
||||
|
||||
upload_url = body.get("uploadUrl")
|
||||
upload_token = body.get("token")
|
||||
shoot_id = body.get("shootId")
|
||||
outlink = body.get("outlink", "")
|
||||
|
||||
hardware_manager.start_idle_timer() # 重新计时
|
||||
|
||||
# 验证必需字段
|
||||
if not upload_url or not upload_token or not shoot_id:
|
||||
self.logger.error("[IMAGE_UPLOAD] 缺少必需参数: uploadUrl, token 或 shootId")
|
||||
self.safe_enqueue({"result": "image_upload_failed", "reason": "missing_params"}, 2)
|
||||
else:
|
||||
self.logger.info(f"[IMAGE_UPLOAD] 收到图片上传命令,shootId: {shoot_id}")
|
||||
# 查找文件名中包含 shoot_id 的图片文件(文件名格式:shot_{shoot_id}_*.bmp)
|
||||
image_extensions = ('.bmp', '.jpg', '.jpeg', '.png')
|
||||
photo_dir = config.PHOTO_DIR
|
||||
target_image = None
|
||||
try:
|
||||
if os.path.isdir(photo_dir):
|
||||
# 优先查找文件名中包含 shoot_id 的图片
|
||||
matched_images = [
|
||||
f for f in os.listdir(photo_dir)
|
||||
if f.lower().endswith(image_extensions) and shoot_id in f
|
||||
]
|
||||
if matched_images:
|
||||
# 按修改时间排序,取最新的匹配文件
|
||||
matched_images.sort(
|
||||
key=lambda f: os.path.getmtime(os.path.join(photo_dir, f)),
|
||||
reverse=True
|
||||
)
|
||||
target_image = os.path.join(photo_dir, matched_images[0])
|
||||
self.logger.info(f"[IMAGE_UPLOAD] 找到匹配shootId的图片: {matched_images[0]}")
|
||||
else:
|
||||
self.logger.warning(f"[IMAGE_UPLOAD] 未找到包含shootId={shoot_id}的图片文件")
|
||||
except Exception as e:
|
||||
self.logger.error(f"[IMAGE_UPLOAD] 查找图片失败: {e}")
|
||||
|
||||
if not target_image:
|
||||
self.logger.error(f"[IMAGE_UPLOAD] 未找到shootId={shoot_id}对应的图片文件")
|
||||
self.safe_enqueue({"result": "image_upload_failed", "reason": "no_image_found", "shootId": shoot_id}, 2)
|
||||
else:
|
||||
# 构建上传key
|
||||
ext = os.path.splitext(target_image)[1].lower()
|
||||
key = f"shootPic/{self.device_id}/{shoot_id}{ext}"
|
||||
self.logger.info(f"[IMAGE_UPLOAD] 准备上传: {target_image} -> {key}")
|
||||
|
||||
# 在新线程中执行上传,避免阻塞主循环
|
||||
import _thread
|
||||
_thread.start_new_thread(
|
||||
self._upload_image_file,
|
||||
(target_image, upload_url, upload_token, key, shoot_id, outlink)
|
||||
)
|
||||
# 立即返回已入队确认
|
||||
self.safe_enqueue({"result": "image_upload_queued", "shootId": shoot_id}, 2)
|
||||
# 处理业务指令
|
||||
elif logged_in and isinstance(body, dict):
|
||||
inner_cmd = None
|
||||
@@ -1753,6 +1917,7 @@ class NetworkManager:
|
||||
self._upload_log_file,
|
||||
(upload_url, wifi_ssid, wifi_password, include_rotated, max_files, archive_format)
|
||||
)
|
||||
|
||||
else: # data的结构不是 dict
|
||||
self.logger.info(f"[NET] body={body}, {time.time()}")
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user