feat(firmware): support rocking-horse workbench delivery
This commit is contained in:
@@ -1,5 +1,15 @@
|
|||||||
# Web Console
|
# Web Console
|
||||||
|
|
||||||
|
## 座椅固件开发工作台
|
||||||
|
|
||||||
|
“开发指令”支持输入“增加动态木马功能”,也可点击位于位置控制、座椅通风、座椅加热之后的
|
||||||
|
“+ 动态木马”快捷按钮。木马模式会自动加入位置控制依赖,基于当前时间控制基线生成
|
||||||
|
“时间控制+位置控制+动态木马”固件,并沿用代码生成、IAR 编译、验证、打包和发布流程。
|
||||||
|
可同时选择通风、加热等能力。固件使用已有木马标定参数及停止、超时保护。
|
||||||
|
|
||||||
|
发布后的产物可下载,并通过该行的“通知车端”按钮发送现有协议的 3 帧发布通知;
|
||||||
|
通知仅提示有可用固件,不会自动升级或启动座椅。未选择木马的工作台任务关闭木马编译开关。
|
||||||
|
|
||||||
## 宠物卡片与 NFC 上报
|
## 宠物卡片与 NFC 上报
|
||||||
|
|
||||||
主页“宠物卡片”区域支持创建多张宠物卡片。`pet_name` 与 `action` 可在卡片中编辑,
|
主页“宠物卡片”区域支持创建多张宠物卡片。`pet_name` 与 `action` 可在卡片中编辑,
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ textarea {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
textarea:focus { border-color: #79a4f4; box-shadow: 0 0 0 3px rgba(40, 105, 232, .1); background: #fff; }
|
textarea:focus { border-color: #79a4f4; box-shadow: 0 0 0 3px rgba(40, 105, 232, .1); background: #fff; }
|
||||||
.prompt-templates { display: flex; gap: 6px; overflow: hidden; }
|
.prompt-templates { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
.prompt-templates button {
|
.prompt-templates button {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
<title>座椅固件开发工作台 · 511 工程</title>
|
<title>座椅固件开发工作台 · 511 工程</title>
|
||||||
<link rel="icon" href="/icons/app-icon-192.png" />
|
<link rel="icon" href="/icons/app-icon-192.png" />
|
||||||
<script src="/firmware-workbench-theme.js?v=1"></script>
|
<script src="/firmware-workbench-theme.js?v=1"></script>
|
||||||
<link rel="stylesheet" href="/firmware-workbench.css?v=7" />
|
<link rel="stylesheet" href="/firmware-workbench.css?v=8" />
|
||||||
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
||||||
<script src="/firmware-workbench.js?v=4" defer></script>
|
<script src="/firmware-workbench.js?v=5" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
<button type="button" data-prompt="增加支持座椅位置参数控制的能力">+ 位置控制</button>
|
<button type="button" data-prompt="增加支持座椅位置参数控制的能力">+ 位置控制</button>
|
||||||
<button type="button" data-prompt="增加座椅通风功能">+ 座椅通风</button>
|
<button type="button" data-prompt="增加座椅通风功能">+ 座椅通风</button>
|
||||||
<button type="button" data-prompt="增加座椅加热功能">+ 座椅加热</button>
|
<button type="button" data-prompt="增加座椅加热功能">+ 座椅加热</button>
|
||||||
|
<button type="button" data-prompt="增加动态木马功能">+ 动态木马</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="capability-brief" aria-label="固件能力升级范围">
|
<div class="capability-brief" aria-label="固件能力升级范围">
|
||||||
<div><span>当前基线</span><strong id="baselineCapabilities">时间控制</strong></div>
|
<div><span>当前基线</span><strong id="baselineCapabilities">时间控制</strong></div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
publish: "cloud-upload",
|
publish: "cloud-upload",
|
||||||
};
|
};
|
||||||
const ACTIVE_STATES = new Set(["waiting", "running", "pending_confirmation"]);
|
const ACTIVE_STATES = new Set(["waiting", "running", "pending_confirmation"]);
|
||||||
const CAPABILITY_ORDER = ["时间控制", "位置控制", "座椅通风", "座椅加热"];
|
const CAPABILITY_ORDER = ["时间控制", "位置控制", "座椅通风", "座椅加热", "动态木马"];
|
||||||
const FALLBACK_BASELINES = {
|
const FALLBACK_BASELINES = {
|
||||||
time_only: ["时间控制"],
|
time_only: ["时间控制"],
|
||||||
};
|
};
|
||||||
@@ -112,8 +112,11 @@
|
|||||||
"位置控制": ["位置", "position"],
|
"位置控制": ["位置", "position"],
|
||||||
"座椅通风": ["通风", "vent"],
|
"座椅通风": ["通风", "vent"],
|
||||||
"座椅加热": ["加热", "heater", "heat"],
|
"座椅加热": ["加热", "heater", "heat"],
|
||||||
|
"动态木马": ["木马", "rocking_horse", "rocking horse", "rocking-horse"],
|
||||||
};
|
};
|
||||||
return CAPABILITY_ORDER.filter((capability) => aliases[capability].some((alias) => lowered.includes(alias)));
|
const requested = CAPABILITY_ORDER.filter((capability) => aliases[capability].some((alias) => lowered.includes(alias)));
|
||||||
|
if (requested.includes("动态木马") && !requested.includes("位置控制")) requested.push("位置控制");
|
||||||
|
return CAPABILITY_ORDER.filter((capability) => requested.includes(capability));
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCapabilityPreview(task = null) {
|
function updateCapabilityPreview(task = null) {
|
||||||
@@ -126,6 +129,7 @@
|
|||||||
: CAPABILITY_ORDER.filter((item) => baseline.includes(item) || parseInstructionCapabilities(ui.instruction.value).includes(item));
|
: CAPABILITY_ORDER.filter((item) => baseline.includes(item) || parseInstructionCapabilities(ui.instruction.value).includes(item));
|
||||||
ui.baselineCapabilities.textContent = baseline.join("+");
|
ui.baselineCapabilities.textContent = baseline.join("+");
|
||||||
ui.targetCapabilities.textContent = (target?.length ? target : baseline).join("+");
|
ui.targetCapabilities.textContent = (target?.length ? target : baseline).join("+");
|
||||||
|
ui.targetCapabilities.title = ui.targetCapabilities.textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
@@ -247,7 +251,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ui.inspector.innerHTML = task.file_changes?.length
|
ui.inspector.innerHTML = task.file_changes?.length
|
||||||
? task.file_changes.map((change) => `<div class="change-item"><span class="change-type">${change.type === "added" ? "新增" : "修改"}</span><div class="change-copy"><strong>${escapeHtml(change.path)}</strong><span>${escapeHtml(change.summary)}</span></div></div>`).join("")
|
? task.file_changes.map((change) => `<div class="change-item"><span class="change-type">${change.type === "added" ? "新增" : change.type === "retained" ? "复用" : "修改"}</span><div class="change-copy"><strong>${escapeHtml(change.path)}</strong><span>${escapeHtml(change.summary)}</span></div></div>`).join("")
|
||||||
: `<div class="empty-inline">尚无文件变更</div>`;
|
: `<div class="empty-inline">尚无文件变更</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_VERSION = "cognitive-agent-pad-v334-video-media";
|
const CACHE_VERSION = "cognitive-agent-pad-v335-firmware-rocking-horse";
|
||||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||||
const APP_SHELL = [
|
const APP_SHELL = [
|
||||||
@@ -27,8 +27,8 @@ const APP_SHELL = [
|
|||||||
"/firmware-workbench-entry.js?v=3",
|
"/firmware-workbench-entry.js?v=3",
|
||||||
"/firmware-workbench.html",
|
"/firmware-workbench.html",
|
||||||
"/firmware-workbench-theme.js?v=1",
|
"/firmware-workbench-theme.js?v=1",
|
||||||
"/firmware-workbench.css?v=7",
|
"/firmware-workbench.css?v=8",
|
||||||
"/firmware-workbench.js?v=4",
|
"/firmware-workbench.js?v=5",
|
||||||
"/assets/pets/self-extension-cat.png?v=3",
|
"/assets/pets/self-extension-cat.png?v=3",
|
||||||
"/assets/pets/self-extension-cat-entrance.png?v=3",
|
"/assets/pets/self-extension-cat-entrance.png?v=3",
|
||||||
"/manifest.webmanifest",
|
"/manifest.webmanifest",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "motor_control.h"
|
#include "motor_control.h"
|
||||||
#include "seat_dynamic_mode_config.h"
|
#include "seat_dynamic_mode_config.h"
|
||||||
|
#include "seat_feature_profile.h"
|
||||||
#include "seat_motor_manager.h"
|
#include "seat_motor_manager.h"
|
||||||
#include "seat_move_config.h"
|
#include "seat_move_config.h"
|
||||||
|
|
||||||
@@ -206,6 +207,12 @@ bool SeatDynamicMode_Start(uint8_t modeId, uint16_t commandSequence)
|
|||||||
SeatDynamic_Definition(modeId);
|
SeatDynamic_Definition(modeId);
|
||||||
uint8_t motor;
|
uint8_t motor;
|
||||||
|
|
||||||
|
#if !SEAT_FEATURE_ROCKING_HORSE
|
||||||
|
if(modeId == SEAT_DYNAMIC_MODE_ROCKING_HORSE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if((definition == NULL) || SeatDynamicMode_IsActive() ||
|
if((definition == NULL) || SeatDynamicMode_IsActive() ||
|
||||||
SeatMotorManager_IsAnyActive())
|
SeatMotorManager_IsAnyActive())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -681,6 +681,13 @@ static bool SeatMove_ParametersValid(const seat_move_command_t *command,
|
|||||||
*reject = SEAT_MOVE_REJECT_PARAM_RANGE;
|
*reject = SEAT_MOVE_REJECT_PARAM_RANGE;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#if !SEAT_FEATURE_ROCKING_HORSE
|
||||||
|
if(command->paramA == SEAT_DYNAMIC_MODE_ROCKING_HORSE)
|
||||||
|
{
|
||||||
|
*reject = SEAT_MOVE_REJECT_BAD_OPCODE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
case SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE:
|
case SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE:
|
||||||
if((command->paramA != 0u) || (command->paramB != 0u))
|
if((command->paramA != 0u) || (command->paramB != 0u))
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#define SEAT_FEATURE_POSITION_CONTROL (1)
|
#define SEAT_FEATURE_POSITION_CONTROL (1)
|
||||||
#define SEAT_FEATURE_VENTILATION (1)
|
#define SEAT_FEATURE_VENTILATION (1)
|
||||||
#define SEAT_FEATURE_HEATING (1)
|
#define SEAT_FEATURE_HEATING (1)
|
||||||
|
#define SEAT_FEATURE_ROCKING_HORSE (1)
|
||||||
|
|
||||||
#if ((SEAT_FEATURE_POSITION_CONTROL != 0) && \
|
#if ((SEAT_FEATURE_POSITION_CONTROL != 0) && \
|
||||||
(SEAT_FEATURE_POSITION_CONTROL != 1))
|
(SEAT_FEATURE_POSITION_CONTROL != 1))
|
||||||
@@ -21,6 +22,13 @@
|
|||||||
#error "SEAT_FEATURE_HEATING must be 0 or 1"
|
#error "SEAT_FEATURE_HEATING must be 0 or 1"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if ((SEAT_FEATURE_ROCKING_HORSE != 0) && (SEAT_FEATURE_ROCKING_HORSE != 1))
|
||||||
|
#error "SEAT_FEATURE_ROCKING_HORSE must be 0 or 1"
|
||||||
|
#endif
|
||||||
|
#if SEAT_FEATURE_ROCKING_HORSE && !SEAT_FEATURE_POSITION_CONTROL
|
||||||
|
#error "Rocking-horse mode requires position control"
|
||||||
|
#endif
|
||||||
|
|
||||||
#define SEAT_FEATURE_THERMAL \
|
#define SEAT_FEATURE_THERMAL \
|
||||||
((SEAT_FEATURE_VENTILATION != 0) || (SEAT_FEATURE_HEATING != 0))
|
((SEAT_FEATURE_VENTILATION != 0) || (SEAT_FEATURE_HEATING != 0))
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ PIPELINE_STAGES = (
|
|||||||
)
|
)
|
||||||
TERMINAL_TASK_STATES = frozenset({"success", "failed", "canceled", "blocked"})
|
TERMINAL_TASK_STATES = frozenset({"success", "failed", "canceled", "blocked"})
|
||||||
ACTIVE_TASK_STATES = frozenset({"waiting", "running", "pending_confirmation"})
|
ACTIVE_TASK_STATES = frozenset({"waiting", "running", "pending_confirmation"})
|
||||||
ALL_CAPABILITIES = ("时间控制", "位置控制", "座椅通风", "座椅加热")
|
ALL_CAPABILITIES = ("时间控制", "位置控制", "座椅通风", "座椅加热", "动态木马")
|
||||||
BASELINE_CATALOG = {
|
BASELINE_CATALOG = {
|
||||||
"time_only": {
|
"time_only": {
|
||||||
"name": "时间控制基线",
|
"name": "时间控制基线",
|
||||||
@@ -854,6 +854,7 @@ class FirmwareWorkbenchService:
|
|||||||
"position_control": "位置控制" in target_capabilities,
|
"position_control": "位置控制" in target_capabilities,
|
||||||
"ventilation": "座椅通风" in target_capabilities,
|
"ventilation": "座椅通风" in target_capabilities,
|
||||||
"heating": "座椅加热" in target_capabilities,
|
"heating": "座椅加热" in target_capabilities,
|
||||||
|
"rocking_horse": "动态木马" in target_capabilities,
|
||||||
}
|
}
|
||||||
requirements = {
|
requirements = {
|
||||||
"task_id": task_id,
|
"task_id": task_id,
|
||||||
@@ -910,6 +911,19 @@ class FirmwareWorkbenchService:
|
|||||||
"summary": "启用靠背加热分级控制与超时保护",
|
"summary": "启用靠背加热分级控制与超时保护",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if "动态木马" in target_capabilities:
|
||||||
|
file_changes.extend([
|
||||||
|
{
|
||||||
|
"path": "App/seat_dynamic_mode.c",
|
||||||
|
"type": "modified",
|
||||||
|
"summary": "启用动态木马四轴循环运动、停止与超时保护",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Config/seat_dynamic_mode_config.h",
|
||||||
|
"type": "retained",
|
||||||
|
"summary": "复用已标定的木马中心姿态、往复端点与电机占空比",
|
||||||
|
},
|
||||||
|
])
|
||||||
task["file_changes"] = file_changes
|
task["file_changes"] = file_changes
|
||||||
task["code_diff"] = code_diff
|
task["code_diff"] = code_diff
|
||||||
self._save_locked()
|
self._save_locked()
|
||||||
@@ -936,7 +950,7 @@ class FirmwareWorkbenchService:
|
|||||||
)
|
)
|
||||||
copied_hex = workspace / "Seat_Controller_demo.hex"
|
copied_hex = workspace / "Seat_Controller_demo.hex"
|
||||||
shutil.copy2(image_path, copied_hex)
|
shutil.copy2(image_path, copied_hex)
|
||||||
self._finish_stage(task_id, "build", started, "IAR 编译成功,0 errors / 0 warnings。")
|
self._finish_stage(task_id, "build", started, "IAR 编译成功,HEX 镜像已生成。")
|
||||||
|
|
||||||
started = self._begin_stage(task_id, "test", "执行固件能力与工程完整性验证。")
|
started = self._begin_stage(task_id, "test", "执行固件能力与工程完整性验证。")
|
||||||
self._check_cancel(cancel_event)
|
self._check_cancel(cancel_event)
|
||||||
@@ -1059,11 +1073,15 @@ class FirmwareWorkbenchService:
|
|||||||
"位置控制": ("位置", "position"),
|
"位置控制": ("位置", "position"),
|
||||||
"座椅通风": ("通风", "vent"),
|
"座椅通风": ("通风", "vent"),
|
||||||
"座椅加热": ("加热", "heater", "heat"),
|
"座椅加热": ("加热", "heater", "heat"),
|
||||||
|
"动态木马": ("木马", "rocking_horse", "rocking horse", "rocking-horse"),
|
||||||
}
|
}
|
||||||
for capability, aliases in patterns.items():
|
for capability, aliases in patterns.items():
|
||||||
if any(alias in lowered for alias in aliases):
|
if any(alias in lowered for alias in aliases):
|
||||||
requested.append(capability)
|
requested.append(capability)
|
||||||
return requested
|
# The ECU executes rocking-horse poses with calibrated Hall position targets.
|
||||||
|
if "动态木马" in requested and "位置控制" not in requested:
|
||||||
|
requested.append("位置控制")
|
||||||
|
return [capability for capability in ALL_CAPABILITIES if capability in requested]
|
||||||
|
|
||||||
def _build_log_callback(self, task_id: str) -> Callable[[str, str], None]:
|
def _build_log_callback(self, task_id: str) -> Callable[[str, str], None]:
|
||||||
def callback(level: str, line: str) -> None:
|
def callback(level: str, line: str) -> None:
|
||||||
@@ -1115,6 +1133,8 @@ class FirmwareWorkbenchService:
|
|||||||
"1" if "座椅通风" in capabilities else "0",
|
"1" if "座椅通风" in capabilities else "0",
|
||||||
"-Heating",
|
"-Heating",
|
||||||
"1" if "座椅加热" in capabilities else "0",
|
"1" if "座椅加热" in capabilities else "0",
|
||||||
|
"-RockingHorse",
|
||||||
|
"1" if "动态木马" in capabilities else "0",
|
||||||
]
|
]
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
command,
|
command,
|
||||||
@@ -1175,6 +1195,7 @@ class FirmwareWorkbenchService:
|
|||||||
"position_control": "位置控制" in target_capabilities,
|
"position_control": "位置控制" in target_capabilities,
|
||||||
"ventilation": "座椅通风" in target_capabilities,
|
"ventilation": "座椅通风" in target_capabilities,
|
||||||
"heating": "座椅加热" in target_capabilities,
|
"heating": "座椅加热" in target_capabilities,
|
||||||
|
"rocking_horse": "动态木马" in target_capabilities,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
if "位置控制" in target_capabilities:
|
if "位置控制" in target_capabilities:
|
||||||
@@ -1194,6 +1215,35 @@ class FirmwareWorkbenchService:
|
|||||||
"座椅加热控制",
|
"座椅加热控制",
|
||||||
"SeatBackrestHeater_SetLevel" in thermal and "MAX_ACTIVE_TIME" in heater,
|
"SeatBackrestHeater_SetLevel" in thermal and "MAX_ACTIVE_TIME" in heater,
|
||||||
))
|
))
|
||||||
|
if "动态木马" in target_capabilities:
|
||||||
|
dynamic = (self.target_root / "App" / "seat_dynamic_mode.c").read_text(
|
||||||
|
encoding="utf-8", errors="replace"
|
||||||
|
)
|
||||||
|
dynamic_config = (
|
||||||
|
self.target_root / "Config" / "seat_dynamic_mode_config.h"
|
||||||
|
).read_text(encoding="utf-8", errors="replace")
|
||||||
|
checks.extend([
|
||||||
|
("动态木马位置控制依赖", feature_profile["position_control"]),
|
||||||
|
(
|
||||||
|
"动态木马控制与停止保护",
|
||||||
|
all(token in dynamic for token in (
|
||||||
|
"SEAT_DYNAMIC_MODE_ROCKING_HORSE",
|
||||||
|
"SEAT_FEATURE_ROCKING_HORSE",
|
||||||
|
"SeatDynamicMode_Start",
|
||||||
|
"SeatDynamicMode_Stop",
|
||||||
|
"SeatDynamic_Fail",
|
||||||
|
))
|
||||||
|
and "SEAT_MOVE_OPCODE_START_DYNAMIC_MODE" in application
|
||||||
|
and "SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE" in application,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"动态木马标定与工程配置",
|
||||||
|
"SEAT_DYNAMIC_HORSE_PHASE_TIMEOUT_MS" in dynamic_config
|
||||||
|
and "SEAT_DYNAMIC_HORSE_FORWARD_M0_HALL" in dynamic_config
|
||||||
|
and "SEAT_DYNAMIC_HORSE_BACKWARD_M0_HALL" in dynamic_config
|
||||||
|
and "seat_dynamic_mode.c" in project,
|
||||||
|
),
|
||||||
|
])
|
||||||
checks.extend([
|
checks.extend([
|
||||||
(
|
(
|
||||||
"IAR 工程模块完整性",
|
"IAR 工程模块完整性",
|
||||||
@@ -1245,6 +1295,7 @@ class FirmwareWorkbenchService:
|
|||||||
"位置控制": "position",
|
"位置控制": "position",
|
||||||
"座椅通风": "vent",
|
"座椅通风": "vent",
|
||||||
"座椅加热": "heat",
|
"座椅加热": "heat",
|
||||||
|
"动态木马": "rocking-horse",
|
||||||
}[capability]
|
}[capability]
|
||||||
for capability in capabilities
|
for capability in capabilities
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
from http.server import ThreadingHTTPServer
|
from http.server import ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from cognitive_agent.application.firmware_workbench import FirmwareWorkbenchService
|
from cognitive_agent.application.firmware_workbench import FirmwareWorkbenchService
|
||||||
from cognitive_agent.auth import AuthConfig, AuthRuntime
|
from cognitive_agent.auth import AuthConfig, AuthRuntime
|
||||||
@@ -75,8 +80,14 @@ class TestFirmwareWorkbenchApi:
|
|||||||
with urllib.request.urlopen(request, timeout=5) as response:
|
with urllib.request.urlopen(request, timeout=5) as response:
|
||||||
return response.status, json.loads(response.read().decode())
|
return response.status, json.loads(response.read().decode())
|
||||||
|
|
||||||
def test_real_http_task_publish_detail_and_download_flow(self) -> None:
|
@pytest.mark.parametrize("instruction,capabilities", [
|
||||||
instruction = "增加位置控制、座椅通风和座椅加热功能"
|
("增加位置控制、座椅通风和座椅加热功能", ["时间控制", "位置控制", "座椅通风", "座椅加热"]),
|
||||||
|
("增加动态木马功能", ["时间控制", "位置控制", "动态木马"]),
|
||||||
|
("增加动态木马、座椅通风和座椅加热功能", ["时间控制", "位置控制", "座椅通风", "座椅加热", "动态木马"]),
|
||||||
|
])
|
||||||
|
def test_real_http_task_publish_detail_and_download_flow(
|
||||||
|
self, instruction: str, capabilities: list[str], monkeypatch
|
||||||
|
) -> None:
|
||||||
status, created = self.post(
|
status, created = self.post(
|
||||||
"/api/firmware-workbench/tasks", {"instruction": instruction}
|
"/api/firmware-workbench/tasks", {"instruction": instruction}
|
||||||
)
|
)
|
||||||
@@ -135,6 +146,26 @@ class TestFirmwareWorkbenchApi:
|
|||||||
assert response.headers["Content-Type"] == "application/zip"
|
assert response.headers["Content-Type"] == "application/zip"
|
||||||
assert "attachment" in response.headers["Content-Disposition"]
|
assert "attachment" in response.headers["Content-Disposition"]
|
||||||
assert len(package) == artifact["size_bytes"]
|
assert len(package) == artifact["size_bytes"]
|
||||||
|
assert artifact["capabilities"] == capabilities
|
||||||
|
with zipfile.ZipFile(io.BytesIO(package)) as archive:
|
||||||
|
assert json.loads(archive.read("manifest.json"))["capabilities"] == capabilities
|
||||||
|
|
||||||
|
frames = []
|
||||||
|
|
||||||
|
def publish(frame):
|
||||||
|
frames.append(frame)
|
||||||
|
return {"status": "published"}
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
SeatDemoHandler, "service", SimpleNamespace(publish_firmware_notification=publish)
|
||||||
|
)
|
||||||
|
notify_status, notified = self.post(
|
||||||
|
f"/api/firmware-workbench/artifacts/{artifact_id}/notify", {}
|
||||||
|
)
|
||||||
|
assert notify_status == 200
|
||||||
|
assert notified["published_count"] == 3
|
||||||
|
assert [frame["sequence"] for frame in frames] == [1, 2, 3]
|
||||||
|
assert all(frame["firmware"]["release_id"] == release_id for frame in frames)
|
||||||
|
|
||||||
delete_request = urllib.request.Request(
|
delete_request = urllib.request.Request(
|
||||||
f"{self.base_url}/api/firmware-workbench/artifacts/{artifact_id}",
|
f"{self.base_url}/api/firmware-workbench/artifacts/{artifact_id}",
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ import time
|
|||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from cognitive_agent.application.firmware_workbench import (
|
from cognitive_agent.application.firmware_workbench import (
|
||||||
FINAL_RELEASE_MESSAGE,
|
FINAL_RELEASE_MESSAGE,
|
||||||
|
FirmwareWorkbenchError,
|
||||||
FirmwareWorkbenchService,
|
FirmwareWorkbenchService,
|
||||||
)
|
)
|
||||||
|
from cognitive_agent.infrastructure.can.gateway import CanGatewayError
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
@@ -213,6 +217,8 @@ def test_position_instruction_merges_with_time_only_baseline(tmp_path: Path) ->
|
|||||||
assert artifact["capability_diff"]["added"] == ["位置控制"]
|
assert artifact["capability_diff"]["added"] == ["位置控制"]
|
||||||
assert "vent" not in artifact["firmware_filename"]
|
assert "vent" not in artifact["firmware_filename"]
|
||||||
assert "heat" not in artifact["firmware_filename"]
|
assert "heat" not in artifact["firmware_filename"]
|
||||||
|
profile_path = tmp_path / "workbench" / "tasks" / task["id"] / "feature_profile.json"
|
||||||
|
assert json.loads(profile_path.read_text("utf-8"))["rocking_horse"] is False
|
||||||
service.close()
|
service.close()
|
||||||
|
|
||||||
|
|
||||||
@@ -251,3 +257,85 @@ def test_running_pipeline_can_be_canceled_and_retried(tmp_path: Path) -> None:
|
|||||||
completed = wait_for_terminal(service, task_id)
|
completed = wait_for_terminal(service, task_id)
|
||||||
assert completed["status"] == "success"
|
assert completed["status"] == "success"
|
||||||
service.close()
|
service.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("instruction", [
|
||||||
|
"增加动态木马功能",
|
||||||
|
"增加木马功能",
|
||||||
|
"Add rocking_horse",
|
||||||
|
"Add Rocking Horse",
|
||||||
|
"Add rocking-horse",
|
||||||
|
])
|
||||||
|
def test_rocking_horse_package_and_notification(tmp_path: Path, instruction: str) -> None:
|
||||||
|
image = make_image(tmp_path)
|
||||||
|
profile = {
|
||||||
|
"position_control": True, "ventilation": False,
|
||||||
|
"heating": False, "rocking_horse": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_runner(task_id: str, log: object, cancel: threading.Event) -> Path:
|
||||||
|
workspace = tmp_path / "workbench" / "tasks" / task_id
|
||||||
|
assert json.loads((workspace / "feature_profile.json").read_text("utf-8")) == profile
|
||||||
|
return image
|
||||||
|
|
||||||
|
service = FirmwareWorkbenchService(
|
||||||
|
ROOT, artifact_root=tmp_path / "workbench", ota_root=tmp_path / "ota",
|
||||||
|
build_runner=build_runner,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
started = service.start_task(instruction)
|
||||||
|
task = wait_for_terminal(service, started["task"]["id"])
|
||||||
|
assert task["status"] == "success", task.get("failure_reason")
|
||||||
|
assert [stage["status"] for stage in task["stages"]] == ["success"] * 8
|
||||||
|
assert task["target_capabilities"] == ["时间控制", "位置控制", "动态木马"]
|
||||||
|
assert any(item["name"] == "动态木马控制与停止保护" for item in task["test_results"])
|
||||||
|
artifact = service.artifact(task["artifact_id"])
|
||||||
|
assert artifact["capability_diff"]["added"] == ["位置控制", "动态木马"]
|
||||||
|
assert "rocking-horse" in artifact["firmware_filename"]
|
||||||
|
package, _ = service.resolve_release_download(task["release_id"])
|
||||||
|
with zipfile.ZipFile(package) as archive:
|
||||||
|
assert json.loads(archive.read("manifest.json"))["capabilities"] == task["target_capabilities"]
|
||||||
|
assert json.loads(archive.read("feature_profile.json")) == profile
|
||||||
|
assert json.loads(archive.read("requirements.json"))["feature_profile"] == profile
|
||||||
|
frames = []
|
||||||
|
|
||||||
|
def publish(frame):
|
||||||
|
frames.append(frame)
|
||||||
|
return {"status": "published"}
|
||||||
|
|
||||||
|
result = service.notify_artifact(artifact["id"], publish)
|
||||||
|
assert result["published_count"] == 3
|
||||||
|
assert [frame["sequence"] for frame in frames] == [1, 2, 3]
|
||||||
|
assert len({frame["message_id"] for frame in frames}) == 1
|
||||||
|
assert all(frame["firmware"]["artifact_name"] == artifact["firmware_filename"] for frame in frames)
|
||||||
|
assert all(frame["firmware"]["sha256"] == artifact["sha256"] for frame in frames)
|
||||||
|
assert all(frame["auto_upgrade"] is False for frame in frames)
|
||||||
|
|
||||||
|
def unavailable(frame):
|
||||||
|
raise CanGatewayError("broker unavailable")
|
||||||
|
|
||||||
|
with pytest.raises(FirmwareWorkbenchError, match="0/3") as error:
|
||||||
|
service.notify_artifact(artifact["id"], unavailable)
|
||||||
|
assert error.value.code == "notification_publish_failed"
|
||||||
|
assert service.artifact(artifact["id"])["status"] == "published"
|
||||||
|
finally:
|
||||||
|
service.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rocking_horse_build_failure_does_not_publish(tmp_path: Path) -> None:
|
||||||
|
def fail_build(task_id: str, log: object, cancel: threading.Event) -> Path:
|
||||||
|
raise RuntimeError("IAR build failed")
|
||||||
|
|
||||||
|
service = FirmwareWorkbenchService(
|
||||||
|
ROOT, artifact_root=tmp_path / "workbench", ota_root=tmp_path / "ota",
|
||||||
|
build_runner=fail_build,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
started = service.start_task("增加动态木马功能")
|
||||||
|
task = wait_for_terminal(service, started["task"]["id"])
|
||||||
|
assert task["status"] == "failed"
|
||||||
|
assert task["failure_reason"] == "IAR build failed"
|
||||||
|
assert service.snapshot()["releases"] == []
|
||||||
|
assert all(item.get("is_baseline") for item in service.snapshot()["artifacts"])
|
||||||
|
finally:
|
||||||
|
service.close()
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ def test_workbench_exposes_artifact_actions_and_always_starts_a_new_task() -> No
|
|||||||
"/firmware-workbench-entry.js?v=3",
|
"/firmware-workbench-entry.js?v=3",
|
||||||
"/firmware-workbench.html",
|
"/firmware-workbench.html",
|
||||||
"/firmware-workbench-theme.js?v=1",
|
"/firmware-workbench-theme.js?v=1",
|
||||||
"/firmware-workbench.css?v=7",
|
"/firmware-workbench.css?v=8",
|
||||||
"/firmware-workbench.js?v=4",
|
"/firmware-workbench.js?v=5",
|
||||||
):
|
):
|
||||||
assert f'"{asset}"' in worker
|
assert f'"{asset}"' in worker
|
||||||
for runtime_file in (
|
for runtime_file in (
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ param(
|
|||||||
[ValidateSet(0, 1)]
|
[ValidateSet(0, 1)]
|
||||||
[int]$Ventilation = 1,
|
[int]$Ventilation = 1,
|
||||||
[ValidateSet(0, 1)]
|
[ValidateSet(0, 1)]
|
||||||
[int]$Heating = 1
|
[int]$Heating = 1,
|
||||||
|
[ValidateSet(0, 1)]
|
||||||
|
[int]$RockingHorse = $PositionControl
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
if ($RockingHorse -eq 1 -and $PositionControl -eq 0) {
|
||||||
|
throw 'Rocking-horse mode requires -PositionControl 1.'
|
||||||
|
}
|
||||||
$repositoryCandidate = if ($RepositoryRoot) {
|
$repositoryCandidate = if ($RepositoryRoot) {
|
||||||
$RepositoryRoot
|
$RepositoryRoot
|
||||||
} else {
|
} else {
|
||||||
@@ -142,23 +147,29 @@ try {
|
|||||||
New-Item -ItemType Junction -Path (Join-Path $stagingRootFull 'tviibh8m') -Target $vendorDevice | Out-Null
|
New-Item -ItemType Junction -Path (Join-Path $stagingRootFull 'tviibh8m') -Target $vendorDevice | Out-Null
|
||||||
|
|
||||||
$featureProfilePath = Join-Path $stagingProjectRoot 'Config\seat_feature_profile.h'
|
$featureProfilePath = Join-Path $stagingProjectRoot 'Config\seat_feature_profile.h'
|
||||||
$featureProfile = @"
|
# Preserve dependency checks and derived switches (including THERMAL).
|
||||||
#ifndef SEAT_FEATURE_PROFILE_H
|
$featureProfile = [System.IO.File]::ReadAllText($featureProfilePath)
|
||||||
#define SEAT_FEATURE_PROFILE_H
|
$featureValues = [ordered]@{
|
||||||
|
SEAT_FEATURE_POSITION_CONTROL = $PositionControl
|
||||||
/* Generated by tools/build_seat_firmware.ps1 for this build only. */
|
SEAT_FEATURE_VENTILATION = $Ventilation
|
||||||
#define SEAT_FEATURE_POSITION_CONTROL ($PositionControl)
|
SEAT_FEATURE_HEATING = $Heating
|
||||||
#define SEAT_FEATURE_VENTILATION ($Ventilation)
|
SEAT_FEATURE_ROCKING_HORSE = $RockingHorse
|
||||||
#define SEAT_FEATURE_HEATING ($Heating)
|
}
|
||||||
|
foreach ($entry in $featureValues.GetEnumerator()) {
|
||||||
#endif /* SEAT_FEATURE_PROFILE_H */
|
$pattern = '(?m)^#define\s+' + $entry.Key + '\s+\([01]\)'
|
||||||
"@
|
if (-not [regex]::IsMatch($featureProfile, $pattern)) {
|
||||||
|
throw "Firmware feature switch is missing: $($entry.Key)"
|
||||||
|
}
|
||||||
|
$featureProfile = [regex]::Replace(
|
||||||
|
$featureProfile, $pattern, "#define $($entry.Key) ($($entry.Value))"
|
||||||
|
)
|
||||||
|
}
|
||||||
[System.IO.File]::WriteAllText(
|
[System.IO.File]::WriteAllText(
|
||||||
$featureProfilePath,
|
$featureProfilePath,
|
||||||
$featureProfile,
|
$featureProfile,
|
||||||
[System.Text.UTF8Encoding]::new($false)
|
[System.Text.UTF8Encoding]::new($false)
|
||||||
)
|
)
|
||||||
Write-Host "Feature profile: position=$PositionControl ventilation=$Ventilation heating=$Heating"
|
Write-Host "Feature profile: position=$PositionControl ventilation=$Ventilation heating=$Heating rocking_horse=$RockingHorse"
|
||||||
|
|
||||||
$stagedProject = Join-Path $stagingProjectRoot 'IAR\Seat_Controller_demo.ewp'
|
$stagedProject = Join-Path $stagingProjectRoot 'IAR\Seat_Controller_demo.ewp'
|
||||||
Invoke-IarBuild -ProjectPath $stagedProject
|
Invoke-IarBuild -ProjectPath $stagedProject
|
||||||
@@ -170,6 +181,9 @@ try {
|
|||||||
Join-Path $repositoryFirmware "IAR\$Configuration\Exe"
|
Join-Path $repositoryFirmware "IAR\$Configuration\Exe"
|
||||||
}
|
}
|
||||||
New-Item -ItemType Directory -Path $artifactOutput -Force | Out-Null
|
New-Item -ItemType Directory -Path $artifactOutput -Force | Out-Null
|
||||||
|
Copy-BuildArtifact -Source $featureProfilePath -Destination (
|
||||||
|
Join-Path $artifactOutput 'seat_feature_profile.h'
|
||||||
|
)
|
||||||
foreach ($artifact in @('Seat_Controller_demo.hex', 'Seat_Controller_demo.out')) {
|
foreach ($artifact in @('Seat_Controller_demo.hex', 'Seat_Controller_demo.out')) {
|
||||||
$sourceArtifact = Join-Path $stagedExe $artifact
|
$sourceArtifact = Join-Path $stagedExe $artifact
|
||||||
if (-not (Test-Path -LiteralPath $sourceArtifact -PathType Leaf)) {
|
if (-not (Test-Path -LiteralPath $sourceArtifact -PathType Leaf)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user