Исправить TESTLAMP, запуск CAN и расчёт фазных токов

Сохранить состояние TESTLAMP при кратких провалах входа и исключить повторный вход Timer1 во время передачи индикации.
Явно сбрасывать служебный обмен CAN при запуске и выбирать соседнюю фазу по локальному индексу канала АЦП.
Сохранить текущую последовательность DATA/CLK и проверочные варианты таймингов, добавить поведенческие тесты и обновить образ прошивки с картой памяти.
This commit is contained in:
2026-10-13 14:13:43 +03:00
parent fa20ed085b
commit 04a9d989fb
18 changed files with 1983 additions and 655 deletions
+44
View File
@@ -0,0 +1,44 @@
# Проверяет передачу фаз и диагностику на настоящих функциях measure.c без аппаратуры C28x.
import argparse # Принимает путь к компилятору хостовой модели, не привязывая тест к одной машине.
import os # Добавляет каталог компилятора в PATH для запуска его вспомогательных программ.
from pathlib import Path # Разрешает пути относительно теста независимо от рабочего каталога.
import re # Находит границы функций и единственное место исторической мутации.
import subprocess # Компилирует модель и проверяет коды завершения исполняемых проверок.
import tempfile # Размещает сгенерированный C и исполняемые файлы вне исходного дерева.
ROOT = Path(__file__).resolve().parents[2] # Поднимается от tests/current_measure к корню репозитория.
SOURCE = ROOT / "Source/Internal/measure.c" # Выбирает текущую production-реализацию измерений.
def extract(source, name): # Извлекает одну настоящую функцию, чтобы тест не содержал копию алгоритма.
match = re.search(r"(?:void|int|float)\s+" + name + r"\([^;{}]*\)\s*\{", source) # Отличает определение от прототипа и вызовов.
assert match is not None, name # Явно завершает тест, если production-функция исчезла или сменила форму.
end = source.index("{", match.start()) + 1 # Начинает подсчёт вложенности сразу после открывающей скобки.
depth = 1 # Учитывает уже найденный внешний блок функции.
while depth: # Читает определение целиком вместе с вложенными условиями.
depth += (source[end] == "{") - (source[end] == "}") # Находит соответствующую закрывающую скобку без обрезки вложенных блоков.
end += 1 # Включает последнюю обработанную скобку в извлекаемый диапазон.
return source[match.start():end] # Возвращает production-код без изменения его вычислений.
parser = argparse.ArgumentParser(description="Проверка фаз COMM и LOAD на хосте") # Описывает назначение самостоятельного запуска.
parser.add_argument("--cc", required=True, help="Путь к GCC") # Позволяет выбрать доступный C-компилятор без изменения теста.
args = parser.parse_args() # Разбирает аргументы до создания временных файлов.
source = re.sub(r"/\*.*?\*/|//[^\n]*", "", SOURCE.read_text(encoding="utf-8"), flags=re.S) # Убирает комментарии с примерами скобок, сохраняя исполняемый код.
body = "\n".join(extract(source, name) for name in ("im_calc", "er_anal", "reset_errs", "Current_count")) # Включает реальные расчёты, накопление ошибки и запись диагностических флагов.
before, after = Path(__file__).with_name("model.inc").read_text(encoding="utf-8").split("/* HANDLERS */") # Вставляет production-функции между моделью окружения и сценариями.
legacy, replacements = re.subn(r"\bpair\s*=\s*chan\s*\^\s*1\s*;", "pair = sens_pair[chan];", body) # Возвращает старую ошибку только во временную мутационную сборку.
assert replacements == 1, "Не найдено единственное место выбора пары фаз" # Не допускает фиктивной мутационной проверки неизменённого кода.
environment = dict(os.environ) # Сохраняет окружение вызывающего процесса для компилятора.
environment["PATH"] = str(Path(args.cc).resolve().parent) + os.pathsep + environment.get("PATH", "") # Делает библиотеки и вспомогательные программы GCC доступными дочерним процессам.
with tempfile.TemporaryDirectory(prefix="current-measure-") as directory: # Автоматически удаляет только временные артефакты этого запуска.
for name, functions in (("production", body), ("legacy", legacy)): # Прогоняет одинаковые поведенческие сценарии на исправлении и исторической ошибке.
c_path = Path(directory) / (name + ".c") # Разделяет исходники двух сборок для однозначных диагностик компилятора.
exe_path = Path(directory) / (name + ".exe") # Даёт каждой модели собственный исполняемый файл.
c_path.write_text(before + functions + after, encoding="utf-8") # Сохраняет временный C с русскими пояснениями в UTF-8.
subprocess.run([args.cc, "-std=c99", "-O3", "-fno-strict-aliasing", "-Werror=implicit-function-declaration", "-I", str(ROOT / "Source/Internal/Include"), str(c_path), "-lm", "-o", str(exe_path)], check=True, env=environment) # Компилирует с O3; отключение alias-оптимизации сохраняет production-псевдонимы ERROR поверх Modbus на хосте.
result = subprocess.run([str(exe_path)], capture_output=True, text=True, env=environment, timeout=20) # Ограничивает зависание и сохраняет результат каждого поведенческого сценария.
if name == "production": # Исправленная реализация обязана пройти все проверки без исключений.
print(result.stdout, end="") # Показывает сценарии, успешно проверенные настоящими функциями прошивки.
assert result.returncode == 0, result.stdout + result.stderr # Возвращает сбой запуска при любой регрессии исправленного кода.
else: # Старая строка должна воспроизводимо ломать и значения фаз, и диагностику.
assert result.returncode == 1 and "FAIL Modbus" in result.stdout and "FAIL Wry" in result.stdout, result.stdout + result.stderr # Доказывает чувствительность теста к обоим последствиям именно этой ошибки.
print("PASS mutation: legacy pair rejected by Modbus and Wry checks") # Сообщает об ожидаемом провале временной сборки со старой строкой.