From 4d99702acd511ef3b66dd32eff89b46907b8f23b Mon Sep 17 00:00:00 2001 From: Miayui <774713819@qq.com> Date: Tue, 22 Sep 2026 11:15:13 +0800 Subject: [PATCH] feat(firmware): support rocking-horse workbench delivery --- apps/web-console/README.md | 10 +++ apps/web-console/firmware-workbench.css | 2 +- apps/web-console/firmware-workbench.html | 5 +- apps/web-console/firmware-workbench.js | 10 ++- apps/web-console/service-worker.js | 6 +- .../App/seat_dynamic_mode.c | 7 ++ .../App/seat_move_application.c | 7 ++ .../Config/seat_feature_profile.h | 8 ++ .../application/firmware_workbench.py | 57 +++++++++++- tests/e2e/test_firmware_workbench_api.py | 35 +++++++- tests/unit/test_firmware_workbench.py | 88 +++++++++++++++++++ ...st_firmware_workbench_frontend_contract.py | 4 +- tools/build_seat_firmware.ps1 | 40 ++++++--- 13 files changed, 250 insertions(+), 29 deletions(-) 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 @@ 座椅固件开发工作台 · 511 工程 - + - +
@@ -48,6 +48,7 @@ +
当前基线时间控制
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)) {