# Hawkbit DDI Client — 运行流程文档 ## 目录 1. [整体架构](#1-整体架构) 2. [首次注册流程](#2-首次注册流程) 3. [守护进程轮询流程](#3-守护进程轮询流程) 4. [单次轮询流程](#4-单次轮询流程) 5. [OTA 部署流程](#5-ota-部署流程) 6. [认证流程](#6-认证流程) 7. [取消任务流程](#7-取消任务流程) 8. [确认任务流程](#8-确认任务流程) 9. [断点续传流程](#9-断点续传流程) 10. [错误处理流程](#10-错误处理流程) 11. [安装钩子定制](#11-安装钩子定制) 12. [部署与运维](#12-部署与运维) --- ## 1. 整体架构 ``` ┌──────────────────────────────────────────────────────────┐ │ ddi-client.py │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 属性检测 │ │ 认证模块 │ │ 轮询模块 │ │ 安装钩子 │ │ │ │ (SN/OS/ │ │ Gateway/ │ │ Poll/ │ │ _install()│ │ │ │ HW/Kernel│ │ Target │ │ Deploy/ │ │ 可覆盖 │ │ │ │ ...) │ │ Token │ │ Download │ │ │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ HTTP 层 │ │ │ │ (urllib + SSL context + Range 支持) │ │ │ └──────────────────────┬───────────────────────────┘ │ └─────────────────────────┼────────────────────────────────┘ │ ▼ ┌──────────────────────┐ │ Hawkbit Server │ │ DDI API v1 (9090) │ └──────────────────────┘ ``` ### 数据流 ``` 属性文件 ──┐ ├──→ 属性检测 ──→ PUT /configData ──→ Hawkbit 记录 target attributes SN 检测 ──┘ 轮询 ──→ GET /controller/v1/{SN} ←── deploymentBase / cancelAction / confirmationBase / configData / 无 部署 ──→ GET /deploymentBase/{id} ←── chunks[].artifacts[] 下载 ──→ GET /softwaremodules/{id}/... ←── 二进制流 (Range 支持) 反馈 ──→ POST /feedback ←── 200 OK ``` --- ## 2. 首次注册流程 **触发条件**:设备 SN 在 Hawkbit 中不存在(或脚本首次连接后重启) ``` 启动参数: ddi-client.py -u http://192.168.77.70:8080 -t DEFAULT \ --gateway-token @/etc/hawkbit/gateway.key ═══════════════════════════════════════════════════════════════ Step 1: 读取 Gateway Token ═══════════════════════════════════════════════════════════════ 输入: @/etc/hawkbit/gateway.key 动作: open("/etc/hawkbit/gateway.key") → read() → strip() 输出: Authorization: GatewayToken <文件内容> 备选: --gateway-token <明文key> 直接传入 --target-token <明文token> 直接传入 --target-token @/etc/hawkbit/target.token 文件读取 ═══════════════════════════════════════════════════════════════ Step 2: 检测设备 SN ═══════════════════════════════════════════════════════════════ 优先级: ① getprop ro.serialno → Android 序列号 ② /sys/class/dmi/id/product_serial → Linux DMI (x86) ③ /sys/devices/virtual/dmi/id/... → Linux DMI (ARM/嵌入式) ④ /etc/machine-id → systemd 机器 ID ⑤ socket.gethostname() → 兜底 跳过条件: --controller-id 显式指定 输出: controllerId = "89aa36c5b6744dc2910479f7ecd6d2c3" ═══════════════════════════════════════════════════════════════ Step 3: 检测设备属性 ═══════════════════════════════════════════════════════════════ 自动检测: osType → /system/build.prop 存在 ? "android" : "linux" platform → platform.machine() # x86_64 / aarch64 kernel → os.uname().release # 6.17.0-35-generic hostname → socket.gethostname() hwRevision → DMI product_name + product_version manufacturer→ DMI sys_vendor / Android ro.product.manufacturer Android 额外属性: androidVersion → ro.build.version.release productModel → ro.product.model buildNumber → ro.build.version.incremental 用户属性文件 (/etc/hawkbit/device_attrs.json): {"dept": "production", "region": "east", "owner": "team-a"} 版本文件 (/etc/hawkbit/current_version) : V1.0.0.0 → swVersion ═══════════════════════════════════════════════════════════════ Step 4: 上报属性 ═══════════════════════════════════════════════════════════════ PUT http://host/DEFAULT/controller/v1/{SN}/configData Body: { "mode": "merge", "data": { "osType": "linux", "platform": "x86_64", "kernel": "6.17.0-35-generic", "hostname": "device-001", "hwRevision": "VMware Virtual Platform", "manufacturer": "VMware, Inc.", "swVersion": "V1.0.0.0", "dept": "production", "region": "east" } } mode 说明: "merge" → 只更新传入的 key,保留已有属性 (推荐) "replace" → 完全替换 "remove" → 删除传入的 key ═══════════════════════════════════════════════════════════════ Step 5: 首次轮询(自动注册) ═══════════════════════════════════════════════════════════════ GET http://host/DEFAULT/controller/v1/{SN} Hawkbit 行为: findOrRegisterTargetIfItDoesNotExist(SN) → 不存在 → INSERT INTO target (controller_id, ip_address, ...) → 存在 → UPDATE last_target_query 响应: 无 action: {"config": {"polling": {"sleep": "00:05:00"}}, "_links": {}} → sleep 5分钟 有 deployment: "_links": { "deploymentBase": {"href": "http://.../deploymentBase/1?c=..."}, "configData": {"href": "http://.../configData"} } → 进入 OTA 部署流程 有 cancel: "_links": {"cancelAction": {"href": "http://.../cancelAction/..."}} → 进入取消流程 有 confirmation: "_links": {"confirmationBase": {"href": "http://.../confirmationBase"}} → 进入确认流程 ``` --- ## 3. 守护进程轮询流程 ``` 启动命令: ddi-client.py -u http://192.168.77.70:8080 -t DEFAULT \ --gateway-token my-key -i 60 ┌─────────────────────────────────────────────────┐ │ 守护模式 (daemon=True) │ ├─────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ │ │ │ 启动/打印 │ ╔═══════════════════╗ │ │ │ banner │ ║ Server: host ║ │ │ └────┬─────┘ ║ Tenant: DEFAULT ║ │ │ │ ║ Device: ║ │ │ ▼ ║ Auth: Gateway ║ │ │ ┌──────────┐ ╚═══════════════════╝ │ │ │ 上报属性 │ PUT /configData │ │ │(首次) │ (仅一次, 成功即跳过) │ │ └────┬─────┘ │ │ │ │ │ ▼ │ │ ┌──────────┐ │ │ │ 轮询 │ GET /controller/v1/{SN} │ │ │ Poll │ │ │ └────┬─────┘ │ │ │ │ │ ┌───┴───┬───────────┬───────────┐ │ │ ▼ ▼ ▼ ▼ │ │ deploy cancel confirm 无任务 │ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ │ OTA流程 取消流程 确认流程 │ │ │ │ │ │ │ │ │ └───┴───┴─────────┴───────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ sleep(N 秒) │ N = max(server建议, │ │ │ │ 默认60s) │ │ └──────┬───────┘ │ │ │ │ │ └──────→ 回到轮询 ◄── 循环 │ │ │ │ 退出: Ctrl+C (KeyboardInterrupt) │ │ max_cycles 达到上限 │ └─────────────────────────────────────────────────┘ 轮询间隔策略: - 默认: --polling-interval 60 (或 -i 指定) - 服务端可覆盖: response.config.polling.sleep → "00:05:00" → 5分钟 - 有活跃 action 时: 服务端可能缩短间隔 (getPollingTimeForAction) - 轮询失败时: 退避到 min(polling_interval, 30s) ``` --- ## 4. 单次轮询流程 ``` 启动命令: ddi-client.py -u http://host:9090 -t DEFAULT \ --gateway-token my-key --once ┌───────────────────────────────────────────────────────────┐ │ 单次模式 (daemon=False, --once) │ ├───────────────────────────────────────────────────────────┤ │ │ │ ① 上报属性 (首次) │ │ │ │ │ ▼ │ │ ② 轮询 GET /controller/v1/{SN} │ │ │ │ │ ┌───┴───┬───────────┬───────────┐ │ │ ▼ ▼ ▼ ▼ │ │ deploy cancel confirm 无任务 │ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ │ OTA流程 取消流程 确认流程 │ │ │ │ │ │ │ │ │ └───┴───┴─────────┴───────────┘ │ │ │ │ │ ▼ │ │ ③ sys.exit(0) │ │ │ │ 用途: │ │ - systemd timer / cron 定时触发 │ │ - 测试/调试 │ │ - 脚本化: && 链式调用后续操作 │ └───────────────────────────────────────────────────────────┘ cron 示例: */5 * * * * /usr/bin/python3 /opt/hawkbit/ddi-client.py \ -u https://hawkbit:9090 -t DEFAULT \ --gateway-token @/etc/hawkbit/gateway.key --once systemd timer 示例: [Unit] Description=Hawkbit DDI poll [Service] ExecStart=/usr/bin/python3 /opt/hawkbit/ddi-client.py \ -u https://hawkbit:9090 -t DEFAULT \ --gateway-token @/etc/hawkbit/gateway.key --once [Timer] OnBootSec=30s OnUnitActiveSec=300s ``` --- ## 5. OTA 部署流程 ``` 前置条件: 轮询返回 _links.deploymentBase ┌───────────────────────────────────────────────────────────────┐ │ OTA 部署完整生命周期 │ ├───────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Step A: 拉取部署详情 │ │ │ │ │ │ │ │ GET /deploymentBase/{actionId}?c=... │ │ │ │ ← { │ │ │ │ "id": "2", │ │ │ │ "deployment": { │ │ │ │ "download": "forced", │ │ │ │ "update": "forced", │ │ │ │ "chunks": [{ │ │ │ │ "part": "os", │ │ │ │ "version": "V1.0.0.0", │ │ │ │ "name": "r3576-OS-android", │ │ │ │ "artifacts": [{ │ │ │ │ "filename": "update.zip", │ │ │ │ "size": 195411, │ │ │ │ "hashes": { │ │ │ │ "sha256": "438b8f9e...", │ │ │ │ "md5": "28a17f5f...", │ │ │ │ "sha1": "223377ea..." │ │ │ │ }, │ │ │ │ "_links": { │ │ │ │ "download-http": { │ │ │ │ "href": "http://.../update.zip" │ │ │ │ } │ │ │ │ } │ │ │ │ }] │ │ │ │ }] │ │ │ │ } │ │ │ │ } │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Step B: 上报 DOWNLOAD (开始下载) │ │ │ │ │ │ │ │ POST /deploymentBase/{id}/feedback │ │ │ │ {"status": { │ │ │ │ "execution": "download", │ │ │ │ "result": {"finished": "none"}, │ │ │ │ "details": ["Starting download"] │ │ │ │ }} │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Step C: 下载 artifacts (逐个) │ │ │ │ │ │ │ │ 对每个 chunk.artifacts[]: │ │ │ │ │ │ │ │ ① 提取 download URL (优先级) │ │ │ │ download (HTTPS) > download-http (HTTP) │ │ │ │ 失败 → fallback: 遍历所有 HAL link 提取 smId │ │ │ │ │ │ │ │ ② 检查本地缓存 /tmp/hawkbit/{filename} │ │ │ │ 存在 + SHA256 匹配 → 跳过下载 │ │ │ │ 存在 + SHA256 不匹配 → 重新下载 │ │ │ │ │ │ │ │ ③ 检查 .tmp 断点文件 │ │ │ │ 存在 → 加 Range: bytes={size}- 头续传 │ │ │ │ │ │ │ │ ④ GET /softwaremodules/{id}/artifacts/{filename} │ │ │ │ 分块读取 64KB, 写入 .tmp │ │ │ │ 打印下载进度百分比 │ │ │ │ │ │ │ │ ⑤ SHA256 校验 .tmp 文件 │ │ │ │ 成功 → rename .tmp → 最终文件 │ │ │ │ 失败 → 返回 None │ │ │ │ │ │ │ │ ⑥ 上报 PROCEEDING (每 artifact) │ │ │ │ execution=proceeding, progress={cnt:1, of:1} │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Step D: 上报 DOWNLOADED (全部下载完成) │ │ │ │ │ │ │ │ POST /deploymentBase/{id}/feedback │ │ │ │ {"status": { │ │ │ │ "execution": "downloaded", │ │ │ │ "result": {"finished": "none"}, │ │ │ │ "details": ["Downloaded 1 artifact(s)"] │ │ │ │ }} │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Step E: 上报 PROCEEDING (开始安装) │ │ │ │ │ │ │ │ POST /deploymentBase/{id}/feedback │ │ │ │ {"status": { │ │ │ │ "execution": "proceeding", │ │ │ │ "result": {"finished": "none"}, │ │ │ │ "details": ["Installing update …"] │ │ │ │ }} │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Step F: 执行安装 _install(files, handling) │ │ │ │ │ │ │ │ 默认行为: 检测到 *.zip 文件 → 返回 True │ │ │ │ 生产: 覆盖此方法执行实际刷写 │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ │ │ ┌────┴────┐ │ │ ▼ ▼ │ │ 成功 失败 │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────────────────────┐ │ │ │ Step G: 上报 CLOSED (最终) │ │ │ │ │ │ │ │ 成功: │ │ │ │ execution=closed │ │ │ │ result.finished=success │ │ │ │ code=200 │ │ │ │ │ │ │ │ 失败: │ │ │ │ execution=closed │ │ │ │ result.finished=failure │ │ │ │ code=500 │ │ │ └───────────────────────────────┘ │ │ │ │ → 返回主循环 sleep → 下次轮询 │ └────────────────────────────────────────────────────────────────┘ Hawkbit Action Status 映射: execution=download → Status.DOWNLOAD execution=downloaded → Status.DOWNLOADED execution=proceeding → Status.RUNNING execution=closed → result.success → Status.FINISHED → result.failure → Status.ERROR execution=canceled → Status.CANCELED execution=rejected → Status.WARNING ``` --- ## 6. 认证流程 ``` ┌───────────────────────────────────────────────────────────┐ │ 认证方式选择 │ ├───────────────────────────────────────────────────────────┤ │ │ │ ① GatewayToken (推荐,所有设备共享) │ │ ────────────────────────────────────── │ │ 服务端配置: │ │ curl -u admin:admin -X PUT \ │ │ .../system/configs/authentication.gatewaytoken.key \│ │ -d '{"value": "my-shared-key"}' │ │ │ │ curl -u admin:admin -X PUT \ │ │ .../system/configs/authentication.gatewaytoken.enabled \│ │ -d '{"value": true}' │ │ │ │ 客户端: │ │ 方式 A: 命令行参数 │ │ --gateway-token my-shared-key │ │ │ │ 方式 B: 从文件读取 │ │ echo "my-shared-key" > /etc/hawkbit/gateway.key │ │ chmod 600 /etc/hawkbit/gateway.key │ │ --gateway-token @/etc/hawkbit/gateway.key │ │ │ │ HTTP: Authorization: GatewayToken my-shared-key │ │ │ │ │ │ ② TargetToken (每设备独立令牌) │ │ ──────────────────────────────── │ │ 服务端配置: │ │ curl -u admin:admin -X PUT \ │ │ .../system/configs/authentication.targettoken.enabled \│ │ -d '{"value": true}' │ │ │ │ # 每个 target 的 securityToken 自动生成或手动设置 │ │ curl -u admin:admin -X PUT \ │ │ .../targets/{controllerId} \ │ │ -d '{"securityToken": "device-unique-token"}' │ │ │ │ 客户端: │ │ --target-token device-unique-token │ │ 或 --target-token @/etc/hawkbit/target.token │ │ │ │ HTTP: Authorization: TargetToken device-unique-token │ │ │ │ │ │ ③ SecurityHeader (反向代理模式, 脚本不支持) │ │ ────────────────────────────────────────── │ │ 适用于 nginx/Envoy 前置认证场景 │ │ │ │ │ │ 优先级 (客户端): --gateway-token > --target-token │ │ 无认证: 服务器返回 401 │ │ 验证失败: 服务器返回 401 │ └───────────────────────────────────────────────────────────┘ ``` --- ## 7. 取消任务流程 ``` 前置条件: 轮询返回 _links.cancelAction ┌───────────────────────────────────────────────────┐ │ 取消任务处理 │ ├───────────────────────────────────────────────────┤ │ │ │ ① GET /cancelAction/{actionId} │ │ ← {"id": "11", │ │ "cancelAction": {"stopId": "11"}} │ │ │ │ ② POST /cancelAction/{actionId}/feedback │ │ Body: │ │ {"status": { │ │ "execution": "canceled", │ │ "result": {"finished": "none"}, │ │ "details": ["Cancel acknowledged"] │ │ }} │ │ │ │ → 回到主循环 sleep → 下次轮询 │ └───────────────────────────────────────────────────┘ ``` --- ## 8. 确认任务流程 ``` 前置条件: action 状态为 WAITING_FOR_CONFIRMATION ┌───────────────────────────────────────────────────┐ │ 确认任务处理 (headless) │ ├───────────────────────────────────────────────────┤ │ │ │ Headless 设备自动确认,不等待人工: │ │ │ │ POST /confirmationBase/{actionId}/feedback │ │ Body: │ │ {"confirmation": "confirmed", │ │ "code": 200, │ │ "details": ["Auto-confirmed by DDI client"]} │ │ │ │ → Action 状态从 WAITING_FOR_CONFIRMATION │ │ 转为 RUNNING │ │ → 下次轮询拿到 deploymentBase 链接 │ │ → 进入正常 OTA 流程 │ │ │ │ 如果 autoConfirm.active=true: │ │ 下次轮询时服务端直接返回 deploymentBase │ │ 跳过确认步骤 │ └───────────────────────────────────────────────────┘ ``` --- ## 9. 断点续传流程 ``` ┌───────────────────────────────────────────────────────────┐ │ 断点续传 (Range 请求) │ ├───────────────────────────────────────────────────────────┤ │ │ │ 触发条件: --no-resume 未设置 (默认启用) │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ 下载 {filename} 之前 │ │ │ │ │ │ │ │ ① 检查 /tmp/hawkbit/{filename} │ │ │ │ 存在 + SHA256 匹配 │ │ │ │ → 跳过下载,直接返回本地路径 │ │ │ │ │ │ │ │ ② 检查 /tmp/hawkbit/{filename}.tmp │ │ │ │ 存在 → os.path.getsize(tmp_path) = 1048576 │ │ │ │ 添加头: Range: bytes=1048576- │ │ │ │ 打开模式: "ab" (追加) │ │ │ │ │ │ │ │ ③ 文件不存在 │ │ │ │ 无 Range 头 │ │ │ │ 打开模式: "wb" (新建) │ │ │ └──────────────────────┬──────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ GET /softwaremodules/{id}/artifacts/{filename} │ │ │ │ Range: bytes=1048576- │ │ │ │ ← 206 Partial Content │ │ │ │ Content-Range: bytes 1048576-195410/195411 │ │ │ │ (服务端从 offset 开始发送) │ │ │ └──────────────────────┬──────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ 分块写入 + 进度报告 │ │ │ │ │ │ │ │ while chunk = resp.read(64KB): │ │ │ │ fh.write(chunk) │ │ │ │ downloaded += len(chunk) │ │ │ │ pct = downloaded / total * 100 │ │ │ │ │ │ │ │ 中断 (Exception / 网络断): │ │ │ │ .tmp 文件保留 │ │ │ │ 下次启动时自动续传 │ │ │ └──────────────────────┬──────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ SHA256 校验通过 │ │ │ │ → os.rename(tmp_path, local_path) 原子重命名 │ │ │ │ │ │ │ │ SHA256 校验失败 │ │ │ │ → 日志记录 mismatch │ │ │ │ → 返回 None │ │ │ │ → 调用方删除 .tmp 或手动恢复 │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 禁用续传: │ │ --no-resume → 每次都从头下载 │ │ → 每次 .tmp 从 0 开始 │ └───────────────────────────────────────────────────────────┘ ``` --- ## 10. 错误处理流程 ``` ┌───────────────────────────────────────────────────────────────┐ │ 错误处理矩阵 │ ├───────────────┬───────────────────────┬───────────────────────┤ │ 场景 │ 行为 │ 恢复 │ ├───────────────┼───────────────────────┼───────────────────────┤ │ 轮询 HTTP 错误 │ 日志记录,退避 sleep │ 下次轮询自动重试 │ │ (network/500) │ min(interval, 30s) │ │ │ │ 守护模式继续 │ │ │ │ --once: exit(1) │ │ ├───────────────┼───────────────────────┼───────────────────────┤ │ 获取部署详情 │ send_feedback(closed, │ 下次轮询后 action │ │ 失败 │ failure, "Cannot │ 可能还在 pending │ │ │ fetch deployment") │ 状态,重新处理 │ │ │ return False │ │ ├───────────────┼───────────────────────┼───────────────────────┤ │ 下载失败 │ send_feedback(closed, │ .tmp 保留 │ │ (HTTP error) │ failure, "Download │ 下次轮询自动续传 │ │ │ failed for xxx") │ │ │ │ return False │ │ ├───────────────┼───────────────────────┼───────────────────────┤ │ SHA256 不匹配 │ 日志 error │ .tmp 保留,可手动删除 │ │ │ "SHA256 MISMATCH" │ 下次重新下载 │ │ │ return None │ │ ├───────────────┼───────────────────────┼───────────────────────┤ │ 下载中断 │ 日志 error │ .tmp 保留 │ │ (Exception) │ "Download interrupted" │ 下次自动续传 │ │ │ return None │ │ ├───────────────┼───────────────────────┼───────────────────────┤ │ 安装失败 │ send_feedback(closed, │ 下次轮询后 action │ │ (_install 返 │ failure, code=500) │ 状态 → ERROR │ │ 回 False) │ return False │ 管理端可见 │ ├───────────────┼───────────────────────┼───────────────────────┤ │ SSL 证书错误 │ 捕获后当作普通 HTTP 错误 │ --no-ssl-verify │ │ │ │ 跳过证书验证(不安全) │ ├───────────────┼───────────────────────┼───────────────────────┤ │ 属性上报失败 │ 日志 error │ _attrs_reported=False │ │ │ 不影响轮询继续 │ 下次轮询自动重试 │ ├───────────────┼───────────────────────┼───────────────────────┤ │ Ctrl+C 中断 │ 日志 "Stopped" │ return True │ │ │ sys.exit(0) │ │ └───────────────┴───────────────────────┴───────────────────────┘ 日志级别: DEBUG - HTTP 请求/响应详情, 下载进度, 属性内容 (--verbose / -v) INFO - 状态变化, action 处理, 睡眠倒计时 (默认) WARNING- 非致命错误 (poll 失败, 缓存失效, 无认证) (默认) ERROR - 致命错误 (下载失败, SHA256 不匹配, HTTP 错误) (默认) ``` --- ## 11. 安装钩子定制 ``` ┌───────────────────────────────────────────────────────────┐ │ _install() 钩子定制 │ ├───────────────────────────────────────────────────────────┤ │ │ │ 默认行为: │ │ 检测下载文件列表中是否有 *.zip 文件 → 返回 True │ │ (适合测试,不做实际操作) │ │ │ │ 输入: │ │ files: List[str] → ["/tmp/hawkbit/update.zip"] │ │ handling: dict → {"download":"forced", │ │ "update":"forced", │ │ "maintenanceWindow":null} │ │ │ │ 返回: │ │ True → send_feedback(closed, success) │ │ False → send_feedback(closed, failure) │ │ │ ├───────────────────────────────────────────────────────────┤ │ │ │ 方式 A: 子类继承 │ │ ───────────────── │ │ │ │ # my_client.py │ │ from ddi_client import HawkbitDDIClient │ │ │ │ class AndroidOTAClient(HawkbitDDIClient): │ │ def _install(self, files, handling): │ │ import subprocess │ │ for fp in files: │ │ if fp.endswith('.zip'): │ │ # 写 recovery 命令 │ │ subprocess.run([ │ │ 'update_engine_client', │ │ '--update', │ │ f'file://{fp}' │ │ ], check=True) │ │ # 写版本号 │ │ with open('/etc/hawkbit/current_version', │ │ 'w') as f: │ │ f.write('V2.0.0.0') │ │ return True │ │ │ │ if __name__ == '__main__': │ │ # 用自定义子类替换 CLI 入口 │ │ client = AndroidOTAClient(...) │ │ client.run() │ │ │ │ │ │ 方式 B: Monkey-Patch (快速原型) │ │ ───────────────────────────── │ │ │ │ client = HawkbitDDIClient(...) │ │ original_install = client._install │ │ def my_install(files, handling): │ │ result = subprocess.run(['unzip', '-o', files[0], │ │ '-d', '/data/ota']) │ │ with open('/etc/hawkbit/current_version','w') as f: │ │ f.write('V2.0.0.0') │ │ return result.returncode == 0 │ │ client._install = my_install │ │ client.run() │ │ │ │ │ │ 方式 C: 简单模式 (不需要 Python 继承) │ │ ─────────────────────────────────── │ │ │ │ 创建安装脚本: /etc/hawkbit/install.sh │ │ │ │ #!/bin/sh │ │ unzip -o "$1" -d /data/ota │ │ echo "V2.0.0.0" > /etc/hawkbit/current_version │ │ exit $? │ │ │ │ 修改 _install: │ │ def _install(self, files, handling): │ │ for fp in files: │ │ if fp.endswith('.zip'): │ │ import subprocess │ │ result = subprocess.run( │ │ ['/etc/hawkbit/install.sh', fp]) │ │ return result.returncode == 0 │ │ return True │ └───────────────────────────────────────────────────────────┘ ``` --- ## 12. 部署与运维 ``` ┌───────────────────────────────────────────────────────────────┐ │ 部署方案 │ ├───────────────────────────────────────────────────────────────┤ │ │ │ Android 设备部署: │ │ ──────────────── │ │ │ │ # 1. 推送脚本 │ │ adb push ddi-client.py /data/local/tmp/ │ │ │ │ # 2. 创建 token 文件 │ │ adb shell "echo 'my-gateway-key' > /data/local/tmp/gateway.key" │ │ adb shell "chmod 600 /data/local/tmp/gateway.key" │ │ │ │ # 3. 创建属性文件 (可选) │ │ adb push device_attrs.json /data/local/tmp/ │ │ adb shell "mkdir -p /etc/hawkbit" │ │ adb shell "cp /data/local/tmp/device_attrs.json /etc/hawkbit/" │ │ │ │ # 4. 运行 │ │ adb shell "python3 /data/local/tmp/ddi-client.py \ │ │ -u https://hawkbit:9090 -t DEFAULT \ │ │ --gateway-token @/data/local/tmp/gateway.key \ │ │ -d /data/local/tmp/hawkbit" │ │ │ │ │ │ Linux 设备部署 (systemd): │ │ ───────────────────────── │ │ │ │ # /opt/hawkbit/ddi-client.py │ │ # /etc/hawkbit/gateway.key │ │ # /etc/hawkbit/device_attrs.json │ │ │ │ # /etc/systemd/system/hawkbit-ddi.service │ │ [Unit] │ │ Description=Hawkbit DDI Client │ │ After=network-online.target │ │ Wants=network-online.target │ │ │ │ [Service] │ │ Type=simple │ │ ExecStart=/usr/bin/python3 /opt/hawkbit/ddi-client.py \ │ │ -u https://hawkbit:9090 -t DEFAULT \ │ │ --gateway-token @/etc/hawkbit/gateway.key \ │ │ -d /var/cache/hawkbit -i 30 │ │ Restart=always │ │ RestartSec=30 │ │ User=root │ │ │ │ [Install] │ │ WantedBy=multi-user.target │ │ │ │ systemctl enable --now hawkbit-ddi │ │ │ │ │ │ 版本管理: │ │ ──────── │ │ │ │ 安装成功后写入版本号: │ │ echo "V2.0.0.0" > /etc/hawkbit/current_version │ │ │ │ 下次轮询时属性中自动包含 swVersion=V2.0.0.0 │ │ 管理员可在 Hawkbit 中按版本筛选设备 │ └───────────────────────────────────────────────────────────────┘ ``` --- ## 附录 A: HTTP 请求/响应速查 | 操作 | 方法 | 路径 | Content-Type | |------|------|------|-------------| | 轮询 | GET | `/{tenant}/controller/v1/{controllerId}` | application/hal+json | | 部署详情 | GET | `/{tenant}/controller/v1/{controllerId}/deploymentBase/{actionId}` | application/hal+json | | 上报反馈 | POST | `/{tenant}/controller/v1/{controllerId}/deploymentBase/{actionId}/feedback` | application/json | | 下载文件 | GET | `/{tenant}/controller/v1/{controllerId}/softwaremodules/{id}/artifacts/{file}` | application/octet-stream | | 上报属性 | PUT | `/{tenant}/controller/v1/{controllerId}/configData` | application/json | | 取消反馈 | POST | `/{tenant}/controller/v1/{controllerId}/cancelAction/{id}/feedback` | application/json | | 确认反馈 | POST | `/{tenant}/controller/v1/{controllerId}/confirmationBase/{id}/feedback` | application/json | ## 附录 B: 命令行参数完整列表 ``` -u, --base-url Hawkbit 服务端地址 (必填) -t, --tenant 租户名称 (必填) --gateway-token Gateway Token 或 @文件路径 --target-token Target Token 或 @文件路径 --controller-id 控制器ID (默认自动检测SN) -d, --download-dir 下载目录 (/tmp/hawkbit) -i, --polling-interval 轮询间隔秒数 (60) --no-verify 跳过 SHA256 校验 --no-resume 禁用断点续传 --no-ssl-verify 跳过 TLS 证书验证 --once 单次轮询后退出 -v, --verbose 调试日志 -h, --help 帮助 ``` ## 附录 C: 设备属性文件格式 ```json # /etc/hawkbit/device_attrs.json { "dept": "production", "region": "east-1", "owner": "team-a", "rack": "A12", "environment": "staging" } ``` ## 附录 D: 状态流转图 (Hawkbit 视角) ``` assignment │ ▼ READY ──→ RETRIEVED ──→ DOWNLOAD ──→ DOWNLOADED ──→ RUNNING ──→ FINISHED │ │ │ (成功) │ │ │ │ │ └──→ ERROR │ │ (失败) │ │ └──────────┬───────────────┘ │ CANCELING ──→ CANCELED │ CANCEL_REJECTED WAITING_FOR_CONFIRMATION ──confirmed──→ RUNNING → ... │ └──denied──→ DENIED ```