Files
seat-firmware/tools/prepare_gcc_application_source.py

384 lines
14 KiB
Python

#!/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())