diff --git a/apps/web-console/README.md b/apps/web-console/README.md
index 33f297d..2086de0 100755
--- a/apps/web-console/README.md
+++ b/apps/web-console/README.md
@@ -1,5 +1,15 @@
# Web Console
+## 座椅固件开发工作台
+
+“开发指令”支持输入“增加动态木马功能”,也可点击位于位置控制、座椅通风、座椅加热之后的
+“+ 动态木马”快捷按钮。木马模式会自动加入位置控制依赖,基于当前时间控制基线生成
+“时间控制+位置控制+动态木马”固件,并沿用代码生成、IAR 编译、验证、打包和发布流程。
+可同时选择通风、加热等能力。固件使用已有木马标定参数及停止、超时保护。
+
+发布后的产物可下载,并通过该行的“通知车端”按钮发送现有协议的 3 帧发布通知;
+通知仅提示有可用固件,不会自动升级或启动座椅。未选择木马的工作台任务关闭木马编译开关。
+
## 宠物卡片与 NFC 上报
主页“宠物卡片”区域支持创建多张宠物卡片。`pet_name` 与 `action` 可在卡片中编辑,
diff --git a/apps/web-console/firmware-workbench.css b/apps/web-console/firmware-workbench.css
index e437e15..a419cde 100644
--- a/apps/web-console/firmware-workbench.css
+++ b/apps/web-console/firmware-workbench.css
@@ -176,7 +176,7 @@ textarea {
outline: none;
}
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 {
min-width: 0;
padding: 5px 8px;
diff --git a/apps/web-console/firmware-workbench.html b/apps/web-console/firmware-workbench.html
index 04fc4ad..50ad4ec 100644
--- a/apps/web-console/firmware-workbench.html
+++ b/apps/web-console/firmware-workbench.html
@@ -8,9 +8,9 @@
当前基线时间控制
diff --git a/apps/web-console/firmware-workbench.js b/apps/web-console/firmware-workbench.js
index 92575ea..5678572 100644
--- a/apps/web-console/firmware-workbench.js
+++ b/apps/web-console/firmware-workbench.js
@@ -28,7 +28,7 @@
publish: "cloud-upload",
};
const ACTIVE_STATES = new Set(["waiting", "running", "pending_confirmation"]);
- const CAPABILITY_ORDER = ["时间控制", "位置控制", "座椅通风", "座椅加热"];
+ const CAPABILITY_ORDER = ["时间控制", "位置控制", "座椅通风", "座椅加热", "动态木马"];
const FALLBACK_BASELINES = {
time_only: ["时间控制"],
};
@@ -112,8 +112,11 @@
"位置控制": ["位置", "position"],
"座椅通风": ["通风", "vent"],
"座椅加热": ["加热", "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) {
@@ -126,6 +129,7 @@
: CAPABILITY_ORDER.filter((item) => baseline.includes(item) || parseInstructionCapabilities(ui.instruction.value).includes(item));
ui.baselineCapabilities.textContent = baseline.join("+");
ui.targetCapabilities.textContent = (target?.length ? target : baseline).join("+");
+ ui.targetCapabilities.title = ui.targetCapabilities.textContent;
}
function formatDate(value) {
@@ -247,7 +251,7 @@
return;
}
ui.inspector.innerHTML = task.file_changes?.length
- ? task.file_changes.map((change) => `
${change.type === "added" ? "新增" : "修改"}${escapeHtml(change.path)}${escapeHtml(change.summary)}
`).join("")
+ ? task.file_changes.map((change) => `
${change.type === "added" ? "新增" : change.type === "retained" ? "复用" : "修改"}${escapeHtml(change.path)}${escapeHtml(change.summary)}
`).join("")
: `
尚无文件变更
`;
}
diff --git a/apps/web-console/service-worker.js b/apps/web-console/service-worker.js
index 310c2d0..acc4729 100644
--- a/apps/web-console/service-worker.js
+++ b/apps/web-console/service-worker.js
@@ -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 RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
@@ -27,8 +27,8 @@ const APP_SHELL = [
"/firmware-workbench-entry.js?v=3",
"/firmware-workbench.html",
"/firmware-workbench-theme.js?v=1",
- "/firmware-workbench.css?v=7",
- "/firmware-workbench.js?v=4",
+ "/firmware-workbench.css?v=8",
+ "/firmware-workbench.js?v=5",
"/assets/pets/self-extension-cat.png?v=3",
"/assets/pets/self-extension-cat-entrance.png?v=3",
"/manifest.webmanifest",
diff --git a/firmware/t2g/Seat_Controller_demo/App/seat_dynamic_mode.c b/firmware/t2g/Seat_Controller_demo/App/seat_dynamic_mode.c
index 771957f..7b05fcb 100644
--- a/firmware/t2g/Seat_Controller_demo/App/seat_dynamic_mode.c
+++ b/firmware/t2g/Seat_Controller_demo/App/seat_dynamic_mode.c
@@ -5,6 +5,7 @@
#include "motor_control.h"
#include "seat_dynamic_mode_config.h"
+#include "seat_feature_profile.h"
#include "seat_motor_manager.h"
#include "seat_move_config.h"
@@ -206,6 +207,12 @@ bool SeatDynamicMode_Start(uint8_t modeId, uint16_t commandSequence)
SeatDynamic_Definition(modeId);
uint8_t motor;
+#if !SEAT_FEATURE_ROCKING_HORSE
+ if(modeId == SEAT_DYNAMIC_MODE_ROCKING_HORSE)
+ {
+ return false;
+ }
+#endif
if((definition == NULL) || SeatDynamicMode_IsActive() ||
SeatMotorManager_IsAnyActive())
{
diff --git a/firmware/t2g/Seat_Controller_demo/App/seat_move_application.c b/firmware/t2g/Seat_Controller_demo/App/seat_move_application.c
index 628caff..c3d8879 100755
--- a/firmware/t2g/Seat_Controller_demo/App/seat_move_application.c
+++ b/firmware/t2g/Seat_Controller_demo/App/seat_move_application.c
@@ -681,6 +681,13 @@ static bool SeatMove_ParametersValid(const seat_move_command_t *command,
*reject = SEAT_MOVE_REJECT_PARAM_RANGE;
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;
case SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE:
if((command->paramA != 0u) || (command->paramB != 0u))
diff --git a/firmware/t2g/Seat_Controller_demo/Config/seat_feature_profile.h b/firmware/t2g/Seat_Controller_demo/Config/seat_feature_profile.h
index 5cd0fa1..d47ad14 100644
--- a/firmware/t2g/Seat_Controller_demo/Config/seat_feature_profile.h
+++ b/firmware/t2g/Seat_Controller_demo/Config/seat_feature_profile.h
@@ -9,6 +9,7 @@
#define SEAT_FEATURE_POSITION_CONTROL (1)
#define SEAT_FEATURE_VENTILATION (1)
#define SEAT_FEATURE_HEATING (1)
+#define SEAT_FEATURE_ROCKING_HORSE (1)
#if ((SEAT_FEATURE_POSITION_CONTROL != 0) && \
(SEAT_FEATURE_POSITION_CONTROL != 1))
@@ -21,6 +22,13 @@
#error "SEAT_FEATURE_HEATING must be 0 or 1"
#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 \
((SEAT_FEATURE_VENTILATION != 0) || (SEAT_FEATURE_HEATING != 0))
diff --git a/src/cognitive_agent/application/firmware_workbench.py b/src/cognitive_agent/application/firmware_workbench.py
index 6738407..ebebcf5 100644
--- a/src/cognitive_agent/application/firmware_workbench.py
+++ b/src/cognitive_agent/application/firmware_workbench.py
@@ -33,7 +33,7 @@ PIPELINE_STAGES = (
)
TERMINAL_TASK_STATES = frozenset({"success", "failed", "canceled", "blocked"})
ACTIVE_TASK_STATES = frozenset({"waiting", "running", "pending_confirmation"})
-ALL_CAPABILITIES = ("时间控制", "位置控制", "座椅通风", "座椅加热")
+ALL_CAPABILITIES = ("时间控制", "位置控制", "座椅通风", "座椅加热", "动态木马")
BASELINE_CATALOG = {
"time_only": {
"name": "时间控制基线",
@@ -854,6 +854,7 @@ class FirmwareWorkbenchService:
"position_control": "位置控制" in target_capabilities,
"ventilation": "座椅通风" in target_capabilities,
"heating": "座椅加热" in target_capabilities,
+ "rocking_horse": "动态木马" in target_capabilities,
}
requirements = {
"task_id": task_id,
@@ -910,6 +911,19 @@ class FirmwareWorkbenchService:
"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["code_diff"] = code_diff
self._save_locked()
@@ -936,7 +950,7 @@ class FirmwareWorkbenchService:
)
copied_hex = workspace / "Seat_Controller_demo.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", "执行固件能力与工程完整性验证。")
self._check_cancel(cancel_event)
@@ -1059,11 +1073,15 @@ class FirmwareWorkbenchService:
"位置控制": ("位置", "position"),
"座椅通风": ("通风", "vent"),
"座椅加热": ("加热", "heater", "heat"),
+ "动态木马": ("木马", "rocking_horse", "rocking horse", "rocking-horse"),
}
for capability, aliases in patterns.items():
if any(alias in lowered for alias in aliases):
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 callback(level: str, line: str) -> None:
@@ -1115,6 +1133,8 @@ class FirmwareWorkbenchService:
"1" if "座椅通风" in capabilities else "0",
"-Heating",
"1" if "座椅加热" in capabilities else "0",
+ "-RockingHorse",
+ "1" if "动态木马" in capabilities else "0",
]
process = subprocess.Popen(
command,
@@ -1175,6 +1195,7 @@ class FirmwareWorkbenchService:
"position_control": "位置控制" in target_capabilities,
"ventilation": "座椅通风" in target_capabilities,
"heating": "座椅加热" in target_capabilities,
+ "rocking_horse": "动态木马" in target_capabilities,
}),
]
if "位置控制" in target_capabilities:
@@ -1194,6 +1215,35 @@ class FirmwareWorkbenchService:
"座椅加热控制",
"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([
(
"IAR 工程模块完整性",
@@ -1245,6 +1295,7 @@ class FirmwareWorkbenchService:
"位置控制": "position",
"座椅通风": "vent",
"座椅加热": "heat",
+ "动态木马": "rocking-horse",
}[capability]
for capability in capabilities
)
diff --git a/tests/e2e/test_firmware_workbench_api.py b/tests/e2e/test_firmware_workbench_api.py
index fa83669..c07130f 100644
--- a/tests/e2e/test_firmware_workbench_api.py
+++ b/tests/e2e/test_firmware_workbench_api.py
@@ -1,12 +1,17 @@
from __future__ import annotations
+import io
import json
import tempfile
import threading
import time
import urllib.request
+import zipfile
from http.server import ThreadingHTTPServer
from pathlib import Path
+from types import SimpleNamespace
+
+import pytest
from cognitive_agent.application.firmware_workbench import FirmwareWorkbenchService
from cognitive_agent.auth import AuthConfig, AuthRuntime
@@ -75,8 +80,14 @@ class TestFirmwareWorkbenchApi:
with urllib.request.urlopen(request, timeout=5) as response:
return response.status, json.loads(response.read().decode())
- def test_real_http_task_publish_detail_and_download_flow(self) -> None:
- instruction = "增加位置控制、座椅通风和座椅加热功能"
+ @pytest.mark.parametrize("instruction,capabilities", [
+ ("增加位置控制、座椅通风和座椅加热功能", ["时间控制", "位置控制", "座椅通风", "座椅加热"]),
+ ("增加动态木马功能", ["时间控制", "位置控制", "动态木马"]),
+ ("增加动态木马、座椅通风和座椅加热功能", ["时间控制", "位置控制", "座椅通风", "座椅加热", "动态木马"]),
+ ])
+ def test_real_http_task_publish_detail_and_download_flow(
+ self, instruction: str, capabilities: list[str], monkeypatch
+ ) -> None:
status, created = self.post(
"/api/firmware-workbench/tasks", {"instruction": instruction}
)
@@ -135,6 +146,26 @@ class TestFirmwareWorkbenchApi:
assert response.headers["Content-Type"] == "application/zip"
assert "attachment" in response.headers["Content-Disposition"]
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(
f"{self.base_url}/api/firmware-workbench/artifacts/{artifact_id}",
diff --git a/tests/unit/test_firmware_workbench.py b/tests/unit/test_firmware_workbench.py
index e902c9a..3ccc3f7 100644
--- a/tests/unit/test_firmware_workbench.py
+++ b/tests/unit/test_firmware_workbench.py
@@ -7,10 +7,14 @@ import time
import zipfile
from pathlib import Path
+import pytest
+
from cognitive_agent.application.firmware_workbench import (
FINAL_RELEASE_MESSAGE,
+ FirmwareWorkbenchError,
FirmwareWorkbenchService,
)
+from cognitive_agent.infrastructure.can.gateway import CanGatewayError
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 "vent" 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()
@@ -251,3 +257,85 @@ def test_running_pipeline_can_be_canceled_and_retried(tmp_path: Path) -> None:
completed = wait_for_terminal(service, task_id)
assert completed["status"] == "success"
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()
diff --git a/tests/unit/test_firmware_workbench_frontend_contract.py b/tests/unit/test_firmware_workbench_frontend_contract.py
index cccb90f..95bdceb 100644
--- a/tests/unit/test_firmware_workbench_frontend_contract.py
+++ b/tests/unit/test_firmware_workbench_frontend_contract.py
@@ -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.html",
"/firmware-workbench-theme.js?v=1",
- "/firmware-workbench.css?v=7",
- "/firmware-workbench.js?v=4",
+ "/firmware-workbench.css?v=8",
+ "/firmware-workbench.js?v=5",
):
assert f'"{asset}"' in worker
for runtime_file in (
diff --git a/tools/build_seat_firmware.ps1 b/tools/build_seat_firmware.ps1
index 6b647db..156cfd8 100755
--- a/tools/build_seat_firmware.ps1
+++ b/tools/build_seat_firmware.ps1
@@ -11,10 +11,15 @@ param(
[ValidateSet(0, 1)]
[int]$Ventilation = 1,
[ValidateSet(0, 1)]
- [int]$Heating = 1
+ [int]$Heating = 1,
+ [ValidateSet(0, 1)]
+ [int]$RockingHorse = $PositionControl
)
$ErrorActionPreference = 'Stop'
+if ($RockingHorse -eq 1 -and $PositionControl -eq 0) {
+ throw 'Rocking-horse mode requires -PositionControl 1.'
+}
$repositoryCandidate = if ($RepositoryRoot) {
$RepositoryRoot
} else {
@@ -142,23 +147,29 @@ try {
New-Item -ItemType Junction -Path (Join-Path $stagingRootFull 'tviibh8m') -Target $vendorDevice | Out-Null
$featureProfilePath = Join-Path $stagingProjectRoot 'Config\seat_feature_profile.h'
- $featureProfile = @"
-#ifndef SEAT_FEATURE_PROFILE_H
-#define SEAT_FEATURE_PROFILE_H
-
-/* Generated by tools/build_seat_firmware.ps1 for this build only. */
-#define SEAT_FEATURE_POSITION_CONTROL ($PositionControl)
-#define SEAT_FEATURE_VENTILATION ($Ventilation)
-#define SEAT_FEATURE_HEATING ($Heating)
-
-#endif /* SEAT_FEATURE_PROFILE_H */
-"@
+ # Preserve dependency checks and derived switches (including THERMAL).
+ $featureProfile = [System.IO.File]::ReadAllText($featureProfilePath)
+ $featureValues = [ordered]@{
+ SEAT_FEATURE_POSITION_CONTROL = $PositionControl
+ SEAT_FEATURE_VENTILATION = $Ventilation
+ SEAT_FEATURE_HEATING = $Heating
+ SEAT_FEATURE_ROCKING_HORSE = $RockingHorse
+ }
+ foreach ($entry in $featureValues.GetEnumerator()) {
+ $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(
$featureProfilePath,
$featureProfile,
[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'
Invoke-IarBuild -ProjectPath $stagedProject
@@ -170,6 +181,9 @@ try {
Join-Path $repositoryFirmware "IAR\$Configuration\Exe"
}
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')) {
$sourceArtifact = Join-Path $stagedExe $artifact
if (-not (Test-Path -LiteralPath $sourceArtifact -PathType Leaf)) {