dexverse-uploader 0.1.4a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dexverse_uploader-0.1.4a1/PKG-INFO +59 -0
- dexverse_uploader-0.1.4a1/README.md +17 -0
- dexverse_uploader-0.1.4a1/cloud_upload.sh +266 -0
- dexverse_uploader-0.1.4a1/package_runner.py +44 -0
- dexverse_uploader-0.1.4a1/pyproject.toml +105 -0
- dexverse_uploader-0.1.4a1/requirements/base.txt +3 -0
- dexverse_uploader-0.1.4a1/requirements/dev.txt +7 -0
- dexverse_uploader-0.1.4a1/requirements/hdf5.txt +2 -0
- dexverse_uploader-0.1.4a1/requirements/piper.txt +4 -0
- dexverse_uploader-0.1.4a1/requirements/tianji.txt +13 -0
- dexverse_uploader-0.1.4a1/requirements/umi.txt +8 -0
- dexverse_uploader-0.1.4a1/requirements/w1.txt +2 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/__init__.py +49 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/chunk_uploader.py +76 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/cloud_upload_config.py +109 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/collection_metadata.py +152 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/converter.py +954 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/converter_worker.py +274 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/device_main.py +170 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/file_scanner.py +666 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/hdf5_metadata.py +193 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/logger.py +41 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/mqtt_client.py +226 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/__init__.py +1 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/__init__.py +1 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/PROVENANCE.md +11 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/__init__.py +1 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/__init__.py +1 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/_stats.py +30 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/base.py +48 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/eef_consistency.py +74 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/modality.py +17 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/raw_activity.py +46 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/rot6d.py +123 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/spike.py +63 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/state_action.py +141 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/timestamp.py +99 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/validity.py +70 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/checks/video.py +194 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/io/__init__.py +1 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/io/loaders.py +356 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_algorithm/io/schema.py +129 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_freeze.py +47 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_metadata.py +36 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_profile.py +104 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_quality_checkers.py +268 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_quality_runtime.py +96 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/piper/piper_quality_worker.py +27 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/__init__.py +1 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/_vendor/__init__.py +1 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/_vendor/boundary_core.py +515 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/tianji_converter.py +294 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/tianji_format.py +63 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/tianji_mcap.py +221 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/tianji_metadata.py +97 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/tianji_output_validator.py +183 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/tianji_profile.py +114 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/tianji_quality.py +404 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/tianji_runtime.py +215 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/tj/tianji_worker_control.py +70 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/umi/__init__.py +1 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/umi/umi_alignment.py +148 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/umi/umi_converter.py +652 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/umi/umi_metadata.py +108 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/umi/umi_quality_checkers.py +427 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/w1/__init__.py +1 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/w1/feedback.py +53 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/w1/gst_h264_encoder.py +152 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/w1/hdf5_converter.py +999 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/protocols/w1/w1_quality_checker.py +251 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/quality_profiles.py +830 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/quality_report_store.py +99 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/quality_result.py +234 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/sample_hash.py +54 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/upload_control.py +343 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/upload_manager.py +1311 -0
- dexverse_uploader-0.1.4a1/src/dexverse_uploader/upload_state.py +126 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dexverse-uploader
|
|
3
|
+
Version: 0.1.4a1
|
|
4
|
+
Summary: DexVerse edge-device data uploader
|
|
5
|
+
Requires-Python: <3.11,>=3.10
|
|
6
|
+
Requires-Dist: paho-mqtt<3,>=2.0
|
|
7
|
+
Requires-Dist: requests<3,>=2.25
|
|
8
|
+
Provides-Extra: hdf5
|
|
9
|
+
Requires-Dist: h5py<4,>=3; extra == 'hdf5'
|
|
10
|
+
Provides-Extra: piper
|
|
11
|
+
Requires-Dist: h5py<4,>=3.8; extra == 'piper'
|
|
12
|
+
Requires-Dist: numpy<3,>=1.24; extra == 'piper'
|
|
13
|
+
Requires-Dist: pillow<13,>=10; extra == 'piper'
|
|
14
|
+
Provides-Extra: tianji
|
|
15
|
+
Requires-Dist: av<19,>=8.1; extra == 'tianji'
|
|
16
|
+
Requires-Dist: h5ffmpeg==2.4.0; extra == 'tianji'
|
|
17
|
+
Requires-Dist: h5py<4,>=3.6; extra == 'tianji'
|
|
18
|
+
Requires-Dist: imgaug==0.4.0; extra == 'tianji'
|
|
19
|
+
Requires-Dist: lz4<5,>=4.3; extra == 'tianji'
|
|
20
|
+
Requires-Dist: mcap-ros2-support==0.5.7; extra == 'tianji'
|
|
21
|
+
Requires-Dist: mcap==1.4.0; extra == 'tianji'
|
|
22
|
+
Requires-Dist: numpy<2,>=1.24; extra == 'tianji'
|
|
23
|
+
Requires-Dist: pillow<12,>=9; extra == 'tianji'
|
|
24
|
+
Requires-Dist: pyyaml<7,>=6; extra == 'tianji'
|
|
25
|
+
Requires-Dist: zstandard<1,>=0.23; extra == 'tianji'
|
|
26
|
+
Provides-Extra: umi
|
|
27
|
+
Requires-Dist: h5ffmpeg==2.4.0; extra == 'umi'
|
|
28
|
+
Requires-Dist: h5py==3.14.0; extra == 'umi'
|
|
29
|
+
Requires-Dist: hdfdict==0.3.1; extra == 'umi'
|
|
30
|
+
Requires-Dist: numpy==1.26.4; extra == 'umi'
|
|
31
|
+
Requires-Dist: opencv-python-headless<5,>=4.8; extra == 'umi'
|
|
32
|
+
Requires-Dist: scipy<2,>=1.11; extra == 'umi'
|
|
33
|
+
Requires-Dist: tqdm<5,>=4.66; extra == 'umi'
|
|
34
|
+
Provides-Extra: w1
|
|
35
|
+
Requires-Dist: h5py<4,>=3; extra == 'w1'
|
|
36
|
+
Requires-Dist: numpy<3,>=1.24; extra == 'w1'
|
|
37
|
+
Requires-Dist: opencv-python-headless<5,>=4.8; extra == 'w1'
|
|
38
|
+
Requires-Dist: pillow<13,>=10; extra == 'w1'
|
|
39
|
+
Requires-Dist: pyyaml<7,>=5.1; extra == 'w1'
|
|
40
|
+
Requires-Dist: tqdm<5,>=4.66; extra == 'w1'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# DexVerse Uploader
|
|
44
|
+
|
|
45
|
+
The uploader is a standard `src`-layout Python package. Install the base
|
|
46
|
+
wheel for the MQTT/HTTP service and select a protocol extra when the target
|
|
47
|
+
runtime needs its Python-side conversion or quality-check dependencies:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
pip install dexverse-uploader
|
|
51
|
+
pip install dexverse-uploader[w1]
|
|
52
|
+
pip install dexverse-uploader[umi]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The profile requirements under `requirements/` remain the offline deployment
|
|
56
|
+
manifests. They are kept separate from package metadata because W1/GStreamer,
|
|
57
|
+
UMI/native codecs, and other protocol components can be supplied by the edge
|
|
58
|
+
image or a dedicated runtime. The service keeps the existing MQTT RPC
|
|
59
|
+
contracts and can be started with `python -m dexverse_uploader.device_main`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# DexVerse Uploader
|
|
2
|
+
|
|
3
|
+
The uploader is a standard `src`-layout Python package. Install the base
|
|
4
|
+
wheel for the MQTT/HTTP service and select a protocol extra when the target
|
|
5
|
+
runtime needs its Python-side conversion or quality-check dependencies:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
pip install dexverse-uploader
|
|
9
|
+
pip install dexverse-uploader[w1]
|
|
10
|
+
pip install dexverse-uploader[umi]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The profile requirements under `requirements/` remain the offline deployment
|
|
14
|
+
manifests. They are kept separate from package metadata because W1/GStreamer,
|
|
15
|
+
UMI/native codecs, and other protocol components can be supplied by the edge
|
|
16
|
+
image or a dedicated runtime. The service keeps the existing MQTT RPC
|
|
17
|
+
contracts and can be started with `python -m dexverse_uploader.device_main`.
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ----------------------------------------------------------------------------
|
|
3
|
+
# Copyright (c) 2021-2025 DexForce Technology Co., Ltd.
|
|
4
|
+
#
|
|
5
|
+
# All rights reserved.
|
|
6
|
+
# ----------------------------------------------------------------------------
|
|
7
|
+
# cloud_upload.sh — 云端上传服务启动/重启/停止脚本
|
|
8
|
+
#
|
|
9
|
+
# 用法:
|
|
10
|
+
# bash dexverse/dexverse-uploader/cloud_upload.sh [start|restart|stop]
|
|
11
|
+
#
|
|
12
|
+
# 说明:
|
|
13
|
+
# 1) Python 包位于 src/dexverse_uploader/,外层目录名不参与导入。
|
|
14
|
+
# 2) base 主进程与 UMI 转换进程可使用不同 Python 环境。
|
|
15
|
+
# 3) 支持 start / restart / stop 三种模式。
|
|
16
|
+
# 4) 单实例锁,防止重复拉起。
|
|
17
|
+
# 5) 日志统一输出到模块内 log/ 目录,模块日志由 get_logger() 自动写入。
|
|
18
|
+
# ----------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
set -u
|
|
21
|
+
|
|
22
|
+
MODE="${1:-start}"
|
|
23
|
+
if [[ "${MODE}" != "start" && "${MODE}" != "restart" && "${MODE}" != "stop" ]]; then
|
|
24
|
+
echo "用法: bash $(basename "$0") [start|restart|stop]"
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# 发布目录;外层目录名不参与模块导入。
|
|
29
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
30
|
+
RUNTIME_DIR="${CLOUD_UPLOAD_RUNTIME_DIR:-${SCRIPT_DIR}}"
|
|
31
|
+
CONFIG_PATH="${CLOUD_UPLOAD_CONFIG:-${RUNTIME_DIR}/config.json}"
|
|
32
|
+
PACKAGE_PARENT="${SCRIPT_DIR}/src"
|
|
33
|
+
|
|
34
|
+
# 日志目录
|
|
35
|
+
LOG_DIR="${CLOUD_UPLOAD_LOG_DIR:-${RUNTIME_DIR}/log}"
|
|
36
|
+
mkdir -p "${LOG_DIR}"
|
|
37
|
+
|
|
38
|
+
# base 主进程只需要扫描、MQTT、HTTP 和日志依赖。
|
|
39
|
+
PYTHON_BIN="${CLOUD_UPLOAD_BASE_PYTHON:-python3}"
|
|
40
|
+
BASE_DEPS_DIR="${CLOUD_UPLOAD_BASE_DEPS_DIR:-${RUNTIME_DIR}/.deps/base}"
|
|
41
|
+
W1_DEPS_DIR="${CLOUD_UPLOAD_W1_DEPS_DIR:-${RUNTIME_DIR}/.deps/w1}"
|
|
42
|
+
PIP_ZIPAPP="${CLOUD_UPLOAD_PIP_ZIPAPP:-${RUNTIME_DIR}/.deps/pip.pyz}"
|
|
43
|
+
BASE_REQUIREMENTS="${SCRIPT_DIR}/requirements/base.txt"
|
|
44
|
+
HDF5_REQUIREMENTS="${SCRIPT_DIR}/requirements/hdf5.txt"
|
|
45
|
+
W1_REQUIREMENTS="${SCRIPT_DIR}/requirements/w1.txt"
|
|
46
|
+
# UMI worker 使用独立环境;未配置时回退到 base Python,便于兼容现有部署。
|
|
47
|
+
UMI_PYTHON_BIN="${CLOUD_UPLOAD_UMI_PYTHON:-${PYTHON_BIN}}"
|
|
48
|
+
START_MODULE="device_main"
|
|
49
|
+
SERVICE_PATTERN="([p]ython[^ ]* -m [d]exverse_uploader.device_main|[p]ackage_runner.py device_main|python3 -m [d]exe_cloud_upload.device_main)"
|
|
50
|
+
|
|
51
|
+
# ── 单实例锁 ──────────────────────────────────────────────────────────
|
|
52
|
+
LAUNCHER_LOCK_FILE="${LOG_DIR}/dexverse_uploader.launcher.pid"
|
|
53
|
+
SERVICE_PID_FILE="${LOG_DIR}/dexverse_uploader.pid"
|
|
54
|
+
|
|
55
|
+
acquire_launcher_lock() {
|
|
56
|
+
local current_pid="$$"
|
|
57
|
+
|
|
58
|
+
if [[ -f "${LAUNCHER_LOCK_FILE}" ]]; then
|
|
59
|
+
local old_pid
|
|
60
|
+
old_pid="$(cat "${LAUNCHER_LOCK_FILE}" 2>/dev/null || true)"
|
|
61
|
+
|
|
62
|
+
if [[ -n "${old_pid}" && "${old_pid}" =~ ^[0-9]+$ ]] && kill -0 "${old_pid}" 2>/dev/null; then
|
|
63
|
+
if [[ "${MODE}" == "restart" || "${MODE}" == "stop" ]]; then
|
|
64
|
+
echo "[WARN] 检测到已有启动脚本实例 (pid=${old_pid}),先停止旧实例。"
|
|
65
|
+
kill -TERM "${old_pid}" 2>/dev/null || true
|
|
66
|
+
for _ in 1 2 3 4 5; do
|
|
67
|
+
if ! kill -0 "${old_pid}" 2>/dev/null; then
|
|
68
|
+
break
|
|
69
|
+
fi
|
|
70
|
+
sleep 1
|
|
71
|
+
done
|
|
72
|
+
if kill -0 "${old_pid}" 2>/dev/null; then
|
|
73
|
+
echo "[WARN] 旧实例未退出,执行强制停止: pid=${old_pid}"
|
|
74
|
+
kill -KILL "${old_pid}" 2>/dev/null || true
|
|
75
|
+
fi
|
|
76
|
+
else
|
|
77
|
+
echo "[ERROR] 已有启动脚本实例在运行 (pid=${old_pid})。"
|
|
78
|
+
echo "[ERROR] 请使用: bash $(basename "$0") restart"
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
echo "${current_pid}" > "${LAUNCHER_LOCK_FILE}"
|
|
85
|
+
trap 'rm -f "${LAUNCHER_LOCK_FILE}"' EXIT
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# ── 环境检查 ──────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
check_env() {
|
|
91
|
+
if [[ ! -f "${PACKAGE_PARENT}/dexverse_uploader/device_main.py" ]]; then
|
|
92
|
+
echo "[ERROR] Python 包不存在: ${PACKAGE_PARENT}/dexverse_uploader"
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
if [[ ! -f "${CONFIG_PATH}" ]]; then
|
|
97
|
+
echo "[ERROR] 配置文件不存在: ${CONFIG_PATH}"
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
if [[ ! -f "${BASE_REQUIREMENTS}" ]]; then
|
|
102
|
+
echo "[ERROR] base 依赖清单不存在: ${BASE_REQUIREMENTS}"
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
if [[ ! -f "${HDF5_REQUIREMENTS}" ]]; then
|
|
106
|
+
echo "[ERROR] HDF5 依赖清单不存在: ${HDF5_REQUIREMENTS}"
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
if [[ ! -f "${W1_REQUIREMENTS}" ]]; then
|
|
110
|
+
echo "[ERROR] W1 依赖清单不存在: ${W1_REQUIREMENTS}"
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ensure_base_dependencies() {
|
|
116
|
+
local import_check="import numpy, paho.mqtt.client, requests, h5py"
|
|
117
|
+
if PYTHONPATH="${BASE_DEPS_DIR}${PYTHONPATH:+:${PYTHONPATH}}" \
|
|
118
|
+
"${PYTHON_BIN}" -c "${import_check}" >/dev/null 2>&1; then
|
|
119
|
+
return 0
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
echo "[INFO] 检测到 base 依赖缺失,安装到模块私有目录: ${BASE_DEPS_DIR}"
|
|
123
|
+
mkdir -p "${BASE_DEPS_DIR}" "$(dirname "${PIP_ZIPAPP}")"
|
|
124
|
+
|
|
125
|
+
if [[ ! -f "${PIP_ZIPAPP}" ]]; then
|
|
126
|
+
echo "[INFO] 下载 pip 启动器..."
|
|
127
|
+
"${PYTHON_BIN}" - "${PIP_ZIPAPP}" <<'PY'
|
|
128
|
+
import pathlib
|
|
129
|
+
import sys
|
|
130
|
+
import urllib.request
|
|
131
|
+
|
|
132
|
+
target = pathlib.Path(sys.argv[1])
|
|
133
|
+
urllib.request.urlretrieve("https://bootstrap.pypa.io/pip/pip.pyz", target)
|
|
134
|
+
PY
|
|
135
|
+
if [[ $? -ne 0 ]]; then
|
|
136
|
+
echo "[ERROR] pip 启动器下载失败,请检查网络或设置 CLOUD_UPLOAD_PIP_ZIPAPP。"
|
|
137
|
+
exit 1
|
|
138
|
+
fi
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
"${PYTHON_BIN}" "${PIP_ZIPAPP}" install \
|
|
142
|
+
--disable-pip-version-check \
|
|
143
|
+
--target "${BASE_DEPS_DIR}" \
|
|
144
|
+
-r "${BASE_REQUIREMENTS}"
|
|
145
|
+
if [[ $? -ne 0 ]]; then
|
|
146
|
+
echo "[ERROR] base 依赖安装失败: ${BASE_REQUIREMENTS}"
|
|
147
|
+
exit 1
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
"${PYTHON_BIN}" "${PIP_ZIPAPP}" install \
|
|
151
|
+
--disable-pip-version-check \
|
|
152
|
+
--no-deps \
|
|
153
|
+
--target "${BASE_DEPS_DIR}" \
|
|
154
|
+
-r "${HDF5_REQUIREMENTS}"
|
|
155
|
+
if [[ $? -ne 0 ]]; then
|
|
156
|
+
echo "[ERROR] HDF5 依赖安装失败: ${HDF5_REQUIREMENTS}"
|
|
157
|
+
exit 1
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
if ! PYTHONPATH="${BASE_DEPS_DIR}${PYTHONPATH:+:${PYTHONPATH}}" \
|
|
161
|
+
"${PYTHON_BIN}" -c "${import_check}" >/dev/null 2>&1; then
|
|
162
|
+
echo "[ERROR] base 依赖安装后仍无法导入,请确认设备 Python 已提供兼容的 NumPy。"
|
|
163
|
+
exit 1
|
|
164
|
+
fi
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# ── 停止已有服务 ──────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
stop_existing_services() {
|
|
170
|
+
local pids
|
|
171
|
+
pids="$(pgrep -f "${SERVICE_PATTERN}" || true)"
|
|
172
|
+
if [[ -z "${pids}" ]]; then
|
|
173
|
+
rm -f "${SERVICE_PID_FILE}"
|
|
174
|
+
echo "[INFO] 未发现运行中的云端上传服务。"
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
echo "[INFO] 准备停止云端上传服务 (pid=${pids//$'\n'/, })"
|
|
179
|
+
for pid in ${pids}; do
|
|
180
|
+
kill -TERM "${pid}" 2>/dev/null || true
|
|
181
|
+
done
|
|
182
|
+
|
|
183
|
+
# 等待优雅退出
|
|
184
|
+
for _ in 1 2 3 4 5; do
|
|
185
|
+
local any_alive=0
|
|
186
|
+
for pid in ${pids}; do
|
|
187
|
+
if kill -0 "${pid}" 2>/dev/null; then
|
|
188
|
+
any_alive=1
|
|
189
|
+
fi
|
|
190
|
+
done
|
|
191
|
+
[[ "${any_alive}" -eq 0 ]] && break
|
|
192
|
+
sleep 1
|
|
193
|
+
done
|
|
194
|
+
|
|
195
|
+
# 强制停止
|
|
196
|
+
for pid in ${pids}; do
|
|
197
|
+
if kill -0 "${pid}" 2>/dev/null; then
|
|
198
|
+
echo "[WARN] 进程 ${pid} 未在超时内退出,执行强制停止。"
|
|
199
|
+
kill -KILL "${pid}" 2>/dev/null || true
|
|
200
|
+
fi
|
|
201
|
+
done
|
|
202
|
+
|
|
203
|
+
rm -f "${SERVICE_PID_FILE}"
|
|
204
|
+
echo "[INFO] 云端上传服务已停止。"
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# ── 主流程 ────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
check_env
|
|
210
|
+
|
|
211
|
+
if [[ "${MODE}" == "stop" ]]; then
|
|
212
|
+
echo "[INFO] 模式: stop"
|
|
213
|
+
stop_existing_services
|
|
214
|
+
exit 0
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
if [[ "${MODE}" == "restart" ]]; then
|
|
218
|
+
echo "[INFO] 模式: restart(先停后起)"
|
|
219
|
+
stop_existing_services
|
|
220
|
+
else
|
|
221
|
+
echo "[INFO] 模式: start"
|
|
222
|
+
existing_pids="$(pgrep -f "${SERVICE_PATTERN}" || true)"
|
|
223
|
+
if [[ -n "${existing_pids}" ]]; then
|
|
224
|
+
echo "[ERROR] 已有云端上传服务运行中 (pid=${existing_pids//$'\n'/, })。"
|
|
225
|
+
echo "[ERROR] 如需切换到当前模块,请使用: bash $(basename "$0") restart"
|
|
226
|
+
exit 1
|
|
227
|
+
fi
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
acquire_launcher_lock
|
|
231
|
+
ensure_base_dependencies
|
|
232
|
+
|
|
233
|
+
cd "${RUNTIME_DIR}" || exit 1
|
|
234
|
+
|
|
235
|
+
echo "[INFO] 运行目录: ${RUNTIME_DIR}"
|
|
236
|
+
echo "[INFO] 启动云端上传服务..."
|
|
237
|
+
|
|
238
|
+
# 启动服务(后台运行,模块方式)
|
|
239
|
+
CONSOLE_LOG="${LOG_DIR}/service_console.log"
|
|
240
|
+
nohup env PYTHONPATH="${PACKAGE_PARENT}:${BASE_DEPS_DIR}${PYTHONPATH:+:${PYTHONPATH}}" \
|
|
241
|
+
CLOUD_UPLOAD_LOG_DIR="${LOG_DIR}" \
|
|
242
|
+
CLOUD_UPLOAD_RUNTIME_DIR="${RUNTIME_DIR}" \
|
|
243
|
+
CLOUD_UPLOAD_CONFIG="${CONFIG_PATH}" \
|
|
244
|
+
CLOUD_UPLOAD_W1_DEPS_DIR="${W1_DEPS_DIR}" \
|
|
245
|
+
CLOUD_UPLOAD_W1_REQUIREMENTS="${W1_REQUIREMENTS}" \
|
|
246
|
+
CLOUD_UPLOAD_PIP_ZIPAPP="${PIP_ZIPAPP}" \
|
|
247
|
+
CLOUD_UPLOAD_UMI_PYTHON="${UMI_PYTHON_BIN}" \
|
|
248
|
+
PYTHONUNBUFFERED=1 \
|
|
249
|
+
"${PYTHON_BIN}" -m "dexverse_uploader.${START_MODULE}" >> "${CONSOLE_LOG}" 2>&1 &
|
|
250
|
+
|
|
251
|
+
SERVICE_PID=$!
|
|
252
|
+
echo "${SERVICE_PID}" > "${SERVICE_PID_FILE}"
|
|
253
|
+
sleep 3
|
|
254
|
+
if ! kill -0 "${SERVICE_PID}" 2>/dev/null; then
|
|
255
|
+
rm -f "${SERVICE_PID_FILE}"
|
|
256
|
+
echo "[ERROR] 云端上传服务启动后立即退出。"
|
|
257
|
+
echo "[ERROR] 最近日志: ${CONSOLE_LOG}"
|
|
258
|
+
tail -n 20 "${CONSOLE_LOG}" 2>/dev/null || true
|
|
259
|
+
exit 1
|
|
260
|
+
fi
|
|
261
|
+
echo "[INFO] 云端上传服务已启动 (pid=${SERVICE_PID})"
|
|
262
|
+
echo "[INFO] 日志:"
|
|
263
|
+
echo " tail -f ${CONSOLE_LOG}"
|
|
264
|
+
echo " tail -f ${LOG_DIR}/mqtt_client.log"
|
|
265
|
+
echo " tail -f ${LOG_DIR}/file_scanner.log"
|
|
266
|
+
echo " tail -f ${LOG_DIR}/upload_manager.log"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""从任意名称的发布目录运行 dexverse_uploader 模块。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import importlib.util
|
|
7
|
+
import runpy
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
PACKAGE_NAME = "dexverse_uploader"
|
|
13
|
+
PACKAGE_PARENT = Path(__file__).resolve().parent / "src"
|
|
14
|
+
PACKAGE_ROOT = PACKAGE_PARENT / PACKAGE_NAME
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_package() -> None:
|
|
18
|
+
"""将源码布局中的固定包名注册给测试和源码运行器。"""
|
|
19
|
+
if PACKAGE_NAME in sys.modules:
|
|
20
|
+
return
|
|
21
|
+
spec = importlib.util.spec_from_file_location(
|
|
22
|
+
PACKAGE_NAME,
|
|
23
|
+
PACKAGE_ROOT / "__init__.py",
|
|
24
|
+
submodule_search_locations=[str(PACKAGE_ROOT)],
|
|
25
|
+
)
|
|
26
|
+
if spec is None or spec.loader is None:
|
|
27
|
+
raise RuntimeError(f"无法加载上传模块: {PACKAGE_ROOT}")
|
|
28
|
+
package = importlib.util.module_from_spec(spec)
|
|
29
|
+
sys.modules[PACKAGE_NAME] = package
|
|
30
|
+
spec.loader.exec_module(package)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main() -> None:
|
|
34
|
+
"""运行命令行指定的子模块(支持点分隔的协议路径)。"""
|
|
35
|
+
if len(sys.argv) < 2:
|
|
36
|
+
raise SystemExit(f"用法: {Path(sys.argv[0]).name} <module> [args...]")
|
|
37
|
+
module_name = sys.argv.pop(1)
|
|
38
|
+
load_package()
|
|
39
|
+
sys.argv[0] = str(PACKAGE_ROOT.joinpath(*module_name.split(".")).with_suffix(".py"))
|
|
40
|
+
runpy.run_module(f"{PACKAGE_NAME}.{module_name}", run_name="__main__")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dexverse-uploader"
|
|
7
|
+
version = "0.1.4a1"
|
|
8
|
+
description = "DexVerse edge-device data uploader"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10,<3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"paho-mqtt>=2.0,<3",
|
|
13
|
+
"requests>=2.25,<3",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
dexverse-uploader = "dexverse_uploader.device_main:main"
|
|
18
|
+
dexverse-uploader-converter = "dexverse_uploader.converter_worker:main"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
# These profiles are intentionally separate: edge images may provide some of
|
|
22
|
+
# the native/system components themselves, while wheel-based environments can
|
|
23
|
+
# opt into the corresponding Python dependencies.
|
|
24
|
+
hdf5 = [
|
|
25
|
+
"h5py>=3,<4",
|
|
26
|
+
]
|
|
27
|
+
w1 = [
|
|
28
|
+
"h5py>=3,<4",
|
|
29
|
+
"numpy>=1.24,<3",
|
|
30
|
+
"opencv-python-headless>=4.8,<5",
|
|
31
|
+
"Pillow>=10,<13",
|
|
32
|
+
"PyYAML>=5.1,<7",
|
|
33
|
+
"tqdm>=4.66,<5",
|
|
34
|
+
]
|
|
35
|
+
piper = [
|
|
36
|
+
"numpy>=1.24,<3",
|
|
37
|
+
"h5py>=3.8,<4",
|
|
38
|
+
"Pillow>=10,<13",
|
|
39
|
+
]
|
|
40
|
+
tianji = [
|
|
41
|
+
"mcap==1.4.0",
|
|
42
|
+
"mcap-ros2-support==0.5.7",
|
|
43
|
+
"lz4>=4.3,<5",
|
|
44
|
+
"zstandard>=0.23,<1",
|
|
45
|
+
"av>=8.1,<19",
|
|
46
|
+
"h5py>=3.6,<4",
|
|
47
|
+
"numpy>=1.24,<2",
|
|
48
|
+
"h5ffmpeg==2.4.0",
|
|
49
|
+
"imgaug==0.4.0",
|
|
50
|
+
"Pillow>=9,<12",
|
|
51
|
+
"PyYAML>=6,<7",
|
|
52
|
+
]
|
|
53
|
+
umi = [
|
|
54
|
+
"h5ffmpeg==2.4.0",
|
|
55
|
+
"h5py==3.14.0",
|
|
56
|
+
"numpy==1.26.4",
|
|
57
|
+
"scipy>=1.11,<2",
|
|
58
|
+
"opencv-python-headless>=4.8,<5",
|
|
59
|
+
"hdfdict==0.3.1",
|
|
60
|
+
"tqdm>=4.66,<5",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[dependency-groups]
|
|
64
|
+
dev = [
|
|
65
|
+
"mypy>=1.15,<2",
|
|
66
|
+
"pytest>=8.3,<10",
|
|
67
|
+
"ruff>=0.11,<1",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[tool.hatch.build.targets.wheel]
|
|
71
|
+
packages = ["src/dexverse_uploader"]
|
|
72
|
+
|
|
73
|
+
[tool.hatch.build.targets.sdist]
|
|
74
|
+
include = [
|
|
75
|
+
"/pyproject.toml",
|
|
76
|
+
"/README.md",
|
|
77
|
+
"/src/dexverse_uploader",
|
|
78
|
+
"/requirements",
|
|
79
|
+
"/cloud_upload.sh",
|
|
80
|
+
"/package_runner.py",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
[tool.pytest.ini_options]
|
|
84
|
+
addopts = "-ra"
|
|
85
|
+
testpaths = ["tests"]
|
|
86
|
+
pythonpath = ["src"]
|
|
87
|
+
|
|
88
|
+
[tool.ruff]
|
|
89
|
+
target-version = "py310"
|
|
90
|
+
line-length = 120
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint]
|
|
93
|
+
select = ["E", "F"]
|
|
94
|
+
ignore = ["E501"]
|
|
95
|
+
|
|
96
|
+
[tool.mypy]
|
|
97
|
+
python_version = "3.10"
|
|
98
|
+
check_untyped_defs = true
|
|
99
|
+
disallow_any_generics = false
|
|
100
|
+
disallow_incomplete_defs = false
|
|
101
|
+
disallow_untyped_defs = false
|
|
102
|
+
warn_return_any = false
|
|
103
|
+
warn_unreachable = true
|
|
104
|
+
ignore_missing_imports = true
|
|
105
|
+
files = ["src"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 天机 converter worker;首次转换时按需安装到模块私有目录。
|
|
2
|
+
mcap==1.4.0
|
|
3
|
+
mcap-ros2-support==0.5.7
|
|
4
|
+
lz4>=4.3,<5
|
|
5
|
+
zstandard>=0.23,<1
|
|
6
|
+
av>=8.1,<19
|
|
7
|
+
h5py>=3.6,<4
|
|
8
|
+
numpy>=1.24,<2
|
|
9
|
+
h5ffmpeg==2.4.0
|
|
10
|
+
# MR-compatible optional resize path (NumPy must remain <2 for imgaug).
|
|
11
|
+
imgaug==0.4.0
|
|
12
|
+
Pillow>=9,<12
|
|
13
|
+
PyYAML>=6,<7
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2021-2025 DexForce Technology Co., Ltd.
|
|
3
|
+
#
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
# ----------------------------------------------------------------------------
|
|
6
|
+
"""云端上传模块 — 将录制的遥操数据上传到云端服务器。
|
|
7
|
+
|
|
8
|
+
控制面:MQTT RPC(get_data / upload)
|
|
9
|
+
数据面:HTTP PUT 到 MINIO presigned-url
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dexverse_uploader.chunk_uploader import ChunkUploader
|
|
13
|
+
from dexverse_uploader.converter import (
|
|
14
|
+
Converter,
|
|
15
|
+
ConverterRegistry,
|
|
16
|
+
TianjiSubprocessConverter,
|
|
17
|
+
UmiSubprocessConverter,
|
|
18
|
+
W1Converter,
|
|
19
|
+
default_registry,
|
|
20
|
+
)
|
|
21
|
+
from dexverse_uploader.file_scanner import FileScanner
|
|
22
|
+
from dexverse_uploader.quality_profiles import (
|
|
23
|
+
QualityProfileError,
|
|
24
|
+
build_default_profile,
|
|
25
|
+
quality_templates,
|
|
26
|
+
validate_quality_profile,
|
|
27
|
+
)
|
|
28
|
+
from dexverse_uploader.quality_result import build_quality_result, compact_quality_result
|
|
29
|
+
from dexverse_uploader.quality_report_store import QualityReportStore
|
|
30
|
+
from dexverse_uploader.upload_manager import UploadManager
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"ChunkUploader",
|
|
34
|
+
"Converter",
|
|
35
|
+
"ConverterRegistry",
|
|
36
|
+
"FileScanner",
|
|
37
|
+
"QualityProfileError",
|
|
38
|
+
"QualityReportStore",
|
|
39
|
+
"TianjiSubprocessConverter",
|
|
40
|
+
"UmiSubprocessConverter",
|
|
41
|
+
"UploadManager",
|
|
42
|
+
"W1Converter",
|
|
43
|
+
"default_registry",
|
|
44
|
+
"build_default_profile",
|
|
45
|
+
"build_quality_result",
|
|
46
|
+
"compact_quality_result",
|
|
47
|
+
"quality_templates",
|
|
48
|
+
"validate_quality_profile",
|
|
49
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2021-2025 DexForce Technology Co., Ltd.
|
|
3
|
+
#
|
|
4
|
+
# All rights reserved.
|
|
5
|
+
# ----------------------------------------------------------------------------
|
|
6
|
+
"""chunk_uploader.py — 文件上传器:抽样摘要计算 + HTTP PUT 到 presigned-url。"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Callable, Dict
|
|
13
|
+
|
|
14
|
+
from dexverse_uploader.logger import get_logger
|
|
15
|
+
from dexverse_uploader.sample_hash import HASH_TYPE, compute_sample_hash_bytes, file_signature
|
|
16
|
+
|
|
17
|
+
logger = get_logger("chunk_uploader")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ChunkUploader:
|
|
21
|
+
"""文件上传器。
|
|
22
|
+
|
|
23
|
+
云端接口模式:接收 presigned-url → HTTP PUT 整个文件 → 返回抽样摘要。
|
|
24
|
+
不再做分片。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def upload_file(
|
|
31
|
+
self,
|
|
32
|
+
file_path: Path,
|
|
33
|
+
presigned_url: str,
|
|
34
|
+
put_cb: Callable[[str, bytes], bool],
|
|
35
|
+
) -> Dict:
|
|
36
|
+
"""上传单个文件到 MINIO presigned-url。
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
file_path: 文件路径
|
|
40
|
+
presigned_url: MINIO presigned upload URL
|
|
41
|
+
put_cb: HTTP PUT 回调 (url, file_bytes) -> bool
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
{"success": bool, "content_hash": str, "hash_type": str, "size": int, "error": str}
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
with open(file_path, "rb") as f:
|
|
48
|
+
before = os.fstat(f.fileno())
|
|
49
|
+
data = f.read()
|
|
50
|
+
after = os.fstat(f.fileno())
|
|
51
|
+
if (file_signature(before) != file_signature(after)
|
|
52
|
+
or file_signature(after) != file_signature(Path(file_path).stat())
|
|
53
|
+
or len(data) != before.st_size):
|
|
54
|
+
raise OSError("文件在上传读取期间发生变化,请重试")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return {"success": False, "content_hash": "", "hash_type": "", "size": 0, "error": str(e)}
|
|
57
|
+
|
|
58
|
+
content_hash = compute_sample_hash_bytes(data)
|
|
59
|
+
file_size = len(data)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
ok = put_cb(presigned_url, data)
|
|
63
|
+
if ok:
|
|
64
|
+
return {"success": True, "content_hash": content_hash, "hash_type": HASH_TYPE, "size": file_size, "error": ""}
|
|
65
|
+
else:
|
|
66
|
+
return {"success": False, "content_hash": content_hash, "hash_type": HASH_TYPE, "size": file_size, "error": "PUT failed"}
|
|
67
|
+
except Exception as e:
|
|
68
|
+
error_type = type(e).__name__
|
|
69
|
+
logger.error("上传回调异常: %s", error_type)
|
|
70
|
+
return {
|
|
71
|
+
"success": False,
|
|
72
|
+
"content_hash": content_hash,
|
|
73
|
+
"hash_type": HASH_TYPE,
|
|
74
|
+
"size": file_size,
|
|
75
|
+
"error": f"PUT callback exception: {error_type}",
|
|
76
|
+
}
|