#!/usr/bin/env python3 """Create the OTA-interactive application source without modifying the reference.""" from __future__ import annotations import argparse import shutil from pathlib import Path def replace_once(text: str, old: str, new: str, label: str) -> str: count = text.count(old) if ( count == 0 and "MOTOR_LEGREST_DEFAULT_DUTY_PERCENT" in text and label in { "calibration explicit legrest duty adapter", "calibration legrest duty setter adapter", } ): return text if count != 1: raise RuntimeError(f"{label}: expected one source anchor, found {count}") return text.replace(old, new, 1) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--source-root", type=Path, required=True) parser.add_argument("--project-root", type=Path, required=True) parser.add_argument("--output-root", type=Path, required=True) args = parser.parse_args() source_root = args.source_root.resolve() project_root = args.project_root.resolve() output_root = args.output_root.resolve() output_src = output_root / "src" if output_src.is_symlink(): output_src.unlink() elif output_src.exists(): shutil.rmtree(output_src) output_src.mkdir(parents=True, exist_ok=True) original_main = source_root / "src" / "main_cm0plus.c" if not original_main.is_file(): raise FileNotFoundError(original_main) text = original_main.read_text(encoding="utf-8") text = replace_once( text, '#include "motor_control.h"', '''#include "motor_control.h" #include "ota_can.h" #include "ota_protocol.h" #include "ota_enter_bootloader.h" #include "ota_seat_state.h" #include "seat_demo_compat.h" #ifndef MOTOR_OTA_INTERACTIVE_TEST #define MOTOR_OTA_INTERACTIVE_TEST (0u) #endif #ifndef MOTOR_OTA_SELF_COMPOSE #define MOTOR_OTA_SELF_COMPOSE (0u) #endif #ifndef MOTOR_OTA_CALIBRATION #define MOTOR_OTA_CALIBRATION (0u) #endif #if (MOTOR_OTA_CALIBRATION != 0u) static void OtaCalibration_ServiceHook(void); #elif (MOTOR_OTA_SELF_COMPOSE != 0u) static void OtaSelfCompose_ServiceHook(void); #endif''', "OTA include adapter", ) text = replace_once( text, "#define MOTOR_OPERATION_TIMEOUT_MS (60000ul)", '''/* The reference calibration firmware allows each endpoint leg up * to 180 seconds. Stall/endpoint detection remains independent and stops * motion much earlier when appropriate. */ #define MOTOR_OPERATION_TIMEOUT_MS (180000ul)''', "calibration operation timeout adapter", ) text = replace_once( text, "#define SIMPLE_DIRECTION_TEST_ACTIVE_MS (3000ul)", '''#if (MOTOR_OTA_INTERACTIVE_TEST != 0u) #define SIMPLE_DIRECTION_TEST_ACTIVE_MS (60000ul) #else #define SIMPLE_DIRECTION_TEST_ACTIVE_MS (3000ul) #endif''', "interactive timeout adapter", ) text = replace_once( text, '''#if (MOTOR_AUTOMATED_TEST_SCENARIO == MOTOR_TEST_SCENARIO_SIMPLE_DIRECTION) #if ((SIMPLE_DIRECTION_TEST_ACTIVE_MS != 3000ul) || \\ (SIMPLE_DIRECTION_TEST_DUTY_PERCENT != 70ul) || \\ (MOTOR_PWM_DUTY_PERCENT != SIMPLE_DIRECTION_TEST_DUTY_PERCENT)) #error "The simple direction test requires exactly 3000 ms at 70 percent duty." #endif #endif''', '''#if (MOTOR_AUTOMATED_TEST_SCENARIO == MOTOR_TEST_SCENARIO_SIMPLE_DIRECTION) #if (MOTOR_OTA_INTERACTIVE_TEST != 0u) #if ((SIMPLE_DIRECTION_TEST_ACTIVE_MS != 60000ul) || \\ (SIMPLE_DIRECTION_TEST_DUTY_PERCENT != 70ul) || \\ (MOTOR_PWM_DUTY_PERCENT != SIMPLE_DIRECTION_TEST_DUTY_PERCENT)) #error "The interactive direction test requires a 60 s safety timeout at 70 percent duty." #endif #else #if ((SIMPLE_DIRECTION_TEST_ACTIVE_MS != 3000ul) || \\ (SIMPLE_DIRECTION_TEST_DUTY_PERCENT != 70ul) || \\ (MOTOR_PWM_DUTY_PERCENT != SIMPLE_DIRECTION_TEST_DUTY_PERCENT)) #error "The simple direction test requires exactly 3000 ms at 70 percent duty." #endif #endif #endif''', "interactive assertion adapter", ) text = replace_once( text, '''#if (MOTOR_AUTOMATED_TEST_SCENARIO == MOTOR_TEST_SCENARIO_POSITION) #define MOTOR_PWM_DUTY_PERCENT (70ul) #elif (MOTOR_SIMPLE_DIRECTION_TEST_ENABLED != 0u)''', '''#if (MOTOR_AUTOMATED_TEST_SCENARIO == MOTOR_TEST_SCENARIO_POSITION) #define MOTOR_PWM_DUTY_PERCENT (70ul) #elif (MOTOR_OTA_SELF_COMPOSE != 0u) #define MOTOR_PWM_DUTY_PERCENT (80ul) #elif (MOTOR_SIMPLE_DIRECTION_TEST_ENABLED != 0u)''', "self-compose duty adapter", ) text = replace_once( text, ''' if(false == state->positionValid) { #if (MOTOR_SIMPLE_DIRECTION_TEST_ENABLED != 0u) /* Scenario 5 intentionally does not calibrate absolute position. * Scenarios 5 and 6 reuse scenario 3's Hall window and stall * event path in either direction; an endpoint is safely treated * as stall because these tests do not establish position. */ if((index == MOTOR_AUTOMATED_TEST_MODE) && (state->recentWindowPulses <= MOTOR_STALL_MAX_PULSES)) { Motor_RequestEventFromIsr(index, MOTOR_EVENT_STALL); } #endif continue; }''', ''' if(false == state->positionValid) { #if ((MOTOR_SIMPLE_DIRECTION_TEST_ENABLED != 0u) || \\ (MOTOR_OTA_SELF_COMPOSE != 0u)) /* Direction tests and self-compose drive steps intentionally run * before absolute endpoint calibration. Hall inactivity is a * fail-closed stall for the selected test motor; self-compose may * select any of the four validated motors at runtime. */ if((((MOTOR_OTA_SELF_COMPOSE != 0u) && (index < MOTOR_PROFILE_COUNT)) || ((MOTOR_SIMPLE_DIRECTION_TEST_ENABLED != 0u) && (index == MOTOR_AUTOMATED_TEST_MODE))) && (state->recentWindowPulses <= MOTOR_STALL_MAX_PULSES)) { Motor_RequestEventFromIsr(index, MOTOR_EVENT_STALL); } #endif continue; }''', "self-compose uncalibrated stall adapter", ) text = replace_once( text, ''' if((false != state->speedControlEnabled) && (false == state->calibrating)) { dutyPercent = state->speedInitialDutyPercent; }''', ''' if(false != state->calibrating) { /* Validated endpoint-seek torque from N7Calibration-backup-0805. */ dutyPercent = (motorIndex == (uint32_t)MOTOR_MODE_3_LEGREST) ? MOTOR_LEGREST_DEFAULT_DUTY_PERCENT : 80u; } else if(false != state->speedControlEnabled) { dutyPercent = state->speedInitialDutyPercent; }''', "calibration duty adapter", ) text = replace_once( text, ''' if(motorIndex == (uint32_t)MOTOR_MODE_3_LEGREST) { dutyPercent = MOTOR_LEGREST_FIXED_DUTY_PERCENT; state->fixedDutyPercent = MOTOR_LEGREST_FIXED_DUTY_PERCENT; state->speedControlEnabled = false; } else if(false != state->calibrating)''', ''' if((motorIndex == (uint32_t)MOTOR_MODE_3_LEGREST) && (MOTOR_OTA_CALIBRATION == 0u)) { dutyPercent = MOTOR_LEGREST_FIXED_DUTY_PERCENT; state->fixedDutyPercent = MOTOR_LEGREST_FIXED_DUTY_PERCENT; state->speedControlEnabled = false; } else if(false != state->calibrating)''', "calibration explicit legrest duty adapter", ) text = replace_once( text, ''' if(index == (uint32_t)MOTOR_MODE_3_LEGREST) { dutyPercent = MOTOR_LEGREST_FIXED_DUTY_PERCENT; motorRuntime[index].speedControlEnabled = false; } motorRuntime[index].fixedDutyPercent = dutyPercent;''', ''' if((index == (uint32_t)MOTOR_MODE_3_LEGREST) && (MOTOR_OTA_CALIBRATION == 0u)) { dutyPercent = MOTOR_LEGREST_FIXED_DUTY_PERCENT; motorRuntime[index].speedControlEnabled = false; } motorRuntime[index].fixedDutyPercent = dutyPercent;''', "calibration legrest duty setter adapter", ) text = replace_once( text, ''' savedIntrStatus = Cy_SysLib_EnterCriticalSection(); motorRuntime[index].maxHallCount = measuredPulses; motorRuntime[index].positionHallCount = measuredPulses; motorRuntime[index].positionValid = true;''', ''' savedIntrStatus = Cy_SysLib_EnterCriticalSection(); /* Endpoint travel validates the mechanism and measuredPulses is * retained for diagnostics. Production position conversion uses * the fixed N7 Base range and is never recalibrated at runtime. */ motorRuntime[index].maxHallCount = motorProfiles[index].defaultMaxHallCount; motorRuntime[index].positionHallCount = motorProfiles[index].defaultMaxHallCount; motorRuntime[index].positionValid = true;''', "fixed production Hall range adapter", ) text = replace_once( text, ''' savedIntrStatus = Cy_SysLib_EnterCriticalSection(); motorServiceActive = false; Cy_SysLib_ExitCriticalSection(savedIntrStatus); }''', ''' savedIntrStatus = Cy_SysLib_EnterCriticalSection(); motorServiceActive = false; Cy_SysLib_ExitCriticalSection(savedIntrStatus); #if (MOTOR_OTA_CALIBRATION != 0u) OtaCalibration_ServiceHook(); #elif (MOTOR_OTA_SELF_COMPOSE != 0u) OtaSelfCompose_ServiceHook(); #endif #if ((MOTOR_OTA_CALIBRATION != 0u) || (MOTOR_OTA_SELF_COMPOSE != 0u)) SeatDemoCompat_Service(); #endif }''', "OTA CAN service hook", ) text = replace_once( text, "int main(void)", '''#if ((MOTOR_OTA_INTERACTIVE_TEST != 0u) || \ (MOTOR_OTA_SELF_COMPOSE != 0u) || \ (MOTOR_OTA_CALIBRATION != 0u)) #include "ota_seat_runtime.inc" #endif #if (MOTOR_OTA_CALIBRATION != 0u) #include "ota_calibration.inc" #endif #if (MOTOR_OTA_INTERACTIVE_TEST != 0u) #include "ota_interactive_test.inc" #endif #if (MOTOR_OTA_SELF_COMPOSE != 0u) #include "ota_self_compose.inc" #endif #if ((MOTOR_OTA_CALIBRATION != 0u) || (MOTOR_OTA_SELF_COMPOSE != 0u)) #include "seat_demo_compat.inc" #endif int main(void)''', "OTA application main include adapter", ) text = replace_once( text, '''#elif (MOTOR_SIMPLE_DIRECTION_TEST_ENABLED != 0u) return SimpleDirectionTest_Run(driverInitOk);''', '''#elif (MOTOR_SIMPLE_DIRECTION_TEST_ENABLED != 0u) #if (MOTOR_OTA_INTERACTIVE_TEST != 0u) return OtaInteractiveTest_Run(driverInitOk); #else return SimpleDirectionTest_Run(driverInitOk); #endif''', "interactive run adapter", ) text = replace_once( text, '''#if (MOTOR_AUTOMATED_TEST_SCENARIO == MOTOR_TEST_SCENARIO_FIVE_MODE_HALL) return FiveModeTest_Run(driverInitOk);''', '''#if (MOTOR_OTA_CALIBRATION != 0u) return OtaCalibration_Run(driverInitOk); #elif (MOTOR_OTA_SELF_COMPOSE != 0u) return OtaSelfCompose_Run(driverInitOk); #elif (MOTOR_AUTOMATED_TEST_SCENARIO == MOTOR_TEST_SCENARIO_FIVE_MODE_HALL) return FiveModeTest_Run(driverInitOk);''', "self-compose run adapter", ) (output_src / "main_cm0plus.c").write_text(text, encoding="utf-8", newline="\n") header_source = source_root / "src" / "motor_control.h" header_text = header_source.read_text(encoding="utf-8") # The vendored base is now the single authoritative physical mapping. # Refuse to build if 01/03 are ever silently crossed again. expected_axis_definitions = ( "MOTOR_MODE0_MAX_POSITION_MM_X100 (21500ul)", "MOTOR_MODE1_DEFAULT_MAX_HALL_COUNT (373ul)", "MOTOR_MODE1_MAX_POSITION_MM_X100 (5500ul)", "MOTOR_MODE2_DEFAULT_MAX_HALL_COUNT (917ul)", "MOTOR_MODE2_MAX_ANGLE_DEG_X100 (6800ul)", "MOTOR_MODE3_DEFAULT_MAX_HALL_COUNT (647ul)", "MOTOR_MODE3_MAX_ANGLE_DEG_X100 (7200ul)", "MOTOR_MODE_1_HEIGHT", "MOTOR_MODE_3_LEGREST", ) missing = [value for value in expected_axis_definitions if value not in header_text] if missing: raise RuntimeError( "N7 axis mapping drifted; missing: " + ", ".join(missing) ) (output_src / "motor_control.h").write_text( header_text, encoding="utf-8", newline="\n" ) copies = { project_root / "firmware" / "application" / "ota_interactive_test.inc": "ota_interactive_test.inc", project_root / "firmware" / "application" / "ota_self_compose.inc": "ota_self_compose.inc", project_root / "firmware" / "application" / "ota_seat_runtime.inc": "ota_seat_runtime.inc", project_root / "firmware" / "application" / "ota_calibration.inc": "ota_calibration.inc", project_root / "firmware" / "application" / "ota_seat_state.h": "ota_seat_state.h", project_root / "firmware" / "application" / "seat_demo_compat.h": "seat_demo_compat.h", project_root / "firmware" / "application" / "seat_demo_compat.inc": "seat_demo_compat.inc", project_root / "firmware" / "application" / "seat_demo_dynamic.inc": "seat_demo_dynamic.inc", project_root / "firmware" / "application" / "seat_demo_thermal.inc": "seat_demo_thermal.inc", project_root / "firmware" / "application" / "ota_self_compose_plan.h": "ota_self_compose_plan.h", project_root / "firmware" / "bootloader" / "src" / "ota_can.h": "ota_can.h", project_root / "firmware" / "common" / "ota_protocol.h": "ota_protocol.h", project_root / "firmware" / "application" / "ota_enter_bootloader.h": "ota_enter_bootloader.h", } for source, name in copies.items(): if not source.is_file(): raise FileNotFoundError(source) shutil.copyfile(source, output_src / name) print(f"GCC_APPLICATION_SOURCE_OK path={output_root}") return 0 if __name__ == "__main__": raise SystemExit(main())