Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26f9d62896 | ||
|
|
4d101302be | ||
|
|
07929c880a | ||
|
|
4f7f6cdb87 | ||
|
|
1fc88684d5 | ||
|
|
fa5ea166a9 | ||
|
|
062f064c85 | ||
|
|
7b1a16d6e4 |
@@ -27,3 +27,6 @@ Desktop.ini
|
||||
|
||||
# Generated native library (built before packaging)
|
||||
python/protocan/native/
|
||||
|
||||
# Кэш pytest создаётся проверками автоматически и не входит в исходники общей библиотеки.
|
||||
.pytest_cache/
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
cmake_minimum_required(VERSION 3.16) # Эта версия поддерживает используемые target_compile_features и CTest без платформенных расширений.
|
||||
project(can485_monitor C) # Переносимое ядро собирается независимо от Linux-порта и приложения SETGUI.
|
||||
add_library(can485_monitor STATIC src/can485_monitor.c) # Все платформы связываются с одним обработчиком команд и wire-форматов.
|
||||
target_include_directories(can485_monitor PUBLIC include) # Потребитель получает только публичный C-контракт библиотеки.
|
||||
target_compile_features(can485_monitor PUBLIC c_std_99) # Ядро не требует C11 и платформенных расширений языка.
|
||||
include(CTest) # Стандартный BUILD_TESTING управляет автономной проверкой библиотеки.
|
||||
if(BUILD_TESTING) # Host-проверка не требует Linux, Qt и физической CAN-шины.
|
||||
add_executable(test_can485_monitor tests/test_can485_monitor.c) # Использует независимые wire-векторы и фикстуру аппаратных отказов.
|
||||
target_link_libraries(test_can485_monitor PRIVATE can485_monitor) # Проверяет настоящую библиотеку, не тестовую копию алгоритма.
|
||||
add_test(NAME can485_monitor_contract COMMAND test_can485_monitor) # Регистрирует единый поведенческий контракт для всех аппаратных портов.
|
||||
endif() # Завершает необязательную сборку тестов.
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux") # Linux-заголовки не попадают в сборки ESP32, Windows и других MCU.
|
||||
add_executable(zynq-can485d ports/zynq-linux/main.c) # SocketCAN, USB и Ethernet реализуются только аппаратным портом.
|
||||
target_link_libraries(zynq-can485d PRIVATE can485_monitor) # Linux использует ровно то же C99-ядро, что host-проверка.
|
||||
endif() # Завершает платформенную часть библиотеки.
|
||||
@@ -0,0 +1,75 @@
|
||||
# CAN485 monitor
|
||||
|
||||
Общее C99-ядро команд CAN485 DevBoard/SETGUI и Linux-порт для PS CAN Zynq.
|
||||
Протоколы CAN (SETCAN, ProtoCAN, УМП, Balsam, DEIF и остальные) разбираются
|
||||
существующими библиотеками `templates/python`, без копий в проекте платы.
|
||||
|
||||
Слои: SETGUI BoardTab → `qt_ports/can485_port.py` → Ethernet TCP или USB tty →
|
||||
`ports/zynq-linux/main.c` → `can485_monitor.c` → SocketCAN / RS485 / каталог SD.
|
||||
В ядре нет Linux, Qt, HAL, выделения памяти и глобального контекста.
|
||||
|
||||
| Файл | Назначение и зависимости |
|
||||
|---|---|
|
||||
| `include/can485_monitor.h` | Контракт C99, `stdint.h`, `stddef.h` |
|
||||
| `src/can485_monitor.c` | Проверка команд, фильтр, маршруты, AA55/CRC8, строки CAN |
|
||||
| `ports/zynq-linux/main.c` | SocketCAN, rtnetlink, TCP, USB tty, UART RS485, SD, POSIX/Linux |
|
||||
| `tests/test_can485_monitor.c` | Эталонный пакет, ошибки/границы, маршрутизация, отказы оборудования |
|
||||
|
||||
Порт предоставляет четыре callback: `emit(context,line)`,
|
||||
`configure(context,kbps,mode,running)`, `send(context,frame,source)` и
|
||||
`control(context,command)`. `source=0` означает CAN, `source=1` — RS485.
|
||||
`configure` и `send` возвращают 1 только после принятия операции драйвером.
|
||||
Аппаратный ACK этим результатом не подтверждается. Контекст и буферы принадлежат приложению.
|
||||
|
||||
```c
|
||||
#include "can485_monitor.h" /* Подключает переносимое ядро библиотеки. */
|
||||
void example(cm_port port) { /* Аппаратные callback передаются вызывающей платформой. */
|
||||
cm_monitor state; /* Контекст не разделяется с другим экземпляром моста. */
|
||||
cm_init(&state, port); /* CAN остановлен, оба направления маршрута выключены. */
|
||||
cm_command(&state, "V"); /* Версия и статус не меняют оборудование. */
|
||||
cm_command(&state, "S125"); /* Запрашивает CAN 125 кбит/с через аппаратный callback. */
|
||||
cm_command(&state, "M1"); /* Listen-only запрещает ручной TX и RS485→CAN. */
|
||||
cm_frame frame = {0x123, 2, 0, {0xAB, 0xCD}}; /* STD ID и два исходных байта. */
|
||||
cm_receive(&state, &frame, 0, 12345); /* Приём CAN в момент 12,345 с монотонного времени. */
|
||||
cm_status(&state, 12345, "sd=idle"); /* Статус содержит только известные платформе поля. */
|
||||
} /* Реальная плата передаёт только фактически принятые кадры в cm_receive. */
|
||||
```
|
||||
|
||||
Поддержаны команды `V ? H S M O C F Q R B T X Z L D G I` и Linux-расширение
|
||||
`Ncan0` / `Ncan1`. Команды строго проверяются целиком. RS485-пакет:
|
||||
`AA 55 FLAGS ID[4]be DLC DATA[8] CRC8(FLAGS..DATA, poly=0x07, init=0)`.
|
||||
CAN STD/EXT/RTR сохраняются. DATA TX через `T` повторяет контракт существующего ESP GUI.
|
||||
|
||||
Linux-порт запускается с CAN остановленным, маршрутами и записью SD выключенными.
|
||||
`S`/`M`/`O` запускают CAN; `C` останавливает интерфейс. Настройка через rtnetlink
|
||||
требует root или `CAP_NET_ADMIN`; RAW CAN — соответствующих прав Linux.
|
||||
M2 требует реальной поддержки `CAN_CTRLMODE_PRESUME_ACK`; ошибка драйвера
|
||||
не маскируется под успешный no-ack. Фильтр программный и не отключает аппаратный ACK.
|
||||
|
||||
TCP-порт по умолчанию **37579**, один управляющий клиент. Подключённый USB
|
||||
может получать поток, но команды второго транспорта возвращают `#ERR busy`.
|
||||
Второй TCP-клиент не вытесняет первого. При обрыве владельца маршруты выключаются,
|
||||
его SD-download отменяется. Медленный клиент отключается при заполнении очереди 64 КиБ.
|
||||
Служба не включает периодический CAN TX: этот таймер принадлежит SETGUI и
|
||||
останавливается при отключении. Смена `N` закрывает только собственный CAN socket.
|
||||
|
||||
`--usb /dev/ttyGS0` использует USB OTG CDC ACM, если образ Linux предоставляет
|
||||
gadget serial. Также допустим выделенный UART через USB-UART, свободный от
|
||||
console/getty. Разъём USB HOST и USB-JTAG сами по себе CDC не создают.
|
||||
USB и RS485 должны использовать разные tty. RTS/CTS выключены. RS485 требует
|
||||
физического трансивера с аппаратным управлением направлением либо предварительной
|
||||
настройки драйвера UART; Linux-порт не переключает произвольные GPIO платы.
|
||||
|
||||
SD — заранее выбранный каталог `--logs`, например `/mnt/sd/can-logs`.
|
||||
Журналы `can_*.csv` содержат совместимые текстовые строки кадров; импорт SETGUI
|
||||
распознаёт этот формат автоматически. Выдача файла асинхронная, блоками 128 байт,
|
||||
с проверяемыми offset/size/CRC32. Путь ограничен одним каталогом, `..` и symlink
|
||||
запрещены. Запись SD выключается перед чтением. `vin`, TEC и REC не выдумываются,
|
||||
если платформа не получила реальные значения.
|
||||
|
||||
Используется: Zynq RK7020 (`demo/can485`) и CAN-панель SETGUI (`pc/zynq_can_panel.py`).
|
||||
Для единого физического источника задавайте одинаковый **TEMPLATES_ROOT** при
|
||||
запуске SETGUI и сборке ARM (`build.ps1 -Templates <тот же каталог>`).
|
||||
После изменения Python перезапустите приложение; изменение C требует пересборки
|
||||
службы, изменение встроенных библиотек EXE — пересборки EXE. Подмодули разных
|
||||
checkout обновляются явно: Git не обновляет уже собранную прошивку автоматически.
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef CAN485_MONITOR_H /* Защищает публичный контракт монитора от повторного включения. */
|
||||
#define CAN485_MONITOR_H /* Одинаковое ядро обслуживает команды DevBoard на разных платформах. */
|
||||
#include <stdint.h> /* Фиксирует размеры CAN ID, счётчиков и байтов независимо от процессора. */
|
||||
#include <stddef.h> /* Размеры выходных строк выражены штатным типом size_t. */
|
||||
typedef struct { uint32_t id; uint8_t dlc, flags, data[8]; } cm_frame; /* flags: бит 0 EXT, бит 1 RTR; Classic CAN содержит до восьми байтов. */
|
||||
typedef struct { /* Платформа предоставляет ввод-вывод, не копируя разбор команд. */
|
||||
void (*emit)(void *, const char *); /* Принимает одну законченную строку с LF для Ethernet, USB и журнала. */
|
||||
int (*configure)(void *, unsigned, unsigned, int); /* Применяет битрейт в кбит/с, режим M0/M1/M2 и состояние драйвера; ноль означает ошибку. */
|
||||
int (*send)(void *, const cm_frame *, int); /* Передаёт кадр в CAN при source=0 либо в RS485 при source=1; успех означает принятие локальным драйвером. */
|
||||
int (*control)(void *, const char *); /* Обслуживает аппаратную скорость B и файловые L/D/G/I; ответы формирует сам порт. */
|
||||
void *context; /* Контекст принадлежит вызывающему коду и позволяет иметь несколько независимых мостов. */
|
||||
} cm_port; /* В ядре нет HAL, Linux, Qt, динамической памяти и глобального состояния. */
|
||||
typedef struct { /* Состояние протокола отделено от сетевых клиентов и файловых дескрипторов. */
|
||||
cm_port port; unsigned bitrate, mode, route, baud, running; /* Скорости заданы в кбит/с для CAN и бод для RS485; route использует два младших бита направлений. */
|
||||
uint32_t filter_id, filter_mask, rx, tx, rsrx, rstx, buserr, dropcan, droprs, rscrc; /* Счётчики относятся к фактическим операциям порта; фильтр сравнивает только 29 бит ID. */
|
||||
uint64_t now; /* Последнее подтверждённое монотонное время позволяет ответу V сохранять реальный uptime между периодическими статусами. */
|
||||
} cm_monitor; /* Всё изменяемое состояние хранится в экземпляре вызывающего приложения. */
|
||||
void cm_init(cm_monitor *, cm_port); /* Инициализирует остановленный CAN 125 кбит/с, normal, маршруты выключены и RS485 512000 бод. */
|
||||
void cm_command(cm_monitor *, const char *); /* Проверяет полную команду без CR/LF и применяет её только после успешной валидации. */
|
||||
void cm_receive(cm_monitor *, const cm_frame *, int, uint64_t); /* Принимает кадр CAN=0 либо RS485=1 и монотонное время в миллисекундах. */
|
||||
void cm_status(cm_monitor *, uint64_t, const char *); /* Печатает совместимый #STAT; дополнительная строка содержит подтверждённые поля SD/Linux. */
|
||||
int cm_packet_decode(const uint8_t *, cm_frame *); /* Проверяет все 17 байт AA55/CRC8, формат ID и DLC до маршрутизации RS485. */
|
||||
void cm_packet_encode(const cm_frame *, uint8_t *); /* Формирует RS485 AA55, FLAGS, ID big-endian, DLC, восемь байтов и CRC8/0x07. */
|
||||
#endif /* Завершает независимый публичный контракт CAN485 monitor. */
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,30 @@
|
||||
"""Проверяет настоящий SCI8 TI против ограничений штатного загрузчика UlitCo.""" # Проверка не открывает COM-порт и не изменяет Flash платы.
|
||||
from pathlib import Path # Находит общий сборщик и установленный TI compiler относительно теста и стандартных каталогов.
|
||||
import os # Позволяет CI явно выбрать установленный TI compiler без путей конкретного пользователя.
|
||||
import struct # Читает эталонный заголовок SCI8 и два слова команды перехода C28x.
|
||||
import subprocess # Получает фактическую точку входа COFF официальным инструментом TI OFD.
|
||||
import sys # Подключает ту же реализацию build_kernel, которую использует postbuild CCS.
|
||||
import tempfile # Каждая компиляция создаёт независимые файлы SDK и результаты.
|
||||
import unittest # Запускает аппаратно независимую проверку стандартной библиотекой Python.
|
||||
import xml.etree.ElementTree as ET # Извлекает адрес _InitBoot из официального описания исполняемого COFF.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools')) # В рабочем templates сборщик находится рядом с каталогом tests.
|
||||
import c2000_sci_build as sci # Компилирует настоящее RAM-ядро без подмены TI compiler и hex2000.
|
||||
|
||||
class SciKernelBuildTests(unittest.TestCase): # Фиксирует несовместимость entry=0x9000 со штатным primary_bootloader.obj.
|
||||
def test_kernel_enters_at_zero_and_branches_to_initboot(self): # Проверяет транспортный вход и реальное назначение машинного перехода.
|
||||
compiler = Path(os.environ.get('FIRMWARE_C2000_COMPILER', 'C:/ti/ccs1230/ccs/tools/compiler/ti-cgt-c2000_22.6.0.LTS')) # Переменная переопределяет стандартную установку TI на машинах CI.
|
||||
if not (compiler / 'bin/cl2000.exe').is_file(): self.skipTest('Не установлен TI C2000 compiler') # Хостовые проверки без проприетарного toolchain остаются доступными.
|
||||
try: ware = sci.find_c2000ware() # Использует тот же поиск SDK F28335, что штатная сборка проекта.
|
||||
except ValueError as error: self.skipTest(str(error)) # Отсутствующий SDK явно помечает проверку как пропущенную.
|
||||
with tempfile.TemporaryDirectory(prefix='ulitco-sci-entry-') as temporary: # Компиляция не изменяет исходники установленного SDK.
|
||||
image = sci.build_kernel(compiler, Path(temporary) / 'f28335_sci_kernel.bin', 30000000, 0x0080, ware) # Плата использует кварц 30 МГц; маска 0x80 выбирает сектор H из комплекта, вызвавшего ошибку.
|
||||
data = image.read_bytes() # Проверяет конечный транспортный BIN после настоящего hex2000.
|
||||
self.assertEqual(data[:2], b'\xAA\x08') # Действующий приёмник ожидает ключ SCI8 0x08AA в порядке младшего байта вперёд.
|
||||
self.assertEqual(struct.unpack_from('<HH', data, 18), (0, 0), 'primary_bootloader.obj отклоняет ненулевой адрес входа') # Восемь резервных слов после ключа оставляют entry на смещении 18 октетов.
|
||||
self.assertEqual(struct.unpack_from('<HHH', data, 22), (2, 0, 0)) # Первый блок должен загрузить двухсловный переход по словному адресу 0x000000.
|
||||
document = ET.fromstring(subprocess.check_output([str(compiler / 'bin/ofd2000.exe'), '-x', str(image.with_suffix('.out'))])) # Извлекает исходную точку входа COFF до её переопределения транспортным заголовком SCI8.
|
||||
entry = int(document.findtext('.//optional_file_header/entry_point'), 0) # Сборщик линкует COFF с символом _InitBoot, который подготавливает стек и _ExitBoot.
|
||||
self.assertGreater(entry, 0) # Нулевой codestart должен перенаправлять исполнение в отдельно размещённую функцию TI.
|
||||
self.assertEqual(struct.unpack_from('<HH', data, 28), (0x0040 | (entry >> 16), entry & 0xFFFF)) # Кодирование LB C28x содержит старшие шесть бит адреса в первом слове и младшие 16 во втором; переход должен вести именно в _InitBoot.
|
||||
|
||||
if __name__ == '__main__': unittest.main() # Позволяет выполнить проверку отдельно от полного набора тестов библиотеки.
|
||||
@@ -39,6 +39,22 @@ class IdentityTests(unittest.TestCase):
|
||||
self.assertEqual(self.prepare([clean,dirty])["build_id"], "abcdef0+")
|
||||
self.assertEqual(self.prepare([dirty,clean])["build_id"], "abcdef0+")
|
||||
|
||||
def test_project_and_repository_survive_sealing(self): # Проверяет сведения, нужные Публикатору после переноса сборки на другой компьютер.
|
||||
for project_name in (None, "Бальзам — ПМ35"): # Поддерживает прежние конфигурации и явно заданное русское название.
|
||||
settings = {"product": "PM35", "target": "TMS320F28335", "version": [1, 2, 3]} # Код изделия сохраняет техническое назначение независимо от названия проекта.
|
||||
if project_name is not None: # Отсутствующее поле должно использовать совместимое значение по умолчанию.
|
||||
settings["project"] = project_name # Название из конфигурации переносится в паспорт без сокращений.
|
||||
self.config.write_text(json.dumps(settings), encoding="utf-8") # Конфигурация сохраняется в той же кодировке, что и проект пользователя.
|
||||
clean = {"commit": "a" * 40, "dirty": False} # Источник и templates остаются известными чистыми состояниями.
|
||||
metadata = self.prepare([dict(clean), dict(clean)]) # Генерирует настоящий precompile JSON и заголовок без запуска компилятора.
|
||||
self.assertEqual(metadata["project"], project_name or "PM35") # Старый JSON получает код изделия, новый — заданное название.
|
||||
self.assertEqual(metadata["source"]["repository"], str(self.root.resolve())) # Абсолютный путь сохраняется в состоянии исходного проекта.
|
||||
image = self.root / "controller.bin" # Отдельный образ позволяет проверить последующий postbuild manifest.
|
||||
image.write_bytes(b"firmware fixture") # Небольшой файл достаточен для проверки сохранения полей и SHA-256.
|
||||
sealed = identity.seal(image, self.root / "firmware_identity.json") # Штатный postbuild дополняет сохранённое происхождение без чтения нового HEAD.
|
||||
self.assertEqual(sealed["project"], metadata["project"]) # Название доходит до того manifest, который импортирует GUI.
|
||||
self.assertEqual(sealed["source"], metadata["source"]) # Путь, полный коммит и признак dirty переживают запечатывание образа.
|
||||
|
||||
def test_missing_git_is_explicit(self):
|
||||
unknown = {"commit":None,"dirty":None}
|
||||
self.assertEqual(self.prepare([unknown,unknown])["build_id"], "NOGIT000")
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Проверки CCS/Keil с интерфейсами pathlib и shutil, доступными в Python 3.7.""" # Воспроизводит ограничения старого Python без установки другого интерпретатора.
|
||||
import importlib.util # Загружает проверяемые инструменты именно из этой библиотеки templates.
|
||||
import json # Проверяет сохранённый счётчик и фактический паспорт компиляции.
|
||||
from pathlib import Path # Моделирует прежнюю сигнатуру удаления временных файлов.
|
||||
import shutil # Моделирует прежнюю сигнатуру копирования каталогов.
|
||||
import tempfile # Ограничивает запись изолированным каталогом теста.
|
||||
import unittest # Проверки запускаются стандартным Python без сторонних зависимостей.
|
||||
from unittest.mock import patch # Подменяет только несовместимые сигнатуры стандартной библиотеки.
|
||||
|
||||
|
||||
def load_tool(name): # Импортирует одну точку входа без запуска CLI.
|
||||
spec = importlib.util.spec_from_file_location("compatibility_" + name, Path(__file__).resolve().parents[1] / "tools" / (name + ".py")) # Выбор не зависит от PATH компьютера сборки.
|
||||
module = importlib.util.module_from_spec(spec) # Отдельный экземпляр сохраняет изоляцию других тестов.
|
||||
spec.loader.exec_module(module) # Загружает публичные функции подготовки и копирования.
|
||||
return module # Тесты вызывают те же функции, что BAT-обвязки двух IDE.
|
||||
|
||||
|
||||
identity, c2000 = load_tool("identity"), load_tool("c2000_build") # Обе цепочки используют общий формат паспорта.
|
||||
original_unlink, original_copytree = Path.unlink, shutil.copytree # Сохраняет настоящие реализации перед ограничением их сигнатур.
|
||||
|
||||
|
||||
def unlink37(path): # Python 3.7 не допускает аргумент missing_ok.
|
||||
return original_unlink(path) # Реальное удаление сохраняет настоящие ошибки доступа и отсутствующего файла.
|
||||
|
||||
|
||||
def copytree37(source, destination, symlinks=False, ignore=None, copy_function=shutil.copy2, ignore_dangling_symlinks=False): # Сигнатура Python 3.7 не допускает dirs_exist_ok.
|
||||
return original_copytree(source, destination, symlinks, ignore, copy_function, ignore_dangling_symlinks) # Старый интерфейс продолжает копировать обычные новые каталоги.
|
||||
|
||||
|
||||
class Python37CompatibilityTests(unittest.TestCase): # Проверяет успешный prebuild и повторный экспорт вместо одного синтаксического анализа.
|
||||
def setUp(self): # Каждый сценарий начинает с отдельного проекта со включённым итератором версии.
|
||||
temporary = tempfile.TemporaryDirectory(prefix="firmware python37 ") # Пробелы в пути соответствуют обычным пользовательским библиотекам.
|
||||
self.addCleanup(temporary.cleanup) # Очистка выполняется после снятия ограничений стандартной библиотеки.
|
||||
self.root = Path(temporary.name) # Рабочие проекты CCS/Keil не изменяются.
|
||||
self.config = self.root / "firmware_identity.json" # Конфигурация принадлежит только временной проверке.
|
||||
self.config.write_text(json.dumps({"product": "TEST_BOARD", "target": "TMS320F2812", "version": [1, 2, 3], "increment_version": True}), encoding="utf-8") # Этот режим воспроизводит исходную ошибку unlink после увеличения версии.
|
||||
|
||||
def test_keil_and_ccs_prepare_generate_headers_with_old_unlink(self): # Обе обвязки должны завершить генерацию при старой сигнатуре pathlib.
|
||||
with patch.object(Path, "unlink", unlink37): # Любой оставшийся missing_ok вызовет тот же TypeError, что в журнале пользователя.
|
||||
keil = identity.prepare(self.root, self.root, self.config, self.root / "Generated/target_0") # Keil напрямую вызывает общий prepare.
|
||||
ccs = c2000.prepare(self.root, Path(__file__).resolve().parents[3], self.root / "STEND40_PCH_1/generated", config=self.config) # CCS загружает генератор из полной проверяемой библиотеки templates.
|
||||
for relative, expected in (("Generated/target_0", [1, 2, 4]), ("STEND40_PCH_1/generated", [1, 2, 5])): # Две попытки резервируют разные номера без пропуска после TypeError.
|
||||
output = self.root / relative # Проверяет фактический include-каталог каждой IDE.
|
||||
self.assertIn("#define FIRMWARE_VERSION_PATCH " + str(expected[2]) + "U", (output / "firmware_identity.h").read_text(encoding="utf-8")) # Заголовок существует и содержит зарезервированную версию.
|
||||
self.assertEqual(json.loads((output / "firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Паспорт совпадает с константами для компилятора.
|
||||
self.assertEqual((keil["version"], ccs["version"]), ([1, 2, 4], [1, 2, 5])) # Обе публичные функции возвращают успешный результат.
|
||||
|
||||
def test_archive_export_and_atomic_copy_support_old_file_apis(self): # Проверяет повторное сохранение комплекта после успешного prebuild.
|
||||
generated = self.root / "generated" # Метаданные сохраняются отдельно от исходного JSON.
|
||||
identity.prepare(self.root, self.root, self.config, generated) # Получает настоящий паспорт для проверки SHA-256 и архива.
|
||||
image = self.root / "application.bin" # Произвольные байты достаточны для общего Keil-контракта файловой целостности.
|
||||
image.write_bytes(b"firmware bytes") # Проверяет сохранение точных байтов при нескольких копированиях.
|
||||
with patch.object(Path, "unlink", unlink37), patch.object(shutil, "copytree", copytree37): # Воспроизводит оба ограничения Python 3.7 одновременно.
|
||||
for attempt in range(2): # Второй экспорт должен объединять уже существующие каталоги.
|
||||
result = identity.archive_build(image, generated / "firmware_identity.json", self.root / "archive", export=self.root / "export") # Печать папки соответствует настоящему postcompile Keil.
|
||||
folder = Path(result["directory"]) # Использует точный архив текущего вызова.
|
||||
(folder / "nested").mkdir(exist_ok=True) # Вложенный сопроводительный файл проверяет рекурсивное копирование.
|
||||
(folder / "nested/map.txt").write_text("map " + str(attempt), encoding="utf-8") # Повторный выпуск обновляет существующий файл.
|
||||
identity.copy_tree(folder, self.root / "ccs-output", copy_function=c2000.atomic_copy) # C2000 сохраняет атомарную замену с прежним интерфейсом unlink.
|
||||
self.assertEqual((self.root / "ccs-output/nested/map.txt").read_text(encoding="utf-8"), "map 1") # Вложенные файлы обновились при повторном выпуске.
|
||||
self.assertEqual((Path(result["exported_directory"]) / image.name).read_bytes(), image.read_bytes()) # Экспорт содержит исходный образ без изменения байтов.
|
||||
self.assertFalse(list((self.root / "ccs-output").rglob(".firmware-*"))) # Успешная атомарная замена не оставляет незавершённых файлов.
|
||||
|
||||
|
||||
if __name__ == "__main__": # Позволяет запустить регрессию стандартным unittest на компьютере сборки.
|
||||
unittest.main() # Ненулевой результат выявляет несовместимость инструментов до настройки IDE.
|
||||
@@ -15,6 +15,7 @@ import xml.etree.ElementTree as ET # Проверяет фактические
|
||||
import sys # Запускает общий CLI и подключает конвертор без зависимости от GUI.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / 'python')) # Контракт защищённой Flash берётся из того же checkout templates, что общий сборщик.
|
||||
from protocan.f28335_flash import APP_START, APP_DATA, APP_STOP, coff_sections, validate_words # Проверяет отсутствие сектора A и описание FAB1 одинаково для COFF и SCI8.
|
||||
from set_devices.firmware_flashing import validate_flashing # Сверяет параметры CAN/RS тем же контрактом, что prebuild Keil/CCS и SETGUI.
|
||||
|
||||
def numeric_define(header, name): # Читает единственное числовое C-определение без исполнения выражений из заголовка платы.
|
||||
text = header.read_text(encoding="utf-8-sig") # Поддерживает UTF-8 с BOM для пользовательских заголовков конфигурации.
|
||||
@@ -181,7 +182,10 @@ def atomic_copy(source, destination): # Сохраняет проверенны
|
||||
shutil.copy2(source, temporary) # Сначала полностью записывает новые байты и сохраняет время исходного артефакта.
|
||||
temporary.replace(destination) # Одной файловой операцией заменяет старый результат после успешной записи временного файла.
|
||||
finally: # Незавершённый временный файл не должен попадать в будущий комплект Публикатора.
|
||||
temporary.unlink(missing_ok=True) # Удаляет только собственный незавершённый файл; после успешной замены его уже нет.
|
||||
try: # Python 3.7 не поддерживает параметр missing_ok у pathlib.Path.unlink.
|
||||
temporary.unlink() # Удаляет только собственный незавершённый файл при ошибке копирования или замены.
|
||||
except FileNotFoundError: # Успешный replace уже перенёс файл на постоянное имя.
|
||||
pass # Отсутствующий временный файл не превращает успешное копирование в ошибку сборки.
|
||||
return str(destination) # Сохраняет контракт copy_function стандартного shutil.copytree.
|
||||
|
||||
|
||||
@@ -360,9 +364,17 @@ def publish(project, templates, tools_templates, compiler, build_dir, artifact,
|
||||
metadata = json.loads(metadata_path.read_text(encoding="utf-8")) # Отсутствующий prebuild останавливает работу до создания выпуска.
|
||||
profile = read_profile(project, templates, eeprom) # Повторно читает текущий выбор EEPROM и общий резерв параметров.
|
||||
settings = json.loads((project / "firmware_identity.json").read_text(encoding="utf-8-sig")) if (project / "firmware_identity.json").is_file() else metadata # Сверяет product и target с действующим проектом.
|
||||
if validate_flashing(settings.get("flashing", {})) != metadata.get("flashing", {}): raise ValueError("Flashing settings changed after prebuild; rebuild the project") # Изменённые скорость или адрес после компиляции требуют нового prebuild и согласованного manifest.
|
||||
expected = {"product": settings["product"], "target": settings["target"], "version": configured_version(project, settings), **({"templates_root": str(templates)} if "eeprom_bytes" in profile else {}), **profile} # Ручной номер и аппаратный вариант обязаны совпадать с prebuild.
|
||||
if any(metadata.get(key) != value for key, value in expected.items()) or metadata.get("can_boot_profile") != profile.get("can_boot_profile"): # Включение и отключение CAN после prebuild также требуют пересборки, как изменение версии, EEPROM или зависимости.
|
||||
raise ValueError("Configuration changed: run prebuild and rebuild before publishing") # Требует пересборку вместо подмены старых метаданных.
|
||||
if metadata.get("flashing", {}).get("default_loader") == "f28335-sci": # Настроенный SCI-профиль автоматически подключает общую сборку приложения и RAM-ядра без проектного Python-файла.
|
||||
if metadata["target"].upper() != "TMS320F28335": raise ValueError("f28335-sci требует target TMS320F28335") # Не применяет Flash API другого процессора по совпадению транспорта.
|
||||
from types import SimpleNamespace # Передаёт общие проверки в специализированную ветку без копирования их реализации.
|
||||
spec = importlib.util.spec_from_file_location("shared_c2000_sci_build", tools_templates / "c/firmware-info/tools/c2000_sci_build.py") # Загружает реализацию именно выбранной полной библиотеки templates.
|
||||
sci = importlib.util.module_from_spec(spec); spec.loader.exec_module(sci) # Импорт не запускает компилятор или запись аппаратуры.
|
||||
api = SimpleNamespace(load_identity=load_identity, verify_embedded=verify_embedded, verify_package=verify_package, write_receipt=write_receipt) # Один набор проверок обслуживает прежний и новый маршруты выпуска.
|
||||
return sci.publish(api, project, compiler, build_dir, artifact, metadata, output, export) # Имя *_flash_rs.bin теперь определяется фактической веткой общего postbuild.
|
||||
verify_linker_profile(map_path.resolve(), profile) if "eeprom_bytes" in profile else None # Проверяет аппаратный выбор линкера, который нельзя вывести только из Python-конфигурации.
|
||||
with tempfile.TemporaryDirectory(prefix=".verified-release-", dir=build_dir) as directory: # Единственное место, куда общий postcompile пишет до подтверждения пригодности BIN.
|
||||
stage = Path(directory) # Изолирует все архивы и промежуточные HEX от постоянных каталогов выпуска.
|
||||
@@ -386,7 +398,7 @@ def publish(project, templates, tools_templates, compiler, build_dir, artifact,
|
||||
output.mkdir(parents=True, exist_ok=True) # Прежние проверенные выпуски сохраняются при добавлении нового.
|
||||
for item in sorted(stage.iterdir(), key=lambda item: item.is_dir()): # Переносит только файлы и архив, созданные данным postcompile.
|
||||
if item.is_dir(): # Архив содержит сопроводительные результаты той же сборки.
|
||||
shutil.copytree(item, output / item.name, dirs_exist_ok=True, copy_function=atomic_copy) # Сохраняет только проверенный архив с атомарной заменой файлов при повторном выпуске той же компиляции.
|
||||
identity.copy_tree(item, output / item.name, copy_function=atomic_copy) # Общий обход совместим с Python 3.7 и сохраняет атомарную замену каждого проверенного файла.
|
||||
else: # Общие служебные файлы, если они появятся, сохраняются в корне текущего выпуска.
|
||||
atomic_copy(item, output / item.name) # Не открывает старые файлы другой ветки на перезапись и не оставляет частичный BIN/HEX при отказе записи.
|
||||
verify_package(output / image_name, metadata) # Подтверждает отсутствие повреждения при копировании на постоянный диск.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
|
||||
"""Проверка зависимостей автоматической настройки CCS/Keil до изменения проекта.""" # Запускается общей Python-обвязкой из SETGUI без компиляции или обращения к MCU.
|
||||
import argparse # Принимает только конкретный профиль загрузки из проверенного JSON.
|
||||
import sys # Возвращает диагностический код установщику проекта.
|
||||
from c2000_sci_build import find_c2000ware # Поиск F28335 Kernel A совпадает с настоящим postbuild.
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__) # Локальная проверка не зависит от запущенной IDE.
|
||||
parser.add_argument("--loader", default="") # Старые проекты без выбранного SCI-профиля требуют только стандартную библиотеку Python.
|
||||
args = parser.parse_args() # Не допускает неизвестные параметры проверки окружения.
|
||||
try: # Отсутствующая зависимость должна остановить применение до создания резервных копий проекта.
|
||||
if args.loader == "f28335-sci": print("C2000Ware: " + str(find_c2000ware())) # Для родного SCI автоматически проверяет установленное ядро TI.
|
||||
print("Python: " + sys.executable) # Диагностика показывает фактический интерпретатор prebuild/postbuild.
|
||||
except (ValueError, OSError) as error: # Ошибки поиска SDK отображаются без Python traceback в интерфейсе оператора.
|
||||
print(str(error), file=sys.stderr); sys.exit(2) # Ненулевой код не позволяет GUI записать неполную настройку IDE.
|
||||
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import hashlib
|
||||
import importlib.util # Загружает общий контракт JSON без импорта протокольного runtime и без зависимости от установленного пакета.
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -18,6 +19,10 @@ from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile # Резервирует номер сборки через временный файл на том же диске, как общий итератор ветки 166.
|
||||
|
||||
_flashing_spec = importlib.util.spec_from_file_location("firmware_flashing_contract", Path(__file__).resolve().parents[3] / "python/set_devices/firmware_flashing.py") # Общий контракт переносится вместе с полным checkout templates.
|
||||
_flashing = importlib.util.module_from_spec(_flashing_spec) # Изолирует проверку настроек от GUI и системных транспортов.
|
||||
_flashing_spec.loader.exec_module(_flashing) # Использует ту же проверку полей, что SETGUI при чтении manifest.
|
||||
|
||||
|
||||
def git_state(repository: Path) -> dict:
|
||||
"""Record full commits, and flag untracked sources as well as tracked edits.
|
||||
@@ -66,6 +71,7 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
|
||||
for field in ("product", "target"):
|
||||
if not isinstance(settings.get(field), str) or not settings[field].strip():
|
||||
raise ValueError(f"missing {field}")
|
||||
flashing = _flashing.validate_flashing(settings.get("flashing", {})) # Проверяет скорости и адреса до резервирования следующего номера сборки.
|
||||
if settings.get("increment_version", False): # Прямые вызовы templates из CCS, Keil и CLI используют один и тот же сохраняемый итератор.
|
||||
version = settings["version"] = next_version(version) # Вычисляет номер для JSON, заголовка и метаданных только один раз.
|
||||
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=config.parent, prefix=".firmware-version-", delete=False) as stream: # Временный файл на том же диске позволяет атомарно заменить настройки без частичной записи.
|
||||
@@ -73,8 +79,13 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
|
||||
try: stream.write(json.dumps(settings, ensure_ascii=False, indent=2) + "\n") # Сохраняет новый номер и все дополнительные поля проекта в UTF-8.
|
||||
except BaseException: stream.close(); temporary.unlink(); raise # Ошибка записи оставляет исходный JSON целым и удаляет незавершённый файл.
|
||||
try: temporary.replace(config) # Занимает номер до дальнейших шагов, поэтому отказ генерации или компиляции не повторит его.
|
||||
finally: temporary.unlink(missing_ok=True) # Удаляет временный файл при ошибке замены; после успеха он уже переименован.
|
||||
finally: # Очистка должна работать и в Python 3.7, который ещё не принимает missing_ok.
|
||||
try: # После успешной замены временное имя уже отсутствует.
|
||||
temporary.unlink() # Удаляет только собственный незавершённый файл при отказе replace.
|
||||
except FileNotFoundError: # Отсутствие файла после успешного rename является штатным результатом.
|
||||
pass # Другие ошибки файловой системы продолжают останавливать подготовку.
|
||||
source, dependency = git_state(repository), git_state(templates)
|
||||
source = dict(source, repository=str(repository.resolve())) # Сохраняет абсолютный локальный путь исходного проекта для Публикатора без адресов remote и учётных данных.
|
||||
# A clean application built with a modified templates checkout is dirty too.
|
||||
known = source["commit"] is not None and dependency["commit"] is not None
|
||||
dirty = bool(source["dirty"] or dependency["dirty"])
|
||||
@@ -86,10 +97,11 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
|
||||
stamp = stamp.replace(microsecond=0)
|
||||
if not 2000 <= stamp.year <= 2199:
|
||||
raise ValueError("build year outside firmware-info v1 range")
|
||||
metadata = {"schema": 1, "product": settings["product"], "target": settings["target"],
|
||||
metadata = {"schema": 1, "project": settings.get("project") or settings["product"], "product": settings["product"], "target": settings["target"], # Необязательное название проекта переносится из конфигурации; старые проекты используют свой код изделия.
|
||||
"version": version, "build_id": build_id, "built_at_utc": stamp.isoformat(),
|
||||
"source": source, "templates": dependency,
|
||||
"identity_contract": 1, "identity_register_count": 12}
|
||||
if "flashing" in settings: metadata["flashing"] = flashing # Переносит все CAN/RS-профили в prebuild, manifest, архив и комплект Публикатора без изменения бинарного протокола паспорта.
|
||||
constants = dict(zip(("FIRMWARE_VERSION_MAJOR", "FIRMWARE_VERSION_MINOR", "FIRMWARE_VERSION_PATCH"), version))
|
||||
constants.update(dict(zip(("FI_YEAR", "FI_MONTH", "FI_DAY", "FI_HOUR", "FI_MINUTE", "FI_SECOND"),
|
||||
(stamp.year, stamp.month, stamp.day, stamp.hour, stamp.minute, stamp.second))))
|
||||
@@ -152,6 +164,19 @@ def build_folder_name(metadata: dict) -> str:
|
||||
return identity + "_" + stamp.strftime("%Y-%m-%d_%H-%M-%S_UTC+3")
|
||||
|
||||
|
||||
def copy_tree(source: Path, destination: Path, copy_function=shutil.copy2): # Объединяет архив с существующим каталогом без dirs_exist_ok, отсутствующего в Python 3.7.
|
||||
source, destination = Path(source), Path(destination) # Принимает пути обоих типов из общего экспорта и проверенной C2000-сборки.
|
||||
destination.mkdir(parents=True, exist_ok=True) # Повторный экспорт сохраняет соседние файлы, но не принимает обычный файл вместо папки.
|
||||
for entry in source.iterdir(): # Копирует только содержимое конкретного проверенного архива.
|
||||
target = destination / entry.name # Сохраняет исходные имена и вложенную структуру комплекта.
|
||||
if entry.is_dir(): # Вложенные каталоги требуют того же объединения при повторном выпуске.
|
||||
copy_tree(entry, target, copy_function) # Передаёт атомарное копирование C2000 на все уровни архива.
|
||||
else: # Каждый файл обрабатывается выбранным способом сохранения.
|
||||
copy_function(entry, target) # По умолчанию сохраняет метаданные copy2, а C2000 использует atomic_copy.
|
||||
shutil.copystat(source, destination) # Восстанавливает время и атрибуты каталога после записи дочерних файлов.
|
||||
return str(destination) # Сохраняет возвращаемый тип стандартного shutil.copytree.
|
||||
|
||||
|
||||
def export_build(folder: Path, export: Path, image_name: str) -> Path:
|
||||
"""Copy the current complete build folder, without scanning old archives."""
|
||||
folder, export = folder.resolve(), export.resolve()
|
||||
@@ -161,7 +186,7 @@ def export_build(folder: Path, export: Path, image_name: str) -> Path:
|
||||
verify(folder / image_name, folder / (image_name + ".manifest.json"))
|
||||
export.mkdir(parents=True, exist_ok=True)
|
||||
if destination != folder:
|
||||
shutil.copytree(folder, destination, dirs_exist_ok=True)
|
||||
copy_tree(folder, destination) # Повторное копирование архива совместимо с Python 3.7 без потери соседних выпусков.
|
||||
verify(destination / image_name, destination / (image_name + ".manifest.json"))
|
||||
print(f"[firmware-build] Exported folder: {destination}")
|
||||
return destination
|
||||
|
||||
@@ -10,6 +10,7 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
|
||||
set(SETPROTOCOL_V2_SOURCES
|
||||
src/c2000_sci.c # Общий автомат SCI Boot F28335 входит во все host и MCU цели.
|
||||
src/firmware_image.c
|
||||
src/altera_logic.c
|
||||
src/altera_stream.c
|
||||
@@ -28,6 +29,7 @@ set(SETPROTOCOL_V2_SOURCES
|
||||
|
||||
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
|
||||
set(SETPROTOCOL_LEGACY_SOURCES
|
||||
src/uksvep_can.c # Общий профиль STM32 UKSVEP: маршруты, payload и карта дискретов.
|
||||
src/balsam_can.c
|
||||
src/set_crc.c
|
||||
src/periph28335.c
|
||||
@@ -46,6 +48,7 @@ set(SETPROTOCOL_LEGACY_SOURCES
|
||||
)
|
||||
|
||||
set(SETPROTOCOL_SOURCES
|
||||
src/set_modbus_gateway.c # Единый кодек USB RTU, Ethernet TCP и таблицы маршрутов используется платой и Qt-панелью.
|
||||
src/deif_xdi.c # Общее C99-ядро DEIF для всех платформ.
|
||||
src/set_invertor_gas.c # Подключает две проекции одной карты STM Invertor: GAS 0x3 и масочный GAS SLOT 0x4.
|
||||
src/set_deif_gauge.c
|
||||
@@ -99,7 +102,13 @@ endif()
|
||||
# JNI/CMSIS-порты подключаются целевым проектом и в host CMake не входят.
|
||||
option(SETP_BUILD_TESTS "Build host tests" ON)
|
||||
if(SETP_BUILD_TESTS)
|
||||
add_executable(test_modbus_gateway tests/test_modbus_gateway.c) # Эталонные ADU защищают маршрутизацию, CRC и настройки от расхождений между портами.
|
||||
target_link_libraries(test_modbus_gateway PRIVATE setprotocol_static) # Проверяет тот же исходник, который выполняется на Zynq.
|
||||
add_test(NAME modbus_gateway COMMAND test_modbus_gateway) # Делает проверку шлюза частью стандартного CTest.
|
||||
enable_testing()
|
||||
add_executable(test_c2000_sci tests/test_c2000_sci.c) # Эталонные байты ROM и TI Kernel A проверяются без устройства.
|
||||
target_link_libraries(test_c2000_sci PRIVATE setprotocol_static) # Проверяет тот же C99-модуль, что использует ctypes.
|
||||
add_test(NAME c2000_sci COMMAND test_c2000_sci) # Регистрирует контракт в обычном CTest.
|
||||
add_executable(test_deif_xdi tests/test_deif_xdi.c) # Прямые проверки C-контракта DEIF.
|
||||
target_link_libraries(test_deif_xdi PRIVATE setprotocol_static) # Используется единственное общее ядро.
|
||||
add_test(NAME deif_xdi COMMAND test_deif_xdi "${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/deif-xdi-v1.tsv") # Векторы общие с JNI и Python.
|
||||
|
||||
@@ -131,3 +131,10 @@ FFI-клиенты подключают `setprotocol_abi.h`. Имена функ
|
||||
## GAS: регистры 0x3 и слоты 0x4
|
||||
|
||||
[Адресация GAS](docs/GAS_SLOT.md): `0x3` — отдельные 16-битные регистры `0…65535`, запись 1…4 слов. `0x4` — слот с Word0-маской и тремя значениями; PERIPH сохраняет 200 слов в 67 слотах `0xMM00…0xMM42`. Формат определяется MsgType, без настройки профиля.
|
||||
|
||||
## УКСВЭП STM32F103RC
|
||||
|
||||
Общий профиль `uksvep_can.h/.c` предоставляет карту дискретов и параметров,
|
||||
маршруты проекта «Ледокол», C-кодирование регистрового CAN и расчёт состояний
|
||||
для эмулятора SETGUI. Контракт ABI, единицы и проверки:
|
||||
[UKSVEP_CAN.md](docs/UKSVEP_CAN.md).
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# UKSVEP STM32F103RC: регистровый CAN
|
||||
|
||||
Профиль относится к `UKSVEP_ICE_22220.6`: `Core/Src/can.c`,
|
||||
`Core/Inc/package.h`, `Core/Src/message.c`, `Core/Src/lampa.c` и `main.h`.
|
||||
Он подключён в SETGUI как отдельный эмулятор проекта «Ледокол».
|
||||
|
||||
Classic CAN, extended 29-битный ID, DLC=8, 250000 бит/с.
|
||||
DeviceID 0…15 задают J0…J3; `Mode=DeviceID+1`.
|
||||
Данные ВЭП: `0x001CE000 | DeviceID`; команды ВЭП:
|
||||
`0x001CE020 | DeviceID`. Это регистровое приложение, а не ProtoCAN Boot Type5.
|
||||
Broadcast из старого приложения не выдаётся этим клиентом за адресный ответ.
|
||||
|
||||
Payload общий с историческим `balsam_can`: word2, word3,
|
||||
адрес/маска, word1, по два байта big endian. В заголовке биты15…13 задают
|
||||
присутствие слов, биты12…0 — начальный адрес. `uksvep_can` ограничивает
|
||||
банк R0…R127; отсутствующие слова не должны обнулять предыдущее состояние.
|
||||
Маршрутизация других Balsam-плат остаётся в их собственном профиле.
|
||||
|
||||
| Регистры | Назначение |
|
||||
|---|---|
|
||||
| 0 / 1 / 2 | Входы / неисправности / аварии |
|
||||
| 8 / 9 | Маски неисправностей / аварий |
|
||||
| 23 | Перемычки; младший полубайт соответствует DeviceID |
|
||||
| 48 | Sleep_time, секунды |
|
||||
| 100 | Brightness, 0…10 тактов ШИМ |
|
||||
| 101 / 102 | Cancount, быстрый / медленный период по 10 мс |
|
||||
| 125 / 126 / 127 | Версия протокола / Mode / команды |
|
||||
|
||||
Назначения входов, GPIO, команд и значения по умолчанию доступны через
|
||||
`uksvep_field_*`, `uksvep_input_name`, `uksvep_command_name`.
|
||||
`uksvep_evaluate_inputs` моделирует ReadEnteres: R0.7 никогда не является
|
||||
неисправностью/аварией; R0.8 всегда является неисправностью и становится
|
||||
аварией по Error_mask.8; R0.9 всегда является неисправностью и аварией.
|
||||
Вход R0.9/PB12 читается без инверсии, остальные назначенные
|
||||
сигналы собираются в соответствии с физической картой STM32-проекта.
|
||||
|
||||
## Слои
|
||||
|
||||
`SETGUI → protocan/uksvep.py (ctypes) → uksvep_can.c → общий payload balsam_can.c`.
|
||||
Ни GPIO, ни Qt, ни часы ОС общему C-профилю не нужны; динамическая память
|
||||
не используется. Вычисления и wire не дублируются в Python.
|
||||
|
||||
Все выходные буферы принадлежат caller и имеют явную capacity:
|
||||
|
||||
- `uksvep_can_encode`: минимум8 байтов, возвращает8 либо0;
|
||||
- `uksvep_can_decode`: минимум8 слов uint16 — направление, количество,
|
||||
затем максимум3 пары адрес/значение; возвращает число пар,0 для чужого ID,
|
||||
−1 для неверных аргументов/своего повреждённого кадра;
|
||||
- `uksvep_field_info`: минимум4 слова uint16 — адрес, максимум, default, flags;
|
||||
- `uksvep_evaluate_inputs`: минимум2 слова uint16 — неисправности и аварии.
|
||||
|
||||
Флаги полей: PARAM=1, COMMAND=2, STATE=4. GUI использует их для выбора
|
||||
допустимых редакторов роли. Имена возвращаются постоянными строками UTF-8;
|
||||
владение памятью клиенту не передаётся. Для применения RX клиент дополнительно
|
||||
отбрасывает STD/RTR и собственное направление/эхо. Физические уровни
|
||||
LED0…LED3 и PVT по текущему регистровому CAN не передаются; интерфейс не
|
||||
должен представлять расчётные лампы как измерение этих GPIO.
|
||||
|
||||
## Проверки
|
||||
|
||||
`test_uksvep_can` фиксирует независимые wire-эталоны, все16 адресов,
|
||||
малый выходной буфер, границы R127, sparse101, неверный DLC, чужие профили,
|
||||
особые входы и регрессию прежнего decoder Бальзам. Python-тесты используют
|
||||
ту же DLL, GUI-тесты подменяют только аппаратный драйвер.
|
||||
@@ -43,6 +43,9 @@ typedef struct {
|
||||
uint16_t values[BALSAM_CAN_REGISTER_COUNT];
|
||||
} balsam_can_frame_t;
|
||||
|
||||
size_t balsam_can_encode_payload(uint16_t address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity); /* Общий payload word2/word3/адрес/word1 для исторических регистровых CAN-профилей. */
|
||||
int balsam_can_decode_payload(const uint8_t *data, size_t size, balsam_can_frame_t *output); /* Разбирает только три слова и маску, оставляя маршрутизацию конкретному профилю. */
|
||||
|
||||
/** Return non-zero for the command/data/terminal IDs used by CAN_Bal_2812. */
|
||||
int balsam_can_is_id(uint32_t can_id);
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef SET_C2000_SCI_H /* Один публичный контракт SCI Boot обслуживает GUI и аппаратные тесты. */
|
||||
#define SET_C2000_SCI_H /* Защищает объявления от повторного включения. */
|
||||
#include "pcan_abi.h" /* Получает стабильные типы, size_t и экспорт символов общей библиотеки. */
|
||||
#ifdef __cplusplus /* C++-потребители используют тот же ABI без изменения имён функций. */
|
||||
extern "C" { /* Открывает C-связность публичных функций. */
|
||||
#endif /* Завершает условное начало C-связности. */
|
||||
PCAN_ABI_API int c2000_sci_decode(const uint8_t *source, size_t size, uint8_t *output, size_t capacity, size_t *written); /* Принимает бинарный SCI8 либо ASCII SCI8 TI со STX/ETX; не принимает Intel HEX. */
|
||||
PCAN_ABI_API int c2000_sci_validate(const uint8_t *data, size_t size, int flash, uint32_t *entry, uint16_t *sectors); /* Проверяет таблицу, адреса слов C28x, перекрытия и присутствие точки входа до передачи. */
|
||||
PCAN_ABI_API size_t c2000_sci_session_size(void); /* Возвращает размер хранилища, принадлежащего вызывающему приложению. */
|
||||
PCAN_ABI_API int c2000_sci_begin(void *storage, size_t size, const uint8_t *kernel, size_t kernel_size, const uint8_t *image, size_t image_size); /* Сохраняет указатели на неизменяемые буферы и проверяет оба образа до autobaud. */
|
||||
PCAN_ABI_API int c2000_sci_next(void *storage, uint8_t *tx, size_t capacity, size_t *tx_size, size_t *rx_size, uint32_t *delay_ms); /* Возвращает 1 для обмена, 2 для паузы, 0 после терминатора и отрицательное число при ошибке состояния. */
|
||||
PCAN_ABI_API int c2000_sci_accept(void *storage, const uint8_t *rx, size_t size); /* Сверяет эхо ROM либо checksum TI Kernel A, затем разрешает следующий шаг. */
|
||||
PCAN_ABI_API unsigned c2000_sci_progress(const void *storage); /* Возвращает завершённую долю передачи 0..100 без доступа к системным часам. */
|
||||
PCAN_ABI_API size_t c2000_sci_ulitco_enter(uint8_t *output, size_t capacity); /* Создаёт штатную команду Monitor: запись единицы в параметр FLASH_NUM=93. */
|
||||
#ifdef __cplusplus /* Закрывает C-связность только в C++-единицах трансляции. */
|
||||
} /* Завершает блок публичного C ABI. */
|
||||
#endif /* Завершает условную C++-обвязку. */
|
||||
#endif /* Завершает публичный интерфейс SCI Boot. */
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef SET_MODBUS_GATEWAY_H /* Защищает единый контракт шлюза от повторного включения. */
|
||||
#define SET_MODBUS_GATEWAY_H /* Кодек используется платой, Qt-портом и независимыми тестами. */
|
||||
#include "pcan_abi.h" /* Предоставляет экспорт C ABI, size_t и целочисленные типы фиксированной ширины. */
|
||||
#ifdef __cplusplus /* Позволяет подключать ядро из C++ без изменения имён экспортируемых функций. */
|
||||
extern "C" { /* Все точки входа сохраняют C ABI для ctypes и прошивок. */
|
||||
#endif /* Завершает условие C++-линковки. */
|
||||
#define SMG_ADU 260u /* Максимум TCP: MBAP 7 байт и PDU 253 байта по спецификации Modbus. */
|
||||
#define SMG_ROUTES 20u /* Двадцать записей по 10 байт помещаются вместе с настройками в один PDU. */
|
||||
#define SMG_CONTROL_UNIT 248u /* Зарезервированный адрес управления исключает конфликт с RTU slave 1…247. */
|
||||
typedef struct { uint16_t transaction, length; uint8_t unit, pdu[253]; } smg_request; /* Поля хранятся в native-порядке; структура никогда не передаётся по проводу. */
|
||||
typedef struct { uint8_t source, target, transport, interface_id; uint32_t ipv4; uint16_t port; } smg_route; /* transport: 0 RS485, 1 TCP; interface_id: 0 маршрут Linux, 1 eth0, 2 eth1. */
|
||||
typedef struct { uint32_t baud; uint16_t timeout_ms; uint8_t parity, stop_bits, default_serial, count; smg_route routes[SMG_ROUTES]; } smg_config; /* parity: 0 N, 1 E, 2 O; адреса IPv4 записаны как числовое значение big-endian. */
|
||||
PCAN_ABI_API int smg_tcp_size(const uint8_t *, size_t); /* Возвращает полный размер ADU, 0 до получения MBAP либо -1 при неверном заголовке. */
|
||||
PCAN_ABI_API int smg_rtu_size(const uint8_t *, size_t); /* Вычисляет размер запроса поддерживаемой функции без зависимости от границ USB-пакетов. */
|
||||
PCAN_ABI_API int smg_rtu_response_size(const uint8_t *, size_t); /* Вычисляет размер ответа, исключения либо конфигурации без повторения смещений в Qt-порте. */
|
||||
PCAN_ABI_API int smg_decode_tcp(const uint8_t *, size_t, smg_request *); /* Декодирует ровно один TCP-запрос без семантического применения команд. */
|
||||
PCAN_ABI_API int smg_decode_rtu(const uint8_t *, size_t, smg_request *); /* Проверяет CRC до извлечения Unit ID и PDU; CRC реализована в set_crc.c. */
|
||||
PCAN_ABI_API unsigned smg_check(const smg_request *); /* Проверяет FC01/02/03/04/05/06/15/16, количества и весь диапазон до записи на внешнюю шину. */
|
||||
PCAN_ABI_API size_t smg_tcp(const smg_request *, uint8_t, const uint8_t *, size_t, uint8_t *, size_t); /* Формирует TCP ADU с исходным Transaction ID и явно выбранным Unit ID. */
|
||||
PCAN_ABI_API size_t smg_rtu(uint8_t, const uint8_t *, size_t, uint8_t *, size_t); /* Добавляет общий CRC16 к PDU; младший байт CRC передаётся первым. */
|
||||
PCAN_ABI_API size_t smg_accept_tcp(const smg_request *, uint8_t, const uint8_t *, size_t, uint8_t *, size_t); /* Проверяет корреляцию, длину и echo ответа до передачи PDU клиенту. */
|
||||
PCAN_ABI_API size_t smg_accept_rtu(const smg_request *, uint8_t, const uint8_t *, size_t, uint8_t *, size_t); /* Проверяет CRC, slave, функцию и форму ответа RTU, включая исключения. */
|
||||
PCAN_ABI_API size_t smg_exception(const smg_request *, unsigned, uint8_t *); /* Формирует двухбайтный PDU исключения; 0A означает отсутствие пути, 0B отсутствие ответа. */
|
||||
PCAN_ABI_API void smg_config_default(smg_config *); /* Создаёт остановленную маршрутизацию с 115200 8N1 и тайм-аутом 1000 мс. */
|
||||
PCAN_ABI_API int smg_config_valid(const smg_config *); /* Проверяет уникальность входных адресов, транспорт и допустимые значения всех маршрутов. */
|
||||
PCAN_ABI_API size_t smg_config_pack(unsigned, const smg_config *, uint8_t *, size_t); /* Частная FC65: операция 1 читает, 2 применяет, 3 возвращает полную конфигурацию. */
|
||||
PCAN_ABI_API int smg_config_unpack(const uint8_t *, size_t, unsigned *, smg_config *); /* Разбирает ту же FC65 на ARM и ПК; произвольные shell-команды отсутствуют. */
|
||||
#ifdef __cplusplus /* Закрывает C ABI при включении из C++. */
|
||||
} /* Завершает внешнюю C-линковку. */
|
||||
#endif /* Завершает условие C++-линковки. */
|
||||
#endif /* Завершает публичный контракт шлюза. */
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef UKSVEP_CAN_H /* Защищает общий профиль ВЭП от повторного объявления. */
|
||||
#define UKSVEP_CAN_H /* Карта соответствует STM32-проекту UKSVEP_ICE_22220.6, а не ВЭП Бальзам. */
|
||||
#include "pcan_abi.h" /* Даёт экспортируемый C ABI для Python и других клиентов templates. */
|
||||
#ifdef __cplusplus /* Позволяет использовать тот же профиль из C++ и JNI. */
|
||||
extern "C" { /* Сохраняет стабильные имена экспортов без C++ mangling. */
|
||||
#endif /* Завершает выбор языка связывания. */
|
||||
#define UKSVEP_REGISTER_COUNT 128U /* message.h выделяет Modbus_LEN=0x80 слов uint16. */
|
||||
#define UKSVEP_FIELD_PARAM 1U /* Поле допускает запись параметра в реальный ВЭП. */
|
||||
#define UKSVEP_FIELD_COMMAND 2U /* Поле R127 содержит битовые команды приложения. */
|
||||
#define UKSVEP_FIELD_STATE 4U /* Поле состояния доступно для редактирования только в роли эмулируемого ВЭП. */
|
||||
PCAN_ABI_API uint32_t uksvep_can_id(unsigned device, unsigned from_device); /* Адрес 0…15: данные 0x1CE000+ID, команды 0x1CE020+ID; UINT32_MAX означает отказ. */
|
||||
PCAN_ABI_API size_t uksvep_can_encode(unsigned device, unsigned from_device, unsigned address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity); /* Строит DLC=8 для одного–трёх слов; capacity задаётся в байтах, требуется минимум 8. */
|
||||
PCAN_ABI_API int uksvep_can_decode(unsigned device, uint32_t id, const uint8_t *data, size_t size, uint16_t *output, size_t capacity); /* output: направление, число, три пары адрес/значение; capacity в словах uint16, минимум 8; 0=чужой ID, -1=ошибка. */
|
||||
PCAN_ABI_API unsigned uksvep_field_count(void); /* Число подтверждённых package.h полей, без выдуманных регистров. */
|
||||
PCAN_ABI_API int uksvep_field_info(unsigned index, unsigned device, uint16_t *output, size_t capacity); /* output[4]: адрес, максимум, начальное значение, флаги роли; capacity задаётся в словах uint16. */
|
||||
PCAN_ABI_API const char *uksvep_field_name(unsigned index); /* Возвращает постоянное имя UTF-8 без передачи владения памятью. */
|
||||
PCAN_ABI_API const char *uksvep_input_name(unsigned bit); /* Возвращает назначение и физический GPIO каждого из четырнадцати входов. */
|
||||
PCAN_ABI_API const char *uksvep_command_name(unsigned bit); /* Описывает только команды package.h; пустая строка означает резерв. */
|
||||
PCAN_ABI_API void uksvep_evaluate_inputs(uint16_t inputs, uint16_t alarm_mask, uint16_t error_mask, uint16_t *output, size_t capacity); /* Вычисляет R1/R2 по ReadEnteres; capacity задаётся в словах uint16, требуется минимум 2. */
|
||||
#ifdef __cplusplus /* Закрывает внешнее C-связывание для C++. */
|
||||
} /* Все клиенты вызывают одну реализацию протокола. */
|
||||
#endif /* Завершает совместимость с C++. */
|
||||
#endif /* Завершает публичный профиль UKSVEP. */
|
||||
@@ -48,11 +48,44 @@ int balsam_can_is_id(uint32_t can_id)
|
||||
&& relative < BALSAM_CAN_DATA_OFFSET + BALSAM_CAN_NODE_COUNT);
|
||||
}
|
||||
|
||||
size_t balsam_can_encode_payload(uint16_t address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity) /* Общий регистровый payload не зависит от выбранной платы. */
|
||||
{ /* Формат совпадает у Бальзам и UKSVEP, но адреса CAN определяют разные профили. */
|
||||
size_t index; /* Перебирает только присутствующие полезные слова. */
|
||||
uint16_t header; /* Хранит трёхбитную маску и 13-битный адрес регистра. */
|
||||
if (capacity < 8U || address > 0x1FFFU || values == NULL || output == NULL || count == 0U || count > 3U) { /* Проверяет геометрию payload до изменения выходных байтов. */
|
||||
return 0U; /* Отказ не создаёт частично готового кадра. */
|
||||
} /* Конкретный профиль дополнительно ограничивает размер своего банка. */
|
||||
memset(output, 0, 8U); /* Отсутствующие слова получают нулевое заполнение без бита присутствия. */
|
||||
header = (uint16_t)(address | (((7U << (3U - count)) & 7U) << 13U)); /* Одно/два/три слова дают маски 100/110/111 в битах 15…13. */
|
||||
output[4] = (uint8_t)(header >> 8U); /* Старший байт адреса/маски передаётся первым. */
|
||||
output[5] = (uint8_t)header; /* Младший байт завершает big-endian заголовок. */
|
||||
for (index = 0U; index < count; ++index) { /* eCAN передаёт word2 и word3 перед word1. */
|
||||
size_t offset = index == 0U ? 6U : (index - 1U) * 2U; /* Первое слово находится в байтах 6/7, следующие — в 0/1 и 2/3. */
|
||||
output[offset] = (uint8_t)(values[index] >> 8U); /* Записывает старшие восемь бит без зависимости от endian хоста. */
|
||||
output[offset + 1U] = (uint8_t)values[index]; /* Записывает младшие восемь бит того же слова. */
|
||||
} /* Порядок слов реализован в одном C-модуле для всех потребителей. */
|
||||
return 8U; /* Исторический регистровый кадр всегда имеет DLC=8. */
|
||||
} /* Завершает общий кодировщик трёх регистров. */
|
||||
|
||||
int balsam_can_decode_payload(const uint8_t *data, size_t size, balsam_can_frame_t *output) /* Отделяет общий payload от профиля CAN ID. */
|
||||
{ /* Маршрут и номер устройства заполняет вызывающий профиль. */
|
||||
uint16_t header; /* Получает маску присутствия и начальный адрес слова. */
|
||||
if (data == NULL || output == NULL || size != BALSAM_CAN_DLC) { /* Не читает неполные либо отсутствующие восемь байтов. */
|
||||
return 0; /* Отказ не изменяет ранее принятые значения клиента. */
|
||||
} /* Сохраняет исходный порядок декодирования Balsam без второго codec. */
|
||||
header = get_be16(&data[4]); /* Адрес находится в третьем 16-битном слове на проводе. */
|
||||
output->present_mask = (uint8_t)((header >> 13U) & 0x07U); /* Три старших бита разрешают отдельные регистры. */
|
||||
output->start_address = (uint16_t)(header & 0x1FFFU); /* Остальные 13 бит задают начальный адрес. */
|
||||
output->values[0] = get_be16(&data[6]); /* Первое логическое слово передаётся последним. */
|
||||
output->values[1] = get_be16(&data[0]); /* Второе логическое слово передаётся первым. */
|
||||
output->values[2] = get_be16(&data[2]); /* Третье логическое слово передаётся вторым. */
|
||||
return 1; /* Все три значения разобраны; применять можно только присутствующие. */
|
||||
} /* Завершает общий разбор payload без назначения платы. */
|
||||
|
||||
int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||
balsam_can_frame_t *output)
|
||||
{
|
||||
uint32_t relative;
|
||||
uint16_t header;
|
||||
if ((data == NULL) || (output == NULL)) return -1;
|
||||
can_id &= 0x1FFFFFFFUL;
|
||||
if (!balsam_can_is_id(can_id)) return 0;
|
||||
@@ -67,14 +100,7 @@ int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||
output->direction = can_id == 0x00745019UL
|
||||
? BALSAM_CAN_FROM_NODE : BALSAM_CAN_TO_NODE;
|
||||
}
|
||||
/* eCAN sends MDL before MDH: value2, value3, address/mask, value1. */
|
||||
header = get_be16(&data[4]);
|
||||
output->present_mask = (uint8_t)((header >> 13) & 0x07U);
|
||||
output->start_address = (uint16_t)(header & 0x1FFFU);
|
||||
output->values[0] = get_be16(&data[6]);
|
||||
output->values[1] = get_be16(&data[0]);
|
||||
output->values[2] = get_be16(&data[2]);
|
||||
return 1;
|
||||
return balsam_can_decode_payload(data, size, output); /* Переиспользует тот же payload decoder, сохраняя прежние проверки и маршрутизацию Бальзам. */
|
||||
}
|
||||
|
||||
const char *balsam_can_device_name(uint8_t device)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#include "c2000_sci.h" /* Реализует независимый от ОС SCI Boot и TI Flash Kernel A. */
|
||||
#include <string.h> /* Копирует уже проверенные байты без платформенных преобразований. */
|
||||
#define SCI_CHUNK_WORDS 1024U /* Буфер TI f28335_flash_kernel имеет PROG_BUFFER_LENGTH=0x400 слов. */
|
||||
typedef struct { /* Контекст не владеет образами и не выделяет динамическую память. */
|
||||
const uint8_t *kernel, *image; /* Сохраняет неизменяемые буферы на весь срок сеанса. */
|
||||
size_t kernel_size, image_size, kernel_at, image_at, pending_bytes; /* Позиции и длины измеряются физическими октетами файлов. */
|
||||
unsigned phase, wait, remaining, block_words, extra_zero; /* Отделяет autobaud, байтовое эхо ROM и порционные подтверждения Flash. */
|
||||
uint16_t expected; /* Хранит ожидаемое эхо либо сумму октетов по контракту TI Kernel A. */
|
||||
} sci_session_t; /* Размер структуры скрыт за функцией session_size для стабильного ABI. */
|
||||
static uint16_t word(const uint8_t *p) { return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); } /* SCI8 передаёт каждый 16-битный элемент младшей октетой вперёд. */
|
||||
static uint32_t address(const uint8_t *p) { return ((uint32_t)word(p) << 16) | word(p + 2); } /* Адрес состоит из старшего слова C28x, затем младшего слова. */
|
||||
static int white(uint8_t c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } /* ASCII SCI8 допускает стандартные разделители между октетами. */
|
||||
static int digit(uint8_t c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'A' && c <= 'F' ? c - 'A' + 10 : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; } /* Декодирует только шестнадцатеричные цифры TI ASCII. */
|
||||
int c2000_sci_decode(const uint8_t *source, size_t size, uint8_t *output, size_t capacity, size_t *written) { /* Приводит оба поддержанных формата к одному бинарному буферу SCI8. */
|
||||
size_t at = 0, count = 0; int high, low; /* Хранит позиции отдельно от результата до полного завершения разбора. */
|
||||
if (written) *written = 0; /* Ошибка никогда не оставляет длину частично принятого файла. */
|
||||
if (!source || !output || !written || size < 2) return -1; /* Проверяет указатели и минимальный ключ до чтения данных. */
|
||||
if (source[0] == 0xAA && source[1] == 0x08) { if (size > capacity) return -2; memmove(output, source, size); *written = size; return 0; } /* Бинарный SCI8 сохраняется побайтно. */
|
||||
while (at < size && white(source[at])) ++at; /* Пропускает пустые строки перед STX. */
|
||||
if (at == size || source[at++] != 2) return -3; /* ASCII TI начинается управляющим STX, а не Intel HEX-двоеточием. */
|
||||
for (;;) { /* Принимает только завершённый ETX поток двухзначных октетов. */
|
||||
while (at < size && white(source[at])) ++at; /* Разделители не входят в передаваемые байты. */
|
||||
if (at < size && source[at] == 3) { ++at; break; } /* ETX завершает ASCII-контейнер, не передаваясь MCU. */
|
||||
if (at + 2 > size || (high = digit(source[at])) < 0 || (low = digit(source[at + 1])) < 0) return -3; /* Усечённая либо нечисловая пара останавливает разбор. */
|
||||
if (count == capacity) return -2; /* Буфер вызывающего не может быть переполнен большим файлом. */
|
||||
output[count++] = (uint8_t)((high << 4) | low); at += 2; /* Записывает один проверенный октет. */
|
||||
if (at < size && !white(source[at]) && source[at] != 3) return -3; /* Склеенные пары и слова шире октета не допускаются. */
|
||||
} /* После ETX разрешены только пустые разделители. */
|
||||
while (at < size && white(source[at])) ++at; /* Не принимает скрытые данные после контейнера. */
|
||||
if (at != size || count < 2 || output[0] != 0xAA || output[1] != 0x08) return -3; /* Проверяет конец ASCII и ключ SCI8 до публикации длины. */
|
||||
*written = count; return 0; /* Публикует только полностью разобранный бинарный результат. */
|
||||
} /* Завершает преобразование представления SCI8. */
|
||||
int c2000_sci_validate(const uint8_t *data, size_t size, int flash, uint32_t *entry, uint16_t *sectors) { /* Проверяет адреса назначения без обращения к устройству. */
|
||||
size_t at = 22, old_at, payload; uint32_t start, stop, old_start, old_stop, target; uint16_t count, mask = 0; unsigned blocks = 0, have_entry = 0; /* SCI8 имеет 22-октетный заголовок и размеры блоков в словах. */
|
||||
if (!data || size < 24 || size % 2 || data[0] != 0xAA || data[1] != 0x08) return -4; /* Ключ 0x08AA и чётный размер обязательны для C28x SCI8. */
|
||||
target = address(data + 18); /* Поле entry находится после ключа и восьми резервных слов ROM. */
|
||||
while (at + 2 <= size) { /* Каждый блок начинается полным 16-битным размером. */
|
||||
count = word(data + at); at += 2; /* Нулевой размер обозначает конец адресной таблицы. */
|
||||
if (!count) { if (at != size || !blocks || !have_entry) return -5; if (entry) *entry = target; if (sectors) *sectors = mask; return 0; } /* Успех требует точного конца и реально загруженной точки входа. */
|
||||
payload = (size_t)count * 2; /* Один адресуемый элемент C28x занимает два октета файла. */
|
||||
if (size - at < 4 || payload > size - at - 4 || ++blocks > 256) return -5; /* Ограничивает полную секцию и число диапазонов до обращения к данным. */
|
||||
start = address(data + at); if (start > UINT32_MAX - count) return -6; stop = start + count; /* Проверяет переполнение словного диапазона. */
|
||||
if (flash) { /* Приложение SCI пишет внутреннюю Flash F28335, включая обычный аппаратный codestart. */
|
||||
if (start < 0x300000UL || stop > 0x33FFF8UL || (start < 0x33FFF6UL && stop > 0x33FF80UL)) return -6; /* Запрещает RAM, OTP, CSM reserved и пароли; разрешает только два слова codestart 33FFF6–33FFF7. */
|
||||
{ uint32_t sector; for (sector = (0x33FFFFUL - (stop - 1)) / 0x8000UL; sector <= (0x33FFFFUL - start) / 0x8000UL; ++sector) mask = (uint16_t)(mask | (1U << sector)); } /* Включает каждый пересечённый сектор по 0x8000 слов; TI нумерует A младшим битом. */
|
||||
} else if (!((start < 0x800UL && stop <= 0x800UL) || (start >= 0x8000UL && stop <= 0x10000UL))) return -6; /* RAM-ядро ограничено M0/M1 и L0–L7, не затрагивая периферийные регистры. */
|
||||
old_at = 22; /* Повторно обходит предыдущие заголовки без heap и без таблицы платформенных структур. */
|
||||
while (old_at + 2 < at) { /* Все предшествующие секции уже имеют проверенные размеры. */
|
||||
uint16_t old_count = word(data + old_at); old_at += 2; /* Получает длину очередного ранее принятого блока. */
|
||||
if (old_at == at) break; old_start = address(data + old_at); old_stop = old_start + old_count; /* Текущая секция не сравнивается сама с собой. */
|
||||
if (start < old_stop && old_start < stop) return -7; old_at += 4 + (size_t)old_count * 2; /* Любое наложение запрещается независимо от совпадения байтов. */
|
||||
} /* Завершает проверку непересечения нового блока. */
|
||||
if (start <= target && target < stop) have_entry = 1; at += 4 + payload; /* Учитывает entry только внутри реально присутствующего кода. */
|
||||
} /* Отсутствующий терминатор не допускается даже при правильных секциях. */
|
||||
return -5; /* Усечённая таблица запрещена до autobaud и стирания MCU. */
|
||||
} /* Завершает проверку бинарного SCI8. */
|
||||
size_t c2000_sci_session_size(void) { return sizeof(sci_session_t); } /* Вызывающий выделяет выровненный буфер достаточного размера. */
|
||||
int c2000_sci_begin(void *storage, size_t size, const uint8_t *kernel, size_t kernel_size, const uint8_t *image, size_t image_size) { /* Начинает сеанс только после проверки обоих буферов. */
|
||||
sci_session_t *s = (sci_session_t *)storage; int status; /* Контекст размещён в памяти вызывающего порта. */
|
||||
if (!s || size < sizeof(*s)) return -1; memset(s, 0, sizeof(*s)); /* Не оставляет состояние предыдущей передачи. */
|
||||
status = c2000_sci_validate(kernel, kernel_size, 0, NULL, NULL); if (status) return status; /* В RAM можно передать только корректное ядро. */
|
||||
status = c2000_sci_validate(image, image_size, 1, NULL, NULL); if (status) return status; /* Ошибочный Flash-образ не должен даже запускать загрузку ядра. */
|
||||
s->kernel = kernel; s->kernel_size = kernel_size; s->image = image; s->image_size = image_size; return 0; /* Срок жизни буферов остаётся обязанностью порта. */
|
||||
} /* Завершает инициализацию сеанса. */
|
||||
int c2000_sci_next(void *storage, uint8_t *tx, size_t capacity, size_t *tx_size, size_t *rx_size, uint32_t *delay_ms) { /* Планирует ровно один обмен либо обязательную паузу TI. */
|
||||
sci_session_t *s = (sci_session_t *)storage; size_t count, i; /* Длины считаются октетами, кроме remaining в словах Flash. */
|
||||
if (!s || !tx || !tx_size || !rx_size || !delay_ms || s->wait || !s->kernel) return -8; *tx_size = *rx_size = 0; *delay_ms = 0; /* Запрещает новый пакет до проверки ответа на предыдущий. */
|
||||
if (s->phase == 9) return 0; /* Нулевой терминатор уже передан; аппаратная проверка чтением не заявляется. */
|
||||
if (s->phase == 2 || s->phase == 4) { ++s->phase; *delay_ms = 3000; return 2; } /* TI f05_DownloadImage выдерживает три секунды после ядра и после его autobaud. */
|
||||
if (!capacity) return -2; /* Даже одиночный символ синхронизации должен помещаться в буфер. */
|
||||
if (s->phase == 0 || s->phase == 3) { tx[0] = 'A'; *tx_size = *rx_size = 1; s->expected = 'A'; } /* ROM и Kernel A независимо определяют скорость по эху ASCII A. */
|
||||
else if (s->phase == 1) { tx[0] = s->kernel[s->kernel_at]; *tx_size = *rx_size = 1; s->expected = tx[0]; } /* ROM подтверждает каждый октет ядра своим точным эхом. */
|
||||
else if (s->phase == 7) { *rx_size = 2; s->expected = 0; } /* TI Kernel A дополнительно возвращает нулевую сумму для пустого остатка блока, кратного 1024 словам. */
|
||||
else { /* Прикладной поток подтверждается суммой порций после успешного вызова Flash API. */
|
||||
if (s->phase == 5) { count = 22; s->image_at = 0; } /* Заголовок подтверждается после стирания выбранных ядром секторов. */
|
||||
else { /* Обрабатывает полный SCI8-блок без передачи следующего до подтверждения текущего. */
|
||||
if (!s->remaining) { s->block_words = s->remaining = word(s->image + s->image_at); s->extra_zero = s->block_words > SCI_CHUNK_WORDS && s->block_words % SCI_CHUNK_WORDS == 0; } /* Учитывает точный размер порции TI и его особый пустой остаток. */
|
||||
if (!s->remaining) { if (capacity < 2) return -2; memcpy(tx, s->image + s->image_at, 2); *tx_size = 2; s->image_at += 2; s->phase = 9; return 1; } /* Терминатор завершает программирование и передаёт управление entry приложения. */
|
||||
count = (size_t)(s->remaining > SCI_CHUNK_WORDS ? SCI_CHUNK_WORDS : s->remaining) * 2; /* Полезная часть порции не превышает RAM-буфер TI. */
|
||||
if (s->remaining == s->block_words) count += 6; /* Первая порция включает размер секции и 32-битный словный адрес. */
|
||||
} /* Получена длина заголовка либо одной порции приложения. */
|
||||
if (capacity < count) return -2; memcpy(tx, s->image + s->image_at, count); *tx_size = count; *rx_size = 2; s->expected = 0; /* Копирует готовые байты без повторной сборки таблицы в порту GUI. */
|
||||
for (i = 0; i < count; ++i) s->expected = (uint16_t)(s->expected + tx[i]); s->pending_bytes = count; /* Сумма октетов по модулю 65536 совпадает с TI SendCheckSum. */
|
||||
} /* Ответ на запланированный обмен должен быть принят до следующего шага. */
|
||||
s->wait = (unsigned)*rx_size; return 1; /* Нулевой rx_size означает отправку терминатора без недостоверного ожидания ACK. */
|
||||
} /* Завершает формирование следующего действия. */
|
||||
int c2000_sci_accept(void *storage, const uint8_t *rx, size_t size) { /* Проверяет фактические ответы ROM и Flash Kernel A. */
|
||||
sci_session_t *s = (sci_session_t *)storage; unsigned words_sent; /* Состояние алгоритма меняется только после достоверного ответа. */
|
||||
if (!s || !rx || !s->wait || size != s->wait) return -8; /* Не принимает пустое, лишнее или усечённое подтверждение. */
|
||||
if ((size == 1 ? rx[0] : word(rx)) != s->expected) return -9; s->wait = 0; /* Несовпадение эха или checksum останавливает передачу. */
|
||||
if (s->phase == 0) s->phase = 1; /* После первого autobaud начинается байтовая передача RAM-ядра. */
|
||||
else if (s->phase == 1) { if (++s->kernel_at == s->kernel_size) s->phase = 2; } /* Последний подтверждённый октет ядра запускает паузу его старта. */
|
||||
else if (s->phase == 3) s->phase = 4; /* После autobaud ядра выдерживается пауза перед стиранием Flash. */
|
||||
else if (s->phase == 5) { s->image_at = 22; s->phase = 6; } /* Подтверждённый заголовок означает завершившийся вызов erase в ядре. */
|
||||
else if (s->phase == 7) s->phase = 6; /* Убирает дополнительный ACK пустого остатка до следующей секции. */
|
||||
else { /* Порция приложения подтверждена после успешного Flash_Program. */
|
||||
words_sent = (unsigned)(s->pending_bytes - (s->remaining == s->block_words ? 6 : 0)) / 2; /* Отделяет служебный заголовок секции от записанных слов. */
|
||||
s->remaining -= words_sent; s->image_at += s->pending_bytes; if (!s->remaining && s->extra_zero) s->phase = 7; /* Продвигает поток только на подтверждённое число октетов. */
|
||||
} /* Завершает успешный переход автомата. */
|
||||
return 0; /* Порт может запросить следующее действие сеанса. */
|
||||
} /* Завершает обработку подтверждения. */
|
||||
unsigned c2000_sci_progress(const void *storage) { const sci_session_t *s = (const sci_session_t *)storage; return !s || !s->image_size ? 0 : s->phase == 9 ? 100 : (unsigned)((s->kernel_at * 20) / s->kernel_size + (s->image_at * 80) / s->image_size); } /* Передача ядра занимает 20 процентов, приложение — остальные 80 процентов шкалы. */
|
||||
size_t c2000_sci_ulitco_enter(uint8_t *output, size_t capacity) { /* Использует только подтверждённую команду Monitor UlitCo из talk_with_monitor.c и param.c. */
|
||||
static const uint8_t request[10] = {247, 213, 255, 93, 0, 0, 1, 0, 93, 1}; /* Заголовок F7/D5, MONITOR_WRITE=255, FLASH_NUM=93, значение 1; контрольная сумма 255+93+1=349 (0x015D) передаётся младшим октетом вперёд и проверяется talk_with_monitor.c. */
|
||||
if (!output || capacity < sizeof(request)) return 0; memcpy(output, request, sizeof(request)); return sizeof(request); /* Возвращает полный штатный десятиоктетный запрос без отправки на устройство. */
|
||||
} /* Завершает общий кодек перехода UlitCo в первичный загрузчик. */
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "set_modbus_gateway.h" /* Общий контракт исключает вторую реализацию форматов в Qt и Linux. */
|
||||
#include "set_crc.h" /* Использует уже существующую реализацию Modbus CRC16 из templates. */
|
||||
#include <string.h> /* Копирует только заранее проверенные ограниченные буферы. */
|
||||
static unsigned get16(const uint8_t *p) { return ((unsigned)p[0] << 8) | p[1]; } /* Пары байтов Modbus всегда передаются старшим байтом вперёд. */
|
||||
static void put16(uint8_t *p, unsigned v) { p[0] = (uint8_t)(v >> 8); p[1] = (uint8_t)v; } /* Сериализация не зависит от endian ARM или ПК. */
|
||||
int smg_tcp_size(const uint8_t *p, size_t n) { /* TCP может доставить заголовок несколькими чтениями. */
|
||||
unsigned length; if (!p) { return -1; } if (n < 6) { return 0; } length = get16(p + 4); /* Шесть байтов достаточно для Protocol ID и длины оставшегося ADU. */
|
||||
return get16(p + 2) || length < 2 || length > 254 ? -1 : (int)(length + 6); /* Protocol ID равен нулю; длина включает Unit ID и PDU до 253 байт. */
|
||||
} /* Возвращённый размер позволяет отделять склеенные TCP-запросы без потери хвоста. */
|
||||
int smg_rtu_size(const uint8_t *p, size_t n) { /* Длина определяется запросом, а не границами USB transfer. */
|
||||
if (!p) { return -1; } if (n < 2) { return 0; } /* До байта функции нельзя выбрать формат запроса. */
|
||||
if (p[1] >= 1 && p[1] <= 6) { return 8; } /* FC01…06 содержат адрес, количество либо значение и CRC. */
|
||||
if (p[1] == 15 || p[1] == 16) { return n < 7 ? 0 : p[6] > 247 ? -1 : 9 + p[6]; } /* Byte count ограничен предельным RTU ADU 256 байт. */
|
||||
if (p[0] == SMG_CONTROL_UNIT && p[1] == 65) { return n < 3 ? 0 : p[2] == 1 ? 5 : n < 14 ? 0 : p[13] > SMG_ROUTES ? -1 : 16 + 10 * p[13]; } /* FC65: после операции и заголовка следует count маршрутов и два байта CRC. */
|
||||
return -1; /* Неизвестные RTU-функции требуют границы тишины и проверяются вызывающим портом отдельно. */
|
||||
} /* Функция не читает ни одного байта за полученным префиксом. */
|
||||
int smg_rtu_response_size(const uint8_t *p, size_t n) { /* Ответ RTU имеет другой формат длины, чем запрос чтения. */
|
||||
if (!p) { return -1; } if (n < 2) { return 0; } if (p[1] & 128) { return 5; } /* Все стандартные исключения содержат slave, функцию, код и CRC. */
|
||||
if (p[1] >= 1 && p[1] <= 4) { return n < 3 ? 0 : p[2] > 250 ? -1 : 5 + p[2]; } /* Ответ чтения содержит byte count и не более 250 байт данных. */
|
||||
if (p[1] == 5 || p[1] == 6 || p[1] == 15 || p[1] == 16) { return 8; } /* Запись подтверждается фиксированным echo адреса и значения либо количества. */
|
||||
if (p[0] == SMG_CONTROL_UNIT && p[1] == 65) { return n < 14 ? 0 : p[2] != 3 || p[13] > SMG_ROUTES ? -1 : 16 + 10 * p[13]; } /* Ответ конфигурации содержит операцию 3 и целое число маршрутов. */
|
||||
return -1; /* Неизвестный ответ не подтверждает незавершённую транзакцию. */
|
||||
} /* Функция нужна потоковому порту USB и не зависит от размера USB-пакета. */
|
||||
int smg_decode_tcp(const uint8_t *p, size_t n, smg_request *r) { /* Не принимает неполные ADU и два сообщения как одно. */
|
||||
int expected = smg_tcp_size(p, n); if (!r || expected < 8 || (size_t)expected != n) { return 0; } /* Пустой PDU не является запросом Modbus. */
|
||||
memset(r, 0, sizeof(*r)); r->transaction = (uint16_t)get16(p); r->unit = p[6]; r->length = (uint16_t)(n - 7); memcpy(r->pdu, p + 7, r->length); return 1; /* Корреляция клиента отделена от адреса конечного устройства. */
|
||||
} /* Семантика функции проверяется до выбора внешнего маршрута. */
|
||||
int smg_decode_rtu(const uint8_t *p, size_t n, smg_request *r) { /* Повреждённые RTU кадры не получают ответа и не меняют состояние. */
|
||||
if (!p || !r || n < 4 || n > 256 || set_crc16_modbus(p, n)) { return 0; } /* Нулевой остаток CRC проверяет также два переданных байта контрольной суммы. */
|
||||
memset(r, 0, sizeof(*r)); r->unit = p[0]; r->length = (uint16_t)(n - 3); memcpy(r->pdu, p + 1, r->length); return 1; /* RTU не содержит Transaction ID; порт назначает его для исходящего TCP. */
|
||||
} /* Исходные байты принадлежат вызывающему и могут быть повторно использованы после возврата. */
|
||||
unsigned smg_check(const smg_request *r) { /* Ошибочный запрос отклоняется до любого внешнего I/O. */
|
||||
unsigned fn, address, quantity, bytes, maximum; if (!r || !r->length || r->length > 253) { return 3; } fn = r->pdu[0]; /* Максимальный PDU одинаков для TCP и RTU. */
|
||||
if (!r->unit) { return 3; } if (fn == 65 && r->unit == SMG_CONTROL_UNIT) { return 0; } /* Широковещательные записи запрещены; адрес 248 обслуживает только настройки шлюза. */
|
||||
if (!(fn >= 1 && fn <= 6) && fn != 15 && fn != 16) { return 1; } if (r->length < 5) { return 3; } /* Поддерживаются те же базовые функции, что в штатной Modbus-панели SETGUI. */
|
||||
address = get16(r->pdu + 1); quantity = get16(r->pdu + 3); if (fn <= 6 && r->length != 5) { return 3; } /* Дополнительный хвост не должен скрывать второй запрос. */
|
||||
if (fn == 5) { return quantity == 0 || quantity == 0xff00 ? 0 : 3; } if (fn == 6) { return 0; } /* FC05 допускает только стандартные значения OFF и ON; FC06 передаёт uint16. */
|
||||
maximum = fn <= 2 ? 2000 : fn <= 4 ? 125 : fn == 15 ? 1968 : 123; /* Ограничения количеств взяты из описаний FC01/02/03/04/15/16 Modbus V1.1b3. */
|
||||
if (!quantity || quantity > maximum) { return 3; } if (address + quantity > 65536u) { return 2; } /* Вычисление в unsigned предотвращает 16-битное переполнение диапазона адресов. */
|
||||
if (fn == 15 || fn == 16) { bytes = fn == 15 ? (quantity + 7) / 8 : quantity * 2; if (r->length != 6 + bytes || r->pdu[5] != bytes) { return 3; } } /* Проверяет весь блок записи до передачи на RS485 либо Ethernet. */
|
||||
return 0; /* Проверка не применяет значения регистров и не меняет запрос. */
|
||||
} /* Исключения возвращаются в стандартном PDU, а ошибки CRC отбрасываются раньше. */
|
||||
size_t smg_tcp(const smg_request *r, uint8_t unit, const uint8_t *pdu, size_t n, uint8_t *out, size_t cap) { /* Один builder используется для запросов и ответов TCP. */
|
||||
if (!r || !pdu || !out || !n || n > 253 || cap < n + 7) { return 0; } put16(out, r->transaction); put16(out + 2, 0); put16(out + 4, (unsigned)n + 1); out[6] = unit; memcpy(out + 7, pdu, n); return n + 7; /* Возвращает исходную корреляцию независимо от изменения slave на маршруте. */
|
||||
} /* Protocol ID всегда равен нулю, длина не включает первые шесть байтов MBAP. */
|
||||
size_t smg_rtu(uint8_t unit, const uint8_t *pdu, size_t n, uint8_t *out, size_t cap) { /* CRC вычисляется только общим кодом templates. */
|
||||
uint16_t crc; if (!pdu || !out || !n || n > 253 || cap < n + 3) { return 0; } out[0] = unit; memcpy(out + 1, pdu, n); crc = set_crc16_modbus(out, n + 1); out[n + 1] = (uint8_t)crc; out[n + 2] = (uint8_t)(crc >> 8); return n + 3; /* В отличие от регистров, CRC16 передаётся младшим байтом вперёд. */
|
||||
} /* Unit ID проверяется при выборе маршрута, чтобы адрес управления 248 также мог иметь RTU-ответ. */
|
||||
static size_t accept_pdu(const smg_request *r, const uint8_t *p, size_t n, uint8_t *out, size_t cap) { /* Не позволяет выдать ответ другого запроса за текущую транзакцию. */
|
||||
unsigned fn, count, bytes; if (!r || !p || !out || !n || n > cap || smg_check(r)) { return 0; } fn = r->pdu[0]; /* Запрос должен быть предварительно допустимым. */
|
||||
if (p[0] == (fn | 128u)) { if (n != 2 || !p[1]) { return 0; } } /* Исключение содержит ровно функцию с битом 7 и ненулевой код. */
|
||||
else if (fn == 65 && r->unit == SMG_CONTROL_UNIT) { smg_config c; unsigned op; if (!smg_config_unpack(p, n, &op, &c) || op != 3) { return 0; } } /* Настройки также проходят проверку общей версией кодека до публикации в GUI. */
|
||||
else { if (p[0] != fn) { return 0; } count = get16(r->pdu + 3); /* Только ответ исходной функции может завершить ожидание. */
|
||||
if (fn <= 4) { bytes = fn <= 2 ? (count + 7) / 8 : count * 2; if (n != bytes + 2 || p[1] != bytes) { return 0; } } /* Byte count проверяется по количеству запроса, а не только по длине принятого пакета. */
|
||||
else if (n != 5 || memcmp(p, r->pdu, 5)) { return 0; } /* FC05/06/15/16 обязаны подтвердить тот же адрес и значение либо количество. */
|
||||
} memcpy(out, p, n); return n; /* Только полностью проверенный PDU передаётся входному клиенту. */
|
||||
} /* На неверном ответе выходной буфер остаётся без изменений. */
|
||||
size_t smg_accept_tcp(const smg_request *r, uint8_t unit, const uint8_t *p, size_t n, uint8_t *out, size_t cap) { /* TCP backend сохраняет единственную исходящую транзакцию. */
|
||||
smg_request reply; if (!r || !smg_decode_tcp(p, n, &reply) || reply.transaction != r->transaction || reply.unit != unit) { return 0; } return accept_pdu(r, reply.pdu, reply.length, out, cap); /* Проверяет MBAP до проверки формы данных. */
|
||||
} /* Чужой Transaction ID не подтверждает запись и не вызывает повтор записи. */
|
||||
size_t smg_accept_rtu(const smg_request *r, uint8_t unit, const uint8_t *p, size_t n, uint8_t *out, size_t cap) { /* RTU backend проверяет целый кадр после межкадровой тишины. */
|
||||
smg_request reply; if (!r || !smg_decode_rtu(p, n, &reply) || reply.unit != unit) { return 0; } return accept_pdu(r, reply.pdu, reply.length, out, cap); /* Неверная CRC либо адрес не завершают запрос успешным ответом. */
|
||||
} /* Исключение целевого устройства передаётся без подмены кодом ошибки шлюза. */
|
||||
size_t smg_exception(const smg_request *r, unsigned code, uint8_t *out) { /* Служба задаёт причину, а формат исключения принадлежит ядру. */
|
||||
if (!r || !r->length || !out || !code || code > 255) { return 0; } out[0] = r->pdu[0] | 128u; out[1] = (uint8_t)code; return 2; /* Два байта PDU одинаково оборачиваются в TCP и RTU. */
|
||||
} /* Никакие ошибки внешней шины не превращаются в успешное подтверждение. */
|
||||
void smg_config_default(smg_config *c) { memset(c, 0, sizeof(*c)); c->baud = 115200; c->timeout_ms = 1000; c->stop_bits = 1; } /* По умолчанию нет маршрутов и нет неявных записей на RS485. */
|
||||
int smg_config_valid(const smg_config *c) { /* Целиком проверяет таблицу перед публикацией новой конфигурации. */
|
||||
unsigned i, j; if (!c || c->count > SMG_ROUTES || c->timeout_ms < 100 || c->timeout_ms > 60000 || c->parity > 2 || c->stop_bits < 1 || c->stop_bits > 2 || c->default_serial > 1) { return 0; } /* Тайм-аут выражен в мс и совпадает с допустимым диапазоном SETGUI. */
|
||||
if (c->baud != 1200 && c->baud != 2400 && c->baud != 4800 && c->baud != 9600 && c->baud != 19200 && c->baud != 38400 && c->baud != 57600 && c->baud != 115200 && c->baud != 230400 && c->baud != 460800 && c->baud != 921600) { return 0; } /* Только скорости, имеющие переносимые Linux termios constants. */
|
||||
for (i = 0; i < c->count; ++i) { const smg_route *r = c->routes + i; /* Unit ID источника однозначно выбирает одну внешнюю транзакцию. */
|
||||
if (!r->source || r->source >= SMG_CONTROL_UNIT || !r->target || r->transport > 1 || r->interface_id > 2) { return 0; } /* Адрес управления не может быть переопределён внешним устройством. */
|
||||
if (!r->transport && (r->target > 247 || r->interface_id || r->ipv4 || r->port)) { return 0; } /* RS485 не принимает сетевые параметры либо широковещательный адрес. */
|
||||
if (r->transport && (!r->port || !r->ipv4 || (r->ipv4 >> 24) == 0 || (r->ipv4 >> 24) >= 224)) { return 0; } /* Ethernet-маршрут требует unicast IPv4 и ненулевой TCP-порт; loopback допустим для независимых проверок порта. */
|
||||
for (j = 0; j < i; ++j) if (c->routes[j].source == r->source) { return 0; } /* Повторяющиеся адреса отклоняются вместо неопределённого выбора назначения. */
|
||||
} return 1; /* Физическое наличие интерфейса дополнительно проверяет порт ОС. */
|
||||
} /* Ядро не обращается к сети, файловой системе и настройкам UART. */
|
||||
size_t smg_config_pack(unsigned op, const smg_config *c, uint8_t *p, size_t cap) { /* Один формат обслуживает запись настроек, чтение и файл состояния. */
|
||||
unsigned i; size_t n; if (!p || op < 1 || op > 3 || cap < 2) { return 0; } if (op == 1) { p[0] = 65; p[1] = 1; return 2; } /* Запрос чтения не передаёт фиктивную конфигурацию. */
|
||||
if (!smg_config_valid(c)) { return 0; } n = 13 + 10 * c->count; if (cap < n) { return 0; } memset(p, 0, n); p[0] = 65; p[1] = (uint8_t)op; p[2] = 1; put16(p + 3, c->timeout_ms); put16(p + 5, c->baud >> 16); put16(p + 7, c->baud); p[9] = c->parity; p[10] = c->stop_bits; p[11] = c->default_serial; p[12] = c->count; /* Версия 1 фиксирует смещения и единицы измерения без сериализации native-структур. */
|
||||
for (i = 0; i < c->count; ++i) { const smg_route *r = c->routes + i; uint8_t *at = p + 13 + 10 * i; at[0] = r->source; at[1] = r->target; at[2] = r->transport; at[3] = r->interface_id; put16(at + 4, r->ipv4 >> 16); put16(at + 6, r->ipv4); put16(at + 8, r->port); } return n; /* Каждая запись имеет десять явных байтов независимо от padding C ABI. */
|
||||
} /* При ошибке ёмкости частичная таблица не формируется. */
|
||||
int smg_config_unpack(const uint8_t *p, size_t n, unsigned *op, smg_config *c) { /* Разбор выполняется во временную структуру до изменения рабочего состояния. */
|
||||
smg_config temporary; unsigned i; if (!p || !op || !c || n < 2 || p[0] != 65 || p[1] < 1 || p[1] > 3) { return 0; } if (p[1] == 1) { if (n != 2) { return 0; } *op = 1; return 1; } /* Чтение не изменяет переданный объект конфигурации. */
|
||||
if (n < 13 || p[2] != 1 || p[12] > SMG_ROUTES || n != 13u + 10u * p[12]) { return 0; } memset(&temporary, 0, sizeof(temporary)); temporary.timeout_ms = (uint16_t)get16(p + 3); temporary.baud = ((uint32_t)get16(p + 5) << 16) | get16(p + 7); temporary.parity = p[9]; temporary.stop_bits = p[10]; temporary.default_serial = p[11]; temporary.count = p[12]; /* Проверяет версию и длину до первого обращения к записи таблицы. */
|
||||
for (i = 0; i < temporary.count; ++i) { smg_route *r = temporary.routes + i; const uint8_t *at = p + 13 + 10 * i; r->source = at[0]; r->target = at[1]; r->transport = at[2]; r->interface_id = at[3]; r->ipv4 = ((uint32_t)get16(at + 4) << 16) | get16(at + 6); r->port = (uint16_t)get16(at + 8); } /* Распаковывает все адреса, сохраняя одинаковый numeric IPv4 на ПК и ARM. */
|
||||
if (!smg_config_valid(&temporary)) { return 0; } *c = temporary; *op = p[1]; return 1; /* Транзакционная публикация исключает частичную замену маршрутов при ошибке. */
|
||||
} /* Протокол управления не содержит имён файлов, команд shell и произвольных путей ОС. */
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "uksvep_can.h" /* Объявляет общий формат, метаданные и ABI профиля UKSVEP. */
|
||||
#include "balsam_can.h" /* Повторно использует исторический порядок трёх слов без второго кодека. */
|
||||
#include <string.h> /* Обнуляет кадр до заполнения присутствующих слов. */
|
||||
|
||||
typedef struct { /* Хранит подтверждённые параметры регистра из package.h/message.c. */
|
||||
uint16_t address; /* Индекс слова в modbus[128], без смещения Modbus 40001. */
|
||||
uint16_t maximum; /* Верхняя граница ввода в исходных единицах прошивки. */
|
||||
uint16_t initial; /* Значение для предпросмотра, не считается принятым от платы. */
|
||||
uint16_t flags; /* Отделяет параметры, команды и эмулируемое состояние. */
|
||||
const char *name; /* Постоянная подпись UTF-8 для всех потребителей профиля. */
|
||||
} field_t; /* Структура остаётся внутренней: через ABI передаются скаляры. */
|
||||
|
||||
static const field_t fields[] = { /* Источник адресов — Core/Inc/package.h текущего STM32-проекта. */
|
||||
{0U, 65535U, 0x247FU, UKSVEP_FIELD_STATE, "Дискретные входы"}, /* R0: нейтраль включает исправные питания, 380 В, ЛСУ и СКК; аварийные биты 8/9 выключены. */
|
||||
{1U, 65535U, 0U, UKSVEP_FIELD_STATE, "Неисправности"}, /* R1 вычисляется из входов и Alarm_mask, а не является прямым аппаратным входом. */
|
||||
{2U, 65535U, 0U, UKSVEP_FIELD_STATE, "Аварии"}, /* R2 вычисляется из входов и Error_mask. */
|
||||
{8U, 65535U, 0x277FU, UKSVEP_FIELD_PARAM, "Маска неисправностей"}, /* Default_params: 0x3FFF без битов 7, 11 и 12. */
|
||||
{9U, 65535U, 0x0200U, UKSVEP_FIELD_PARAM, "Маска аварий"}, /* По умолчанию выбран бит 9, авария сети 24 В. */
|
||||
{23U, 65535U, 0U, UKSVEP_FIELD_STATE, "Перемычки J0…J3 / TEST"}, /* Jumpers=R0x17, младший байт задаёт физические перемычки. */
|
||||
{48U, 65535U, 60U, UKSVEP_FIELD_PARAM, "Время до отключения, с"}, /* Millisecond умножает Sleep_time на 1000 мс. */
|
||||
{100U, 10U, 9U, UKSVEP_FIELD_PARAM, "Яркость лампы, 0…10"}, /* ШИМ использует десять отсчётов, штатное значение Brightness=9. */
|
||||
{101U, 65535U, 20U, UKSVEP_FIELD_PARAM, "Быстрый CAN, ×10 мс"}, /* Cancount[0]=20 соответствует 200 мс между пакетами. */
|
||||
{102U, 65535U, 200U, UKSVEP_FIELD_PARAM, "Медленный CAN, ×10 мс"}, /* Cancount[1]=200 соответствует 2000 мс между пакетами. */
|
||||
{125U, 65535U, 1U, UKSVEP_FIELD_STATE, "Версия прикладного протокола"}, /* PROTOKOL=1 относится к регистровому приложению, не к ProtoCAN Boot. */
|
||||
{126U, 65535U, 1U, UKSVEP_FIELD_STATE, "Mode = DeviceID + 1"}, /* При запросе метаданных значение уточняется по выбранному адресу. */
|
||||
{127U, 65535U, 0U, UKSVEP_FIELD_COMMAND, "Команды"} /* cTestLamp/cDefParam/cSaveParam/cLoadParam/cExtLamp/cExtLite/cReset. */
|
||||
}; /* Все адреса и значения по умолчанию определены в одном общем модуле. */
|
||||
|
||||
static const char *const inputs[] = { /* Порядок битов повторяет ReadEnteres в Core/Src/lampa.c. */
|
||||
"ИП1 · контроллер · PC13 · активный 0", /* Бит 0 читает инверсию IN_06. */
|
||||
"ИП2 · периферия · PC15 · активный 0", /* Бит 1 читает инверсию IN_05. */
|
||||
"ИП3 · замки / освещение · PA0 · активный 0", /* Бит 2 читает инверсию IN_04. */
|
||||
"ИП4 · датчики + · PA1 · активный 0", /* Бит 3 читает инверсию IN_03. */
|
||||
"ИП5 · датчики − · PA2 · активный 0", /* Бит 4 читает инверсию IN_02. */
|
||||
"ИП6 · драйверы · PA3 · активный 0", /* Бит 5 читает инверсию IN_01. */
|
||||
"Три фазы 380 В · PB15 · активный 0", /* Бит 6 читает инверсию IN_07. */
|
||||
"Заряд накопителя · PB14 · активный 0", /* Бит 7 не становится неисправностью либо аварией. */
|
||||
"Разряд накопителя · PB13 · активный 0", /* Бит 8 всегда становится неисправностью при единице. */
|
||||
"Авария 24 В · PB12 · активный 1", /* Бит 9 единственный читается без инверсии IN_10. */
|
||||
"Питание ЛСУ · PB1 · активный 0", /* Бит 10 читает инверсию IN_11. */
|
||||
"Резерв 11 · PB0 · активный 0", /* Бит 11 читает инверсию IN_12. */
|
||||
"Резерв 12 · PA5 · активный 0", /* Бит 12 читает инверсию IN_13. */
|
||||
"Питание СКК · PA4 · активный 0" /* Бит 13 читает инверсию IN_14. */
|
||||
}; /* Логическая единица здесь означает установленный бит R0, а не высокий GPIO. */
|
||||
|
||||
static const char *const commands[16] = { /* Имена битов берутся из package.h, без команд чужого задатчика. */
|
||||
"Тест ламп", "Параметры по умолчанию", "Сохранить параметры", "Загрузить параметры", /* Биты 0…3 действительно обслуживаются main. */
|
||||
"", "", "ExtLamp · флаг", "ExtLite · флаг", /* Биты 6/7 объявлены, но текущая логика лампы не использует их как отдельные команды. */
|
||||
"", "", "", "", "", "", "", "Перезагрузка" /* Бит 15 запрещает продление IWDG и вызывает аппаратный reset. */
|
||||
}; /* Пустые имена не создают ложных кнопок для резервных битов. */
|
||||
|
||||
uint32_t uksvep_can_id(unsigned device, unsigned from_device) /* Повторяет Setup_CAN_addr без связи с адресом boot Type=5. */
|
||||
{ /* Весь DeviceID ограничен четырьмя перемычками платы. */
|
||||
if (device > 15U || from_device > 1U) { /* Не обрезает ошибочный номер до адреса другого устройства. */
|
||||
return UINT32_MAX; /* Явный недопустимый ID исключает скрытую передачу. */
|
||||
} /* Направление 1 означает данные от эмулируемого или реального ВЭП. */
|
||||
return 0x001CE000UL | device | (from_device ? 0U : 0x20U); /* Командное смещение 0x20 отличается от 0x10 в Бальзам. */
|
||||
} /* Завершает единственное вычисление маршрутов UKSVEP. */
|
||||
|
||||
size_t uksvep_can_encode(unsigned device, unsigned from_device, unsigned address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity) /* Проверяет геометрию до общего кодека. */
|
||||
{ /* Один кадр вмещает до трёх последовательных слов uint16. */
|
||||
if (capacity < 8U || uksvep_can_id(device, from_device) == UINT32_MAX || address >= UKSVEP_REGISTER_COUNT || count == 0U || count > 3U || count > UKSVEP_REGISTER_COUNT - address) { /* Исключает переполнение банка R0…R127. */
|
||||
return 0U; /* Не выдаёт частично собранный пакет ошибочного диапазона. */
|
||||
} /* Параметры относятся к существующей карте платы. */
|
||||
return balsam_can_encode_payload((uint16_t)address, values, count, output, capacity); /* Общий кодек задаёт порядок word2, word3, адрес/маска, word1. */
|
||||
} /* Не содержит второго endian/масочного алгоритма. */
|
||||
|
||||
int uksvep_can_decode(unsigned device, uint32_t id, const uint8_t *data, size_t size, uint16_t *output, size_t capacity) /* Разбирает только выбранное устройство и обе его роли. */
|
||||
{ /* ABI возвращает плоские скаляры без зависимости от выравнивания C-структур. */
|
||||
balsam_can_frame_t frame = {0}; /* Общий payload decoder заполняет маску, адрес и три слова. */
|
||||
unsigned index; /* Перебирает только реально присутствующие биты маски. */
|
||||
unsigned count = 0U; /* Считает адресные пары, которые разрешено применить к RX-снимку. */
|
||||
if (capacity < 8U || device > 15U || output == NULL || id > 0x1FFFFFFFUL) { /* Отвергает неверные аргументы до сравнения маршрутов. */
|
||||
return -1; /* Ошибка не должна частично изменить снимок клиента. */
|
||||
} /* Проверка чужого ID выполняется до DLC и указателя данных. */
|
||||
if (id != uksvep_can_id(device, 0U) && id != uksvep_can_id(device, 1U)) { /* Broadcast и чужой ВЭП не выдаются за адресный ответ. */
|
||||
return 0; /* Позволяет общему CAN-монитору передать кадр другим потребителям. */
|
||||
} /* Дальше кадр гарантированно адресует выбранный профиль. */
|
||||
if (data == NULL || size != 8U || !balsam_can_decode_payload(data, size, &frame) || frame.present_mask == 0U) { /* Требует полный classic CAN data payload с полезными словами. */
|
||||
return -1; /* Повреждённый собственный кадр учитывается как ошибка RX. */
|
||||
} /* Проверяет все адреса до публикации результата. */
|
||||
for (index = 0U; index < 3U; ++index) { /* В кадре максимум три последовательных адреса. */
|
||||
if ((frame.present_mask & (4U >> index)) != 0U && frame.start_address + index >= UKSVEP_REGISTER_COUNT) { /* Отсутствующее слово не создаёт ложной записи R128/R129. */
|
||||
return -1; /* При выходе присутствующего слова за карту отвергается весь пакет. */
|
||||
} /* Проверка проходит до заполнения плоского output. */
|
||||
} /* Геометрия всех значащих слов проверена. */
|
||||
output[0] = id == uksvep_can_id(device, 1U); /* Сохраняет реальное направление, чтобы собственный TX не стал ответом платы. */
|
||||
for (index = 0U; index < 3U; ++index) { /* Сохраняет только слова с установленными битами присутствия. */
|
||||
if ((frame.present_mask & (4U >> index)) != 0U) { /* Неприсутствующие нули не должны затирать предыдущие RX-значения. */
|
||||
output[2U + count * 2U] = (uint16_t)(frame.start_address + index); /* Возвращает точный индекс регистра. */
|
||||
output[3U + count * 2U] = frame.values[index]; /* Возвращает полезное uint16 без потери старшего бита. */
|
||||
++count; /* Продвигает плоскую таблицу только после полезной пары. */
|
||||
} /* Завершает применение одного бита маски. */
|
||||
} /* Все полезные слова готовы к атомарному обновлению модели. */
|
||||
output[1] = (uint16_t)count; /* Число пар делает ABI независимым от завершающих неиспользуемых элементов. */
|
||||
return (int)count; /* Положительный результат отличает собственный валидный кадр от чужого. */
|
||||
} /* Завершает общий декодер профиля без GPIO и Qt. */
|
||||
|
||||
unsigned uksvep_field_count(void) /* Предоставляет размер карты из единственного источника. */
|
||||
{ /* Клиент не хранит вторую таблицу адресов. */
|
||||
return (unsigned)(sizeof(fields) / sizeof(fields[0])); /* Размер определяется фактическим C-массивом. */
|
||||
} /* Завершает запрос количества полей. */
|
||||
|
||||
int uksvep_field_info(unsigned index, unsigned device, uint16_t *output, size_t capacity) /* Возвращает метаданные в явном массиве четырёх uint16. */
|
||||
{ /* Неверный адрес устройства не должен давать правдоподобные параметры другой платы. */
|
||||
if (capacity < 4U || index >= uksvep_field_count() || device > 15U || output == NULL) { /* Проверяет границы всех входных аргументов. */
|
||||
return 0; /* При отказе выходной буфер не изменяется. */
|
||||
} /* Индекс описывает существующее именованное поле. */
|
||||
output[0] = fields[index].address; /* Индекс слова modbus без дополнительного смещения. */
|
||||
output[1] = fields[index].maximum; /* Ограничение ввода остаётся общим для GUI и будущих портов. */
|
||||
output[2] = fields[index].address == 126U ? (uint16_t)(device + 1U) : fields[index].initial; /* Mode зависит от четырёх перемычек выбранного адреса. */
|
||||
if (fields[index].address == 23U) { /* R23 содержит активные логические перемычки J0…J3 в младшем полубайте. */
|
||||
output[2] = (uint16_t)device; /* Начальный эмулируемый адрес соответствует выбранному DeviceID без TEST. */
|
||||
} /* Профиль не создаёт противоречия между исходящим ID и состоянием перемычек. */
|
||||
output[3] = fields[index].flags; /* Роль определяет доступность редактирования реальной платы. */
|
||||
return 1; /* Все четыре скаляра успешно заполнены. */
|
||||
} /* Завершает запрос метаданных без передачи C padding через ABI. */
|
||||
|
||||
const char *uksvep_field_name(unsigned index) /* Возвращает UTF-8 имя именованного регистра. */
|
||||
{ /* Неверный индекс не должен приводить к чтению произвольного указателя. */
|
||||
return index < uksvep_field_count() ? fields[index].name : ""; /* Пустая строка явно означает отсутствие поля. */
|
||||
} /* Память строки остаётся во владении библиотеки. */
|
||||
|
||||
const char *uksvep_input_name(unsigned bit) /* Возвращает назначение физического дискретного входа. */
|
||||
{ /* В прошивке используются ровно четырнадцать разрядов R0. */
|
||||
return bit < 14U ? inputs[bit] : ""; /* Резервные 14/15 не получают выдуманных GPIO. */
|
||||
} /* Завершает запрос общей карты входов. */
|
||||
|
||||
const char *uksvep_command_name(unsigned bit) /* Возвращает назначение управляющего бита R127. */
|
||||
{ /* Сохраняет пустые позиции резервных команд package.h. */
|
||||
return bit < 16U ? commands[bit] : ""; /* Проверяет индекс до доступа к таблице. */
|
||||
} /* Завершает запрос именованных команд. */
|
||||
|
||||
void uksvep_evaluate_inputs(uint16_t value, uint16_t alarm_mask, uint16_t error_mask, uint16_t *output, size_t capacity) /* Модель ReadEnteres для исходящих данных эмулируемого ВЭП. */
|
||||
{ /* Расчёт используется только по явной кнопке и не считается ответом реальной платы. */
|
||||
if (output == NULL || capacity < 2U) { /* Позволяет безопасно отвергнуть неверный буфер клиента. */
|
||||
return; /* Не обращается к отсутствующей памяти. */
|
||||
} /* Все действия ниже повторяют особые случаи битов 7/8/9 из lampa.c. */
|
||||
output[0] = (uint16_t)((uint16_t)~value & alarm_mask & (uint16_t)~0x0380U); /* Обычные неисправности — отсутствие включённых в маску входов; 7/8/9 обрабатываются отдельно. */
|
||||
output[0] |= (uint16_t)(value & 0x0300U); /* Разряд и авария 24 В всегда являются неисправностями. */
|
||||
output[1] = (uint16_t)((uint16_t)~value & error_mask & (uint16_t)~0x0380U); /* Обычные аварии также определяются инверсией входов и маской. */
|
||||
output[1] |= (uint16_t)((value & error_mask & 0x0100U) | (value & 0x0200U)); /* Разряд зависит от Error_mask.8, авария 24 В всегда активна при R0.9=1. */
|
||||
} /* Заряд накопителя R0.7 никогда не создаёт неисправность либо аварию. */
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "c2000_sci.h" /* Проверяет публичный ABI штатного SCI-загрузчика без COM и аппаратного стирания. */
|
||||
#include <stdlib.h> /* malloc предоставляет корректное выравнивание непрозрачного C-контекста. */
|
||||
#include <stdio.h> /* Печатает место нарушенного контракта при запуске CTest. */
|
||||
#include <string.h> /* Сравнивает пакеты с неизменяемыми эталонными октетами. */
|
||||
#define CHECK(condition) do { if (!(condition)) { fprintf(stderr, "SCI test line %d\n", __LINE__); exit(1); } } while (0) /* Проверки работают также в Release с NDEBUG. */
|
||||
static const uint8_t ram[] = {0xAA,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x90,2,0,0,0,0,0x90,0x12,0x34,0x56,0x78,0,0}; /* Два слова RAM L4 по адресу 0x9000 и та же точка входа. */
|
||||
static const uint8_t flash[] = {0xAA,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x32,0,0,0x80,2,0,0x32,0,0,0x80,0x12,0x34,0x56,0x78,0,0}; /* Два слова Flash сектора C по адресу 0x328000; ожидаемая маска TI равна 4. */
|
||||
static void packet(void *session, const uint8_t *expected, size_t length, size_t reply_size, const uint8_t *reply) { /* Проверяет одну эталонную передачу, не собирая пакет повторным кодеком. */
|
||||
uint8_t tx[2054]; size_t sent, received; uint32_t delay; /* Максимальная порция TI: 1024 слова и заголовок секции из шести октетов. */
|
||||
CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 1); /* Следующее действие обязано быть обменом, а не пропущенной паузой. */
|
||||
CHECK(sent == length && received == reply_size && delay == 0 && !memcmp(tx, expected, length)); /* Проверяет точное содержимое, границы и ожидаемый тип ответа. */
|
||||
if (reply_size) CHECK(c2000_sci_accept(session, reply, reply_size) == 0); /* Только достоверный ответ разрешает последующее действие. */
|
||||
} /* Завершает проверку одного шага. */
|
||||
static void pause_ti(void *session) { /* Ожидание проверяется детерминированно без системных часов и реальной трёхсекундной задержки. */
|
||||
uint8_t tx[1]; size_t sent, received; uint32_t delay; /* Пауза не требует передачи или ответа. */
|
||||
CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 2 && !sent && !received && delay == 3000); /* Длительность повторяет официальный host TI для Kernel A. */
|
||||
} /* Завершает проверку обязательной паузы. */
|
||||
int main(void) { /* Выполняет эталонный сеанс и основные ошибочные случаи на общем C-ядре. */
|
||||
void *session = malloc(c2000_sci_session_size()); uint8_t changed[sizeof(flash)], tx[2054], decoded[34]; size_t sent, received, written, i; uint32_t delay, entry; uint16_t sectors; /* Хранилище и выходные значения принадлежат host-тесту. */
|
||||
static const uint8_t sync[] = {'A'}, header_ack[] = {0x64,1}, block_ack[] = {0xC8,1}, monitor[] = {247,213,255,93,0,0,1,0,93,1}; /* Суммы эталона: заголовок 0x0164, секция 0x01C8; Monitor пишет параметр 93 значением 1 и передаёт сумму 255+93+1=349 (0x015D) младшим октетом вперёд. */
|
||||
static const uint8_t ascii[] = "\002 AA 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 90 02 00 00 00 00 90 12 34 56 78 00 00 \003\r\n"; /* Независимый ASCII-вектор TI содержит STX/ETX, а управляющие октеты не передаются MCU. */
|
||||
CHECK(session != NULL); CHECK(c2000_sci_validate(ram, sizeof(ram), 0, &entry, §ors) == 0 && entry == 0x9000 && sectors == 0); /* RAM-ядро должно иметь реально загруженный entry и не включать Flash-секторы. */
|
||||
CHECK(c2000_sci_validate(flash, sizeof(flash), 1, &entry, §ors) == 0 && entry == 0x328000 && sectors == 4); /* Прикладной эталон относится именно к сектору C F28335. */
|
||||
CHECK(c2000_sci_decode(ascii, sizeof(ascii) - 1, decoded, sizeof(decoded), &written) == 0 && written == sizeof(ram) && !memcmp(decoded, ram, written)); /* Проверяет точное совпадение бинарного и ASCII-представления одного образа. */
|
||||
CHECK(c2000_sci_begin(session, c2000_sci_session_size(), ram, sizeof(ram), flash, sizeof(flash)) == 0); /* Исправные буферы допускают начало autobaud. */
|
||||
packet(session, sync, 1, 1, sync); /* ROM подтверждает autobaud одним ASCII A. */
|
||||
for (i = 0; i < sizeof(ram); ++i) packet(session, ram + i, 1, 1, ram + i); /* Первичный загрузчик обязан подтвердить каждый октет RAM-ядра точным эхом. */
|
||||
pause_ti(session); packet(session, sync, 1, 1, sync); pause_ti(session); /* Ядро запускается и независимо синхронизирует UART перед erase. */
|
||||
packet(session, flash, 22, 2, header_ack); packet(session, flash + 22, 10, 2, block_ack); packet(session, flash + 32, 2, 0, NULL); /* Проверяет контрольные суммы после erase и программирования, затем терминатор без ACK. */
|
||||
CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 0 && c2000_sci_progress(session) == 100); /* Только полный подтверждённый сеанс заканчивает прогрессом 100 процентов. */
|
||||
memcpy(changed, flash, sizeof(flash)); changed[18] = changed[24] = 0x33; changed[20] = changed[26] = 0x80; changed[21] = changed[27] = 0xFF; CHECK(c2000_sci_validate(changed, sizeof(changed), 1, NULL, NULL) == -6); /* Загруженный entry не обходит запрет записи в CSM-область 0x33FF80. */
|
||||
CHECK(c2000_sci_validate(flash, sizeof(flash) - 2, 1, NULL, NULL) < 0 && c2000_sci_validate(flash, sizeof(flash), 0, NULL, NULL) < 0); /* Усечение таблицы и подмена RAM-ядра Flash-приложением отклоняются. */
|
||||
CHECK(c2000_sci_begin(session, c2000_sci_session_size(), ram, sizeof(ram), flash, sizeof(flash)) == 0); CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 1); /* Повторный независимый сеанс нужен для проверки неверного эха. */
|
||||
tx[0] = 'B'; CHECK(c2000_sci_accept(session, tx, 1) == -9 && c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == -8); /* После ошибки подтверждения автомат не должен продолжать стирание или запись. */
|
||||
CHECK(c2000_sci_ulitco_enter(tx, sizeof(tx)) == sizeof(monitor) && !memcmp(tx, monitor, sizeof(monitor))); /* Штатный переход Monitor также фиксируется эталонным wire-вектором. */
|
||||
free(session); return 0; /* Освобождает host-контекст; аппаратный интерфейс нигде не создавался. */
|
||||
} /* Завершает автономную проверку SCI Boot F28335. */
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "set_modbus_gateway.h" /* Проверяет публичный C-контракт той же сборки, что используется GUI и Zynq. */
|
||||
#include <assert.h> /* Независимые ожидаемые байты и границы должны прерывать выпуск при расхождении. */
|
||||
#include <stdio.h> /* Единственный PASS печатается после всех протокольных сценариев. */
|
||||
#include <string.h> /* Сравнивает эталонные wire-байты и неизменность конфигурации при отказе. */
|
||||
int main(void) { /* Тест не открывает USB, Ethernet или реальные регистры устройств. */
|
||||
const uint8_t golden_rtu[] = {1,3,0,0,0,10,0xc5,0xcd}, golden_tcp[] = {0x12,0x34,0,0,0,6,1,3,0,0,0,10}; smg_request r, parsed; uint8_t wire[SMG_ADU], pdu[253]; smg_config c, restored, before; unsigned op; size_t n; /* CRC C5 CD задана независимым распространённым вектором FC03. */
|
||||
assert(smg_decode_rtu(golden_rtu, sizeof(golden_rtu), &r)); r.transaction = 0x1234; assert(!smg_check(&r)); n = smg_tcp(&r, 1, r.pdu, r.length, wire, sizeof(wire)); assert(n == sizeof(golden_tcp) && !memcmp(wire, golden_tcp, n)); /* Перевод RTU → TCP сохраняет PDU и назначенный Transaction ID. */
|
||||
assert(smg_decode_tcp(golden_tcp, sizeof(golden_tcp), &parsed)); n = smg_rtu(parsed.unit, parsed.pdu, parsed.length, wire, sizeof(wire)); assert(n == sizeof(golden_rtu) && !memcmp(wire, golden_rtu, n)); /* Обратный перевод формирует строго исходный CRC и порядок байтов. */
|
||||
for (size_t i = 0; i < 6; ++i) assert(!smg_tcp_size(golden_tcp, i)); for (size_t i = 6; i <= sizeof(golden_tcp); ++i) assert(smg_tcp_size(golden_tcp, i) == 12); assert(!smg_decode_tcp(golden_tcp, 11, &parsed)); /* Разделённый MBAP не трактуется как повреждённый или уже завершённый пакет. */
|
||||
memcpy(wire, golden_tcp, 12); wire[2] = 1; assert(smg_tcp_size(wire, 12) == -1); wire[2] = 0; wire[4] = 1; wire[5] = 255; assert(smg_tcp_size(wire, 12) == -1); /* Некорректный Protocol ID и чрезмерная длина отклоняются до копирования. */
|
||||
memcpy(wire, golden_rtu, 8); wire[7] ^= 1; assert(!smg_decode_rtu(wire, 8, &parsed)); assert(!smg_tcp(&r, 1, r.pdu, r.length, wire, 11)); /* Ошибка CRC и недостаточная ёмкость не порождают валидный запрос. */
|
||||
r.pdu[3] = 0; r.pdu[4] = 0; assert(smg_check(&r) == 3); r.pdu[4] = 126; assert(smg_check(&r) == 3); r.pdu[4] = 2; r.pdu[1] = 255; r.pdu[2] = 255; assert(smg_check(&r) == 2); /* Количество и адресный wraparound проверяются до внешней операции. */
|
||||
r.pdu[0] = 5; r.pdu[3] = 1; assert(smg_check(&r) == 3); r.pdu[3] = 255; r.pdu[4] = 0; assert(!smg_check(&r)); /* FC05 принимает стандартные FF00 и 0000, а не произвольную маску. */
|
||||
r.pdu[0] = 16; r.pdu[1] = r.pdu[2] = r.pdu[3] = 0; r.pdu[4] = 2; r.pdu[5] = 4; r.length = 10; assert(!smg_check(&r)); r.pdu[5] = 3; assert(smg_check(&r) == 3); /* Весь блок FC16 валидируется до передачи, включая byte count. */
|
||||
r.pdu[5] = 4; n = smg_exception(&r, 11, pdu); assert(n == 2 && pdu[0] == 0x90 && pdu[1] == 11); n = smg_rtu(7, pdu, n, wire, sizeof(wire)); assert(smg_accept_rtu(&r, 7, wire, n, pdu, sizeof(pdu)) == 2); assert(!smg_accept_rtu(&r, 8, wire, n, pdu, sizeof(pdu))); /* Исключение устройства передаётся только при совпадении конечного slave. */
|
||||
const uint8_t echo[] = {16,0,0,0,2}; n = smg_tcp(&r, 7, echo, sizeof(echo), wire, sizeof(wire)); assert(smg_accept_tcp(&r, 7, wire, n, pdu, sizeof(pdu)) == 5); wire[0] ^= 1; assert(!smg_accept_tcp(&r, 7, wire, n, pdu, sizeof(pdu))); wire[0] ^= 1; wire[n - 1] = 3; assert(!smg_accept_tcp(&r, 7, wire, n, pdu, sizeof(pdu))); /* Чужой Transaction ID и подтверждение другого количества не принимаются. */
|
||||
smg_config_default(&c); assert(smg_config_valid(&c) && !c.default_serial && !c.count); c.count = 2; c.routes[0] = (smg_route){5,1,0,0,0,0}; c.routes[1] = (smg_route){6,1,1,2,0xc0a8010a,502}; n = smg_config_pack(2, &c, pdu, sizeof(pdu)); assert(n == 33 && smg_config_unpack(pdu, n, &op, &restored) && op == 2 && !memcmp(&c, &restored, sizeof(c))); /* Таблица одновременно содержит RS485 и Ethernet RJ2 с remap двух одинаковых Slave ID. */
|
||||
before = restored; pdu[23] = 5; assert(!smg_config_unpack(pdu, n, &op, &restored) && !memcmp(&before, &restored, sizeof(before))); /* Дубликат источника отвергается транзакционно, не затирая действующую таблицу. */
|
||||
c.count = SMG_ROUTES; for (unsigned i = 0; i < c.count; ++i) c.routes[i] = (smg_route){(uint8_t)(i + 1),1,0,0,0,0}; n = smg_config_pack(3, &c, pdu, sizeof(pdu)); assert(n == 213 && smg_config_unpack(pdu, n, &op, &restored)); n = smg_rtu(SMG_CONTROL_UNIT, pdu, n, wire, sizeof(wire)); assert(n == 216 && smg_rtu_response_size(wire, n) == 216 && smg_rtu_size(wire, n) == 216); /* Максимальная таблица помещается в один стандартный RTU ADU. */
|
||||
r.unit = SMG_CONTROL_UNIT; r.length = (uint16_t)smg_config_pack(1, NULL, r.pdu, sizeof(r.pdu)); assert(smg_accept_rtu(&r, SMG_CONTROL_UNIT, wire, n, pdu, sizeof(pdu)) == 213); /* Ответ FC65 проходит тот же CRC и потоковый USB-контракт, что обычные данные. */
|
||||
puts("PASS modbus gateway: golden TCP/RTU, bounds, CRC, correlation, configuration"); return 0; /* PASS относится только к завершённым проверкам общего ядра. */
|
||||
} /* Аппаратные тайминги и работа конкретного PHY проверяются отдельными сценариями. */
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "uksvep_can.h" /* Проверяет C99-профиль, который вызывают все GUI через ABI. */
|
||||
#include "balsam_can.h" /* Сохраняет регрессию существующей маршрутизации Бальзам после выделения общего payload. */
|
||||
#include <assert.h> /* Неверный проводной вектор должен завершать тест с отказом. */
|
||||
#include <string.h> /* Сравнивает байты с независимым эталоном, а не только с обратным decoder. */
|
||||
|
||||
int main(void) /* Фиксирует реальную карту UKSVEP из can.c/lampa.c/package.h. */
|
||||
{ /* Проверки не требуют Qt, аппаратного CAN или микроконтроллера. */
|
||||
const uint16_t words[] = {0x1234U, 0xABCDU, 0x8001U}; /* Различные слова выявляют перестановку и потерю старшего бита. */
|
||||
const uint8_t golden[] = {0xABU, 0xCDU, 0x80U, 0x01U, 0xE0U, 0x08U, 0x12U, 0x34U}; /* R8…R10: word2, word3, маска111/адрес8, word1. */
|
||||
const uint8_t sparse[] = {0xAAU, 0x55U, 0x33U, 0x44U, 0xA0U, 0x7DU, 0x11U, 0x22U}; /* Маска101 разрешает R125 и R127, оставляя R126 неизменным. */
|
||||
uint8_t payload[8]; /* Выходной буфер реального кодировщика C. */
|
||||
uint16_t decoded[8]; /* Плоский ABI буфер направления и пар регистров. */
|
||||
uint16_t info[4]; /* Адрес, максимум, начальное значение и роль регистра. */
|
||||
uint16_t state[2]; /* Расчётные неисправности и аварии эмулируемого ВЭП. */
|
||||
balsam_can_frame_t previous = {0}; /* Независимо проверяет существующий публичный decoder Бальзам. */
|
||||
unsigned device; /* Перебирает все положения четырёх перемычек. */
|
||||
unsigned index; /* Перебирает общую карту подтверждённых полей. */
|
||||
|
||||
for (device = 0U; device < 16U; ++device) { /* Адреса не должны наследовать Mode=9 и базу BA0000 другого ВЭП. */
|
||||
assert(uksvep_can_id(device, 0U) == 0x001CE020U + device); /* Командный ID берётся из Setup_CAN_addr текущего STM32-проекта. */
|
||||
assert(uksvep_can_id(device, 1U) == 0x001CE000U + device); /* Исходящий ID ВЭП не содержит командного смещения 0x20. */
|
||||
} /* Проверяет обе роли каждого физического адреса. */
|
||||
assert(uksvep_can_id(16U, 0U) == UINT32_MAX && uksvep_can_id(0U, 2U) == UINT32_MAX); /* Ошибочная цель не обрезается до соседнего устройства. */
|
||||
assert(uksvep_can_encode(5U, 0U, 8U, words, 3U, payload, 7U) == 0U); /* Малый выходной буфер не допускает записи восьми байтов за его границы. */
|
||||
assert(uksvep_can_encode(5U, 0U, 8U, words, 3U, payload, sizeof(payload)) == 8U); /* Строит стандартный трёхсловный командный кадр. */
|
||||
assert(memcmp(payload, golden, sizeof(golden)) == 0); /* Эталон проверяет endian, адрес и маску независимо от decoder. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE025U, golden, 8U, decoded, 8U) == 3); /* Распознаёт собственное командное направление. */
|
||||
assert(decoded[0] == 0U && decoded[2] == 8U && decoded[3] == 0x1234U && decoded[6] == 10U && decoded[7] == 0x8001U); /* Возвращает исходные пары без потери разрядов. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE005U, sparse, 8U, decoded, 8U) == 2); /* Частичный RX не требует всех трёх последовательных регистров. */
|
||||
assert(decoded[0] == 1U && decoded[2] == 125U && decoded[3] == 0x1122U && decoded[4] == 127U && decoded[5] == 0x3344U); /* Отсутствующий R126 не появляется среди обновлений. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE004U, golden, 8U, decoded, 8U) == 0); /* Данные другого адреса игнорируются. */
|
||||
assert(uksvep_can_decode(5U, 0x00BA0018U, golden, 8U, decoded, 8U) == 0); /* Старый ВЭП проекта Бальзам не подменяет STM32 УКСВЭП. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE005U, golden, 7U, decoded, 8U) == -1); /* Повреждённый DLC собственного узла отвергается. */
|
||||
assert(uksvep_can_encode(5U, 0U, 127U, words, 2U, payload, sizeof(payload)) == 0U); /* Запись второго слова не может пересечь R127. */
|
||||
assert(uksvep_can_encode(5U, 0U, 127U, words, 1U, payload, sizeof(payload)) == 8U && payload[4] == 0x80U && payload[5] == 0x7FU); /* Одно последнее слово использует маску100 и нулевые соседи. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE005U, payload, 8U, decoded, 8U) == 1); /* Последний валидный регистр можно принять отдельно. */
|
||||
payload[4] = 0xE0U; /* Искусственно делает присутствующими запрещённые R128/R129. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE005U, payload, 8U, decoded, 8U) == -1); /* Такой пакет отвергается целиком, без частичного применения R127. */
|
||||
assert(balsam_can_decode(0x00BA0017U, golden, 8U, &previous) == 1 && previous.device == 8U && previous.values[1] == 0xABCDU); /* Общий payload сохраняет прежний маршрут и слова задатчика. */
|
||||
for (index = 0U; index < uksvep_field_count(); ++index) { /* Каждое именованное поле должно принадлежать реальной карте. */
|
||||
assert(uksvep_field_info(index, 7U, info, 4U) && info[0] < 128U && info[2] <= info[1]); /* Начальное значение не превышает UI-границу поля. */
|
||||
assert(strlen(uksvep_field_name(index)) != 0U); /* Нет безымянных полей, которые выглядели бы пропущенной картой. */
|
||||
if (info[0] == 126U) { /* Физический адрес отличается от Mode на единицу. */
|
||||
assert(info[2] == 8U); /* DeviceID7 соответствует Mode8 независимо от названия устройства. */
|
||||
} /* Остальные defaults одинаковы для всех адресов. */
|
||||
} /* Размер карты задаёт C, а не отдельный список Python. */
|
||||
assert(!uksvep_field_info(999U, 0U, info, 4U) && strlen(uksvep_input_name(14U)) == 0U); /* За границами нет фиктивных регистров и GPIO. */
|
||||
assert(strstr(uksvep_input_name(9U), "PB12") != NULL && strlen(uksvep_command_name(4U)) == 0U); /* Особый прямой вход и резервная команда сохраняют реальное назначение. */
|
||||
uksvep_evaluate_inputs(0x247FU, 0x277FU, 0x0200U, state, 2U); /* Моделирует исправные штатные входы и маски. */
|
||||
assert(state[0] == 0U && state[1] == 0U); /* Исправное состояние не должно вызывать ложную аварию. */
|
||||
uksvep_evaluate_inputs(0x247EU, 0x277FU, 0x0200U, state, 2U); /* Убирает только исправность ИП1. */
|
||||
assert(state[0] == 1U && state[1] == 0U); /* Маска по умолчанию делает потерю ИП1 неисправностью, а не аварией. */
|
||||
uksvep_evaluate_inputs(0x27FFU, 0U, 0U, state, 2U); /* Включает заряд, разряд и аварию 24 В при нулевых масках. */
|
||||
assert(state[0] == 0x0300U && state[1] == 0x0200U); /* Заряд не влияет, разряд всегда неисправность, 24 В всегда авария. */
|
||||
uksvep_evaluate_inputs(0x25FFU, 0U, 0x0100U, state, 2U); /* Разрешает аварийный разряд отдельным битом маски. */
|
||||
assert(state[1] == 0x0100U); /* Особый бит 8 проверяется по своей полярности без инверсии. */
|
||||
return 0; /* Все эталоны, границы и регрессия старого профиля выполнены. */
|
||||
} /* Завершает независимую host-проверку C-профиля. */
|
||||
@@ -24,6 +24,8 @@ INCLUDE = ROOT / "include"
|
||||
JNI_INCLUDES: list[Path] = []
|
||||
SOURCES = [
|
||||
ROOT / "src" / name for name in (
|
||||
"c2000_sci.c", # Прямая MSVC/CC-сборка включает общий автомат SCI F28335.
|
||||
"set_modbus_gateway.c", # Экспортирует общий USB RTU/TCP кодек также при прямой MSVC/CC сборке без CMake.
|
||||
"deif_xdi.c", # Общий DEIF входит в host-библиотеку.
|
||||
"set_invertor_gas.c", # Собирает общий адаптер двух GAS для STM Invertor при прямом запуске host-builder.
|
||||
"set_deif_gauge.c", "set_igbt_realtime.c",
|
||||
@@ -32,6 +34,7 @@ SOURCES = [
|
||||
"altera_stream.c",
|
||||
"set_gas_watch.c", # Добавляет общий GAS_WATCH2 в прямые MSVC/CC сборки, чтобы новые ctypes/JNI символы были доступны.
|
||||
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c", "set_signal.c", "set_wavegen.c",
|
||||
"uksvep_can.c", # Включает общий профиль ВЭП также в прямую MSVC/CC сборку DLL.
|
||||
"balsam_can.c", "set_crc.c", "periph28335.c", "tms2812.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
|
||||
"pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",
|
||||
"pcan_gas.c", "pcan_modbus_server.c",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Прошивка STM32 через J-Link
|
||||
|
||||
В SETGUI откройте **Сервис → Прошивка** и выберите **STM32 / J-Link JTAG или SWD**.
|
||||
Установленный SEGGER J-Link Commander обнаруживается в Keil, каталоге SEGGER или PATH.
|
||||
При необходимости укажите `JLink.exe` кнопкой «Обзор». Одноимённый инструмент Java JDK программатором не является.
|
||||
|
||||
Выберите микроконтроллер из базы J-Link, JTAG либо SWD, частоту в кГц и фактический объём Flash в КиБ.
|
||||
Начальная частота — 1000 кГц. При нескольких подключённых J-Link укажите серийный номер нужного программатора.
|
||||
COM-порт и доступ к BOOT0 для этого режима не требуются.
|
||||
|
||||
Выберите HEX или BIN и нажмите «Начать прошивку». HEX сохраняет собственные абсолютные адреса и разрывы;
|
||||
для BIN укажите адрес в поле «Адрес приложения». При изменении файла после выбора требуется выбрать его заново.
|
||||
Если есть manifest выпуска, его target должен соответствовать выбранному MCU.
|
||||
|
||||
Каждая операция создаёт отдельный каталог в выбранной папке резервных копий:
|
||||
|
||||
- `before.bin` — вся Flash до записи;
|
||||
- `image.hex` — проверенный разреженный снимок записываемых данных;
|
||||
- `after.bin` — вся Flash после записи;
|
||||
- `program.jlink`, `program.log`, `program-dll.log` — команды и результаты программирования;
|
||||
- `run.jlink`, `run.log`, `run-dll.log` — отдельный этап сброса и запуска.
|
||||
|
||||
Программирование выполняется через `loadfile` без команды полного стирания.
|
||||
После встроенной проверки J-Link выполняется независимое побайтное сравнение readback с планом.
|
||||
Только после успешного сравнения отдельный процесс делает reset и `g`.
|
||||
Нулевой код выхода без полных снимков Flash не считается успешной записью.
|
||||
Отмена останавливает собственный процесс Commander; при прерванной записи нужна повторная прошивка.
|
||||
|
||||
## UKSVEP STM32F103RC
|
||||
|
||||
Для текущей платы оставьте включённым профиль UKSVEP, устройство `STM32F103RC`, объём 256 КиБ.
|
||||
Приложение начинается с `0x08008000`. Страница параметров `0x0801F800..0x0801FFFF` запрещена для записи,
|
||||
её сохранность проверяется сравнением `before.bin` и `after.bin`.
|
||||
|
||||
Для первой установки выберите разреженный `uksvep_install.hex`, содержащий boot и приложение.
|
||||
Для обновления через JTAG можно выбрать обычный HEX приложения или BIN с адресом `0x08008000`.
|
||||
Общий план добавит manifest CRC32 по адресу `0x0803F800`, который проверяет постоянный загрузчик.
|
||||
Готовый manifest установочного HEX сверяется с приложением. Перед запуском снимаются старые одноразовые
|
||||
маркеры boot в BKP DR9/DR10 без стирания резервного домена.
|
||||
|
||||
Для других плат снимите флажок UKSVEP и задайте их микроконтроллер и объём Flash.
|
||||
Защита страницы и формирование manifest UKSVEP относятся только к этому явно выбранному профилю.
|
||||
|
||||
## Расположение общей реализации
|
||||
|
||||
- `python/set_devices/stm32_jtag.py` — неизменяемые настройки, проверка образа, разреженный HEX, сценарии и readback;
|
||||
- `python/set_devices/qt_ports/stm32_jtag.py` — асинхронный порт QProcess, журналы, тайм-аут, отмена и reset/run;
|
||||
- `python/set_devices/qt_ports/stm32_jtag_panel.py` — встраиваемая форма Qt 5/6 с сохранением настроек;
|
||||
- `python/tests/test_stm32_jtag.py` — проверки плана, защиты параметров и ошибок без физического MCU.
|
||||
|
||||
Модули не импортируют `gui_desktop`. SETGUI подключает панель к существующим кнопкам, прогрессу и терминалу.
|
||||
Код загрузчика `stm32-jtag`, транспорт `stm32`, соответствует индексу 8 формы;
|
||||
профиль `flashing.profiles.stm32-jtag` может задавать `base_address`.
|
||||
Параметры конкретного J-Link сохраняются локально и не публикуются вместе с прошивкой.
|
||||
SEGGER Commander должен быть установлен отдельно; пакет SETGUI не скачивает и не включает сторонний executable.
|
||||
@@ -0,0 +1,146 @@
|
||||
"""Расчёт токов асинхронного двигателя по состояниям ключей инвертора.""" # Модель не зависит от GUI, sigrok и сторонних пакетов Python.
|
||||
import cmath # Комплексные числа представляют неподвижные электрические оси α и β.
|
||||
import math # Проверки параметров и преобразования СИ используют стандартную библиотеку.
|
||||
|
||||
|
||||
def finite(value, name, minimum=None, strict=False): # Единая проверка исключает NaN, бесконечности и нефизические параметры.
|
||||
number = float(value) # Параметры sigrok и непосредственных вызовов приводятся к одному числовому типу.
|
||||
if not math.isfinite(number) or minimum is not None and (number <= minimum if strict else number < minimum): # Строгая граница применяется к сопротивлениям и индуктивностям.
|
||||
raise ValueError('{} has an invalid value: {}'.format(name, value)) # Сообщение указывает конкретную настройку пользователя.
|
||||
return number # Возвращается только проверенное значение в единицах вызывающей стороны.
|
||||
|
||||
|
||||
def clarke(values): # Амплитудно-инвариантное преобразование сохраняет амплитуду симметричных фаз.
|
||||
return complex((2 * values[0] - values[1] - values[2]) / 3, (values[1] - values[2]) / math.sqrt(3)) # Оси αβ соответствуют порядку фаз A, B, C.
|
||||
|
||||
|
||||
def phases(vector): # Обратное преобразование восстанавливает три фазы трёхпроводной нагрузки.
|
||||
return (vector.real, -vector.real / 2 + math.sqrt(3) * vector.imag / 2, -vector.real / 2 - math.sqrt(3) * vector.imag / 2) # Сумма фаз равна нулю по первому закону Кирхгофа.
|
||||
|
||||
|
||||
class InductionMotor: # Линейная модель короткозамкнутого двигателя использует приведённые к статору параметры ротора.
|
||||
def __init__(self, rs=1.0, rr=0.8, lls_mh=5.0, llr_mh=5.0, lm_mh=150.0, pole_pairs=2, speed_rpm=1440.0, initial_flux=0.0, flux_angle=0.0, ia=0.0, ib=0.0): # Значения по умолчанию являются примером, а не паспортом конкретного двигателя.
|
||||
self.rs = finite(rs, 'Stator R (ohm)', 0, True) # Положительное сопротивление одной обмотки статора задаётся в омах.
|
||||
self.rr = finite(rr, 'Rotor R referred to stator (ohm)', 0, True) # Сопротивление ротора должно быть приведено к той же обмотке статора.
|
||||
lls = finite(lls_mh, 'Stator leakage L (mH)', 0, True) / 1000 # Миллигенри переводятся в генри перед вычислением уравнений.
|
||||
llr = finite(llr_mh, 'Rotor leakage L (mH)', 0, True) / 1000 # Параметр ротора также приводится к статору и переводится в генри.
|
||||
lm = finite(lm_mh, 'Magnetizing L (mH)', 0, True) / 1000 # Взаимная индуктивность задаёт магнитную связь без насыщения.
|
||||
pairs = finite(pole_pairs, 'Pole pairs', 1) # Число пар полюсов переводит механическую скорость в электрическую.
|
||||
if pairs != int(pairs): # Дробное число пар полюсов не описывает физический двигатель.
|
||||
raise ValueError('Pole pairs must be an integer.') # Ошибка выдаётся до расчёта токов.
|
||||
omega = finite(speed_rpm, 'Mechanical speed (rpm)') * pairs * math.tau / 60 # Один оборот равен 2π радиан, одна минута содержит 60 секунд.
|
||||
lr = llr + lm # Полная индуктивность ротора включает рассеяние и намагничивание.
|
||||
self.lsigma = lls + lm * llr / lr # Устойчивая форма Ls−Lm²/Lr не теряет точность при малом рассеянии.
|
||||
self.k = lm / lr # Коэффициент связи переводит потокосцепление ротора в уравнение статора.
|
||||
self.a = -(self.rs + self.rr * self.k * self.k) / self.lsigma # Первый диагональный коэффициент учитывает потери статора и приведённого ротора.
|
||||
self.b = self.k * complex(self.rr / lr, -omega) / self.lsigma # Влияние потока ротора включает его вращение относительно неподвижных осей.
|
||||
self.c = self.rr * self.k # Ток статора возбуждает поток ротора через взаимную индуктивность.
|
||||
self.d = complex(-self.rr / lr, omega) # Поток ротора затухает с постоянной Lr/Rr и вращается с электрической скоростью ротора.
|
||||
self.det = self.a * self.d - self.b * self.c # Определитель нужен для точного вынужденного решения при постоянном напряжении.
|
||||
self.mid = (self.a + self.d) / 2 # Половина следа матрицы задаёт среднее собственных значений.
|
||||
self.root = cmath.sqrt(((self.a - self.d) / 2) ** 2 + self.b * self.c) # Разность собственных значений позволяет вычислить матричную экспоненту без numpy.
|
||||
initial = (finite(ia, 'Initial Ia (A)'), finite(ib, 'Initial Ib (A)')) # Начальные линейные токи задаются в амперах.
|
||||
self.current = clarke((initial[0], initial[1], -sum(initial))) # Третья фаза определяется условием трёхпроводного подключения.
|
||||
self.flux = finite(initial_flux, 'Initial rotor flux linkage (Wb)', 0) * cmath.exp(1j * math.radians(finite(flux_angle, 'Initial rotor flux angle (deg)'))) # Модуль и угол задают неизвестное при начале записи начальное потокосцепление.
|
||||
self.cache = {} # Повторяющиеся интервалы переключений используют готовую матрицу перехода.
|
||||
|
||||
def advance(self, seconds, voltage): # Между фронтами ключей напряжение неизменно, поэтому используется точное линейное решение.
|
||||
dt = finite(seconds, 'Time interval (s)', 0) # Отрицательное время и неконечные интервалы запрещены.
|
||||
if not dt: # Повторные события одного отсчёта меняют ключи без изменения физического состояния.
|
||||
return phases(self.current) # Начальный ток сохраняется без искусственного шага интегрирования.
|
||||
matrix = self.cache.get(dt) # На равномерной сетке большинство длительностей повторяется.
|
||||
if matrix is None: # Новая длительность требует вычисления exp(A·dt) ровно один раз.
|
||||
if abs(self.root * dt) < 1e-8: # При близких собственных значениях деление на их разность теряет точность.
|
||||
factor = cmath.exp(self.mid * dt) # Общая экспонента остаётся ограниченной для пассивного двигателя.
|
||||
ratio = factor * dt * (1 + (self.root * dt) ** 2 / 6) # Разложение sinh(z)/z сохраняет точность около нуля.
|
||||
center = factor * (1 + (self.root * dt) ** 2 / 2) # Разложение cosh(z) согласовано с тем же пределом.
|
||||
else: # Раздельные экспоненты избегают переполнения cosh при сильно различающихся постоянных времени.
|
||||
first, second = cmath.exp((self.mid + self.root) * dt), cmath.exp((self.mid - self.root) * dt) # Обе моды учитывают затухание и вращение потока ротора.
|
||||
ratio, center = (first - second) / (2 * self.root), (first + second) / 2 # Эти коэффициенты задают экспоненту матрицы размерности два.
|
||||
matrix = (center + ratio * (self.a - self.mid), ratio * self.b, ratio * self.c, center + ratio * (self.d - self.mid)) # Порядок элементов соответствует строкам тока статора и потока ротора.
|
||||
if len(self.cache) >= 1024: # Ограничение в 1024 различных длительности исключает рост памяти на нерегулярном ШИМ.
|
||||
self.cache.clear() # Сброс кэша влияет только на производительность, а не на состояние двигателя.
|
||||
self.cache[dt] = matrix # Сохраняется только матрица перехода, независимая от текущего напряжения.
|
||||
drive = clarke(voltage) / self.lsigma # Фазные напряжения переводятся в неподвижный вектор скорости изменения тока.
|
||||
equilibrium_i, equilibrium_flux = -self.d * drive / self.det, self.c * drive / self.det # Частное решение −A⁻¹B·u учитывает постоянное напряжение на всём интервале.
|
||||
delta_i, delta_flux = self.current - equilibrium_i, self.flux - equilibrium_flux # Начальное состояние переносится относительно частного решения.
|
||||
self.current = equilibrium_i + matrix[0] * delta_i + matrix[1] * delta_flux # Ток обновляется без явного Эйлера и зависимости устойчивости от частоты отображения.
|
||||
self.flux = equilibrium_flux + matrix[2] * delta_i + matrix[3] * delta_flux # Поток использует прежний ток, сохранённый до обновления состояния.
|
||||
if not all(math.isfinite(value) for value in (self.current.real, self.current.imag, self.flux.real, self.flux.imag)): # Чрезмерные параметры не должны порождать молчаливые NaN в графиках.
|
||||
raise ValueError('Motor state overflow: check motor parameters and capture timing.') # Сообщение связывает ошибку с физическими параметрами и временем записи.
|
||||
return phases(self.current) # Возвращаются мгновенные токи трёх эквивалентных обмоток в амперах.
|
||||
|
||||
|
||||
class PwmMotor: # Связь логических входов, силовой схемы и общего двигателя не содержит зависимости от DSView.
|
||||
def __init__(self, samplerate, topology='2-level', dc_voltage=560.0, midpoint_percent=50.0, connection='star', active_high=True, **motor): # Полное напряжение звена задаётся между положительной и отрицательной шинами.
|
||||
self.rate = finite(samplerate, 'Samplerate (Hz)', 0, True) # Частота захвата переводит номера отсчётов в секунды.
|
||||
if topology not in ('2-level', '3-level P/N', '3-level NPC') or connection not in ('star', 'delta'): # Поддерживаются явные схемы без автоматического угадывания кодировки.
|
||||
raise ValueError('Unsupported inverter topology or winding connection.') # Неизвестная схема не подменяется другой электрической моделью.
|
||||
dc = finite(dc_voltage, 'Total DC link (V)', 0, True) # Значение в вольтах относится ко всему звену, а не к его половине.
|
||||
split = finite(midpoint_percent, 'Upper DC link (%)', 0, True) / 100 # Доля верхней половины позволяет моделировать дисбаланс конденсаторов.
|
||||
if split >= 1: # Обе половины звена должны иметь строго положительное напряжение.
|
||||
raise ValueError('Upper DC link fraction must be between 0 and 100 percent.') # Граничные значения не описывают трёхуровневое звено.
|
||||
self.upper, self.lower = dc * split, -dc * (1 - split) # Потенциалы полюсов отсчитываются от средней точки конденсаторов.
|
||||
self.topology, self.connection, self.active_high = topology, connection, bool(active_high) # Параметры сохраняются для интерпретации каждой новой комбинации ключей.
|
||||
ia, ib = motor.get('ia', 0.0), motor.get('ib', 0.0) # Начальные токи пользователь задаёт в линейных проводах независимо от схемы обмоток.
|
||||
if connection == 'delta': # В симметричном треугольнике линейный ток равен разности токов соседних обмоток.
|
||||
motor.update(ia=(2 * ia + ib) / 3, ib=(ib - ia) / 3) # Обратное преобразование предполагает отсутствие циркулирующего тока нулевой последовательности.
|
||||
self.motor = InductionMotor(**motor) # Параметры Rr и Lr всегда приведены к одной фактической обмотке статора.
|
||||
self.sample, self.pins, self.poles = None, None, (0.0, 0.0, 0.0) # До первого отсчёта не создаётся вымышленный участок записи.
|
||||
self.warnings = () # Диагностика сообщает о комбинациях, не задающих однозначное выходное напряжение.
|
||||
self.interval_warnings = () # Завершённый интервал сохраняет собственную диагностику до приёма следующего состояния затворов.
|
||||
|
||||
def currents(self): # Публичные результаты относятся к проводам A, B, C, подключённым к инвертору.
|
||||
a, b, c = phases(self.motor.current) # Эквивалентные токи обмоток сохраняют условие отсутствия нулевой последовательности.
|
||||
return (a - c, b - a, c - b) if self.connection == 'delta' else (a, b, c) # Для треугольника выводятся линейные токи, а не токи внутри обмоток.
|
||||
|
||||
def set_gates(self, pins): # Нумерация: Q1/P фаз A/B/C, Q4/N фаз A/B/C, затем Q2/Q3 каждой фазы.
|
||||
if len(pins) != 12 or any(value not in (0, 1, None) for value in pins): # Отсутствующий логический канал обозначается None до нормализации полярности.
|
||||
raise ValueError('Expected twelve gate inputs containing 0, 1 or None.') # Служебные значения sigrok не принимаются за реальный логический уровень.
|
||||
bits = tuple(None if value is None else bool(value) == self.active_high for value in pins) # Инверсия применяется только к реально назначенным входам.
|
||||
required = range(3) if self.topology == '2-level' else range(6) if self.topology == '3-level P/N' else range(12) # Три, шесть или двенадцать каналов нужны выбранной схеме.
|
||||
if any(bits[index] is None for index in required): # Неполная трёхфазная схема не позволяет определить линейные напряжения двигателя.
|
||||
raise ValueError('Assign all gate channels required by the selected topology.') # Ошибка возникает до продолжения расчёта по отсутствующим сигналам.
|
||||
if self.topology == '2-level' and any(value is not None for value in bits[3:6]) and any(value is None for value in bits[3:6]): # Нижние ключи двухуровневой схемы назначаются одновременно для всех фаз.
|
||||
raise ValueError('Assign either all three lower gates or none.') # Частично измеренные нижние ключи не смешиваются с идеальными дополнениями.
|
||||
poles, warnings = [], [] # Каждая фаза сначала получает собственный потенциал и список неоднозначностей.
|
||||
currents = self.currents() # Направление линейного тока задаёт приближённую проводимость обратных диодов во время dead time.
|
||||
for phase in range(3): # Все входы одного отсчёта обрабатываются одновременно, без ложных промежуточных состояний.
|
||||
high, low = bits[phase], bits[phase + 3] # В P/N это команды положительного и отрицательного уровней; в NPC это внешние Q1 и Q4.
|
||||
state = (high, low) # Стандартная таблица P/N использует только два внешних логических входа.
|
||||
table = {(True, False): self.upper, (False, True): self.lower, (False, False): 0.0} # Состояние 00 задаёт среднюю точку только в явной кодировке P/N.
|
||||
if self.topology == '2-level': # Один вход на фазу означает идеальную комплементарную пару без измеренного dead time.
|
||||
if low is None: # Нижняя команда восстанавливается только когда пользователь не назначил соответствующие входы.
|
||||
low = not high # Низкий уровень верхней команды соответствует отрицательной шине.
|
||||
state = (high, low) # Реальные нижние команды сохраняют интервалы одновременного закрытия ключей.
|
||||
table = {(True, False): self.upper, (False, True): self.lower} # Одновременное включение двух ключей не имеет допустимого фазного напряжения.
|
||||
if self.topology == '3-level NPC': # NPC обрабатывает четыре отдельных затворных сигнала каждого плеча.
|
||||
state = (high, bits[6 + 2 * phase], bits[7 + 2 * phase], low) # Физический порядок ключей идёт сверху вниз от положительной шины к отрицательной.
|
||||
table = {(True, True, False, False): self.upper, (False, True, True, False): 0.0, (False, False, True, True): self.lower} # Допустимые состояния Q1…Q4 равны 1100, 0110 и 0011.
|
||||
if state in table: # Однозначная команда непосредственно определяет полюсное напряжение.
|
||||
pole = table[state] # Токи интегрируются по реальному напряжению, а не по сглаженному заполнению ШИМ.
|
||||
elif not any(state): # Полностью закрытое плечо проводит через обратные диоды при ненулевом токе.
|
||||
pole = self.lower if currents[phase] > 1e-9 else self.upper if currents[phase] < -1e-9 else 0.0 # Порог 1 нА подавляет выбор шины по ошибке округления около нулевого тока.
|
||||
warnings.append('Phase {}: diode/dead-time approximation'.format('ABC'[phase])) # Неизвестные падения на диодах и смена проводимости явно отмечаются как приближение.
|
||||
else: # Недопустимые перекрытия и неполные состояния NPC не интерпретируются как достоверное напряжение.
|
||||
pole = self.poles[phase] # Последний определённый потенциал используется только как явно помеченное продолжение оценки.
|
||||
warnings.append('Phase {}: invalid gates {}; current estimate unreliable'.format('ABC'[phase], ''.join(str(int(value)) for value in state))) # Диагностика содержит фазу и фактическую комбинацию ключей.
|
||||
poles.append(pole) # Все три полюса сохраняются до вычисления напряжений обмоток.
|
||||
self.pins, self.poles, self.warnings = bits, tuple(poles), tuple(warnings) # Состояние относится к интервалу после текущего одновременного переключения.
|
||||
|
||||
def update(self, sample, pins=None): # Сначала завершается предыдущий электрический интервал и только затем меняются ключи.
|
||||
stamp = int(sample) # Номера отсчётов сохраняются целыми, чтобы исключить накопление ошибки временной сетки.
|
||||
if stamp != sample or stamp < 0 or self.sample is not None and stamp < self.sample: # Отсчёты должны поступать в хронологическом порядке.
|
||||
raise ValueError('Sample numbers must be nonnegative, integral and monotonic.') # Ошибка входной последовательности не маскируется нулевой длительностью.
|
||||
previous, start = self.currents(), self.sample # Левый конец сегмента берётся до интегрирования предыдущего напряжения.
|
||||
self.interval_warnings = self.warnings # Выходной пакет должен описывать ключи рассчитанного интервала, а не уже принятую новую команду.
|
||||
if start is not None and stamp > start: # Первый отсчёт определяет только начальное состояние записи.
|
||||
a, b, c = self.poles # Полюсные напряжения неизменны с прошлого обработанного фронта.
|
||||
voltage = (a - b, b - c, c - a) if self.connection == 'delta' else (a, b, c) # Преобразование Кларка само вычитает плавающий потенциал нейтрали звезды.
|
||||
self.motor.advance((stamp - start) / self.rate, voltage) # Точное решение учитывает длительность каждого фактического состояния ключей.
|
||||
self.sample = stamp # Обновление временной отметки предшествует приёму новых команд того же отсчёта.
|
||||
if pins is not None: # Периодические точки графика могут приходить без нового фронта.
|
||||
self.set_gates(pins) # Новые уровни действуют только справа от своей временной отметки.
|
||||
elif self.pins is not None: # Переоценка диодов на сетке ограничивает ошибку около смены знака тока.
|
||||
self.set_gates(tuple(None if value is None else int(value == self.active_high) for value in self.pins)) # Логические уровни переводятся обратно во входную полярность без двойной инверсии.
|
||||
return None if start is None or stamp == start else (start, stamp, previous, self.currents()) # Сегмент содержит физические амперы на обоих концах интервала.
|
||||
@@ -19,6 +19,11 @@ class PwmChannel:
|
||||
self.window_index = 1
|
||||
self.high = 0
|
||||
self.previous = None
|
||||
self.carrier_start = self.carrier_period = None # Два действительных активных фронта нужны для измерения несущей; начальный уровень не считается фронтом.
|
||||
self.carrier_count = self.carrier_samples = 0 # Счётчик полных периодов и их суммарная длина в отсчётах позволяют усреднять несущую без хранения записи.
|
||||
self.field_previous = self.field_crossing = self.field_pending = None # Интерполированное пересечение уровня 50% отделено от подтверждённого полного периода поля.
|
||||
self.field_armed = False # До отрицательной полуволны начальный фрагмент огибающей не может открыть измерение поля.
|
||||
self.field_carrier = None # Снимок счётчиков несущей привязывает среднюю частоту к последнему завершённому периоду поля.
|
||||
|
||||
def next_boundary(self):
|
||||
if self.window is None or self.origin is None:
|
||||
@@ -34,9 +39,33 @@ class PwmChannel:
|
||||
alpha = -math.expm1(-(midpoint - previous[0]) / self.tau)
|
||||
value = previous[1] + alpha * (value - previous[1])
|
||||
self.previous = (midpoint, value)
|
||||
carrier = self.rate / self.carrier_period if self.carrier_period and end - self.carrier_start <= 2 * self.carrier_period else None # После двух периодов без фронтов прежняя несущая больше не считается текущим измерением.
|
||||
field = self._field(midpoint, value) # Частота поля определяется по той же нормированной огибающей, которая выводится на график.
|
||||
return dict(start=start, end=end, duty=duty, value=value,
|
||||
frequency=self.rate / (end - start), midpoint=midpoint,
|
||||
previous=previous)
|
||||
previous=previous, carrier_frequency=carrier, field=field) # Частота окна остаётся совместимой со старым контрактом, измеренная несущая и полный период поля передаются отдельно.
|
||||
|
||||
def _field(self, sample, value): # Потоковый детектор измеряет электрическую частоту между одноимёнными пересечениями уровня заполнения 50%.
|
||||
previous = self.field_previous # Предыдущая точка нужна для линейной интерполяции времени пересечения между серединами периодов ШИМ.
|
||||
self.field_previous = (sample, value) # Храним только последнюю точку, поэтому память не зависит от длительности захвата.
|
||||
if value <= -0.02: # Гистерезис 0,02 нормированной шкалы соответствует одному процентному пункту заполнения ниже 50%.
|
||||
self.field_armed = True # Отрицательная полуволна разрешает одно следующее положительное пересечение.
|
||||
if self.field_armed and previous is not None and previous[1] <= 0 < value: # Учитываем только восходящий переход, чтобы не удвоить электрическую частоту.
|
||||
self.field_pending = previous[0] - previous[1] * (sample - previous[0]) / (value - previous[1]) # Интерполяция устраняет квантование периода поля шагом несущей.
|
||||
if value <= 0: # Возврат через нуль до положительного порога означает неподтверждённое пересечение.
|
||||
self.field_pending = None # Колебания внутри полосы гистерезиса не создают отдельные периоды.
|
||||
if not self.field_armed or self.field_pending is None or value < 0.02: # Положительный порог также отстоит от 50% на один процентный пункт заполнения.
|
||||
return None # Постоянное заполнение и неполные полуволны не получают выдуманную частоту поля.
|
||||
crossing = self.field_pending # Подтверждение использует время нулевого пересечения, а не более позднее достижение порога.
|
||||
last = self.field_crossing # Два подтверждённых восходящих пересечения ограничивают один полный электрический период.
|
||||
counters = (self.carrier_count, self.carrier_samples) # Снимок относится только к действительно наблюдавшимся активным фронтам.
|
||||
old_counters = self.field_carrier # Разность накопленных счётчиков даёт среднюю несущую за текущий период поля.
|
||||
self.field_crossing, self.field_carrier = crossing, counters # Следующий период начинается с текущего подтверждённого пересечения.
|
||||
self.field_armed, self.field_pending = False, None # Новое измерение разрешается только после следующей отрицательной полуволны.
|
||||
if last is None: # Первое пересечение задаёт начало, но ещё не содержит полного периода.
|
||||
return None # Начальный неполный фрагмент записи исключён из результата.
|
||||
count, elapsed = counters[0] - old_counters[0], counters[1] - old_counters[1] # Длительность измеренных периодов несущей выражена в отсчётах, а не миллисекундах.
|
||||
return dict(start=round(last), end=round(crossing), frequency=self.rate / (crossing - last), carrier_frequency=self.rate * count / elapsed if elapsed else None) # Полный период поля получает частоту в Гц и среднюю измеренную несущую, если за интервал были фронты.
|
||||
|
||||
def update(self, sample, pin):
|
||||
level = bool(pin) == self.active_high
|
||||
@@ -48,6 +77,8 @@ class PwmChannel:
|
||||
if sample < self.last:
|
||||
raise ValueError('Samples must be monotonic.')
|
||||
events = []
|
||||
if level != self.level and level and self.window is None: # В автоматическом режиме новый активный фронт одновременно завершает текущий период ШИМ.
|
||||
self._carrier_edge(sample) # Обновляем измерение до выдачи завершённого периода, чтобы частота относилась именно к нему.
|
||||
if self.window is not None:
|
||||
boundary = self.next_boundary()
|
||||
while boundary <= sample:
|
||||
@@ -58,6 +89,8 @@ class PwmChannel:
|
||||
self.window_index += 1
|
||||
boundary = self.next_boundary()
|
||||
self.high += (sample - self.last) * self.level
|
||||
if level != self.level and level: # В режиме окон частота несущей всё равно измеряется по фронтам, независимо от заданного размера окна.
|
||||
self._carrier_edge(sample) # Фронт на границе окна относится к следующему окну и не меняет уже выданное измерение.
|
||||
elif level != self.level:
|
||||
if level:
|
||||
if self.start is not None and self.fall is not None:
|
||||
@@ -67,3 +100,121 @@ class PwmChannel:
|
||||
self.fall = sample
|
||||
self.last, self.level = sample, level
|
||||
return events
|
||||
|
||||
def _carrier_edge(self, sample): # Оба режима восстановления используют единый счётчик фактических периодов несущей.
|
||||
if self.carrier_start is not None and sample > self.carrier_start: # Первый фронт только задаёт начало; нулевой период не допускается.
|
||||
self.carrier_period = sample - self.carrier_start # Интервал активный фронт — активный фронт задаёт полный период ШИМ в отсчётах.
|
||||
self.carrier_count += 1 # Счётчик нужен для среднего числа полных периодов за электрический цикл.
|
||||
self.carrier_samples += self.carrier_period # Суммируем длительности, чтобы переменная несущая усреднялась как число периодов за время.
|
||||
self.carrier_start = sample # Следующее измерение начинается с текущего реального фронта.
|
||||
|
||||
|
||||
class NpcChannel(PwmChannel): # Один вход NPC объединяет четыре затвора S1…S4, расположенные сверху вниз в фазной стойке.
|
||||
STATES = {(1, 1, 0, 0): 1, (0, 1, 1, 0): 0, (0, 0, 1, 1): -1} # Допустимые состояния классического NPC дают +Udc/2, нейтраль и −Udc/2 соответственно.
|
||||
|
||||
def __init__(self, samplerate, active_high=True, carrier_hz=0, smoothing_ms=0): # Настройки времени и полярности совпадают с двухуровневым режимом.
|
||||
super().__init__(samplerate, active_high, carrier_hz, smoothing_ms) # Общий расчёт частоты поля и фильтра применяется к среднему знаковому уровню NPC.
|
||||
self.integral = self.known = 0 # Интеграл уровня выражен в отсчётах; known хранит длительность достоверных состояний.
|
||||
self.last_valid = self.pulse_sign = None # Пропуск dead-time не должен ошибочно создавать новый импульс той же полярности.
|
||||
|
||||
def state(self, gates): # Преобразование использует все четыре затвора, а не только два внешних ключа.
|
||||
return self.STATES.get(tuple(int(bool(pin) == self.active_high) for pin in gates)) # Неизвестное состояние, включая все выключенные ключи, возвращает None вместо выдуманного нуля напряжения.
|
||||
|
||||
def _accumulate(self, sample): # Между цифровыми событиями уровень стойки считается постоянным только при допустимой комбинации затворов.
|
||||
if self.level is not None: # Во время dead-time напряжение зависит от тока и диодов и не определяется одними управляющими сигналами.
|
||||
self.integral += (sample - self.last) * self.level # Знаковая площадь сохраняет отличие положительных, нулевых и отрицательных участков.
|
||||
self.known += sample - self.last # В среднем используются лишь участки, однозначно восстановленные по затворам.
|
||||
self.last = sample # Новый интервал начинается после полностью учтённой части времени.
|
||||
|
||||
def _npc_emit(self, start, end): # Среднее по допустимым состояниям является оценкой огибающей, а не измерением напряжения в dead-time.
|
||||
coverage = self.known / (end - start) # Доля известного времени позволяет отделить короткие коммутационные паузы от повреждённой записи.
|
||||
if coverage < 0.95: # При более чем 5% неизвестного времени в периоде оценка считается недостаточно определённой и пропускается.
|
||||
self.previous = self.field_previous = self.field_crossing = self.field_pending = self.field_carrier = None # Разрыв не должен соединяться графиком или измерением поля с предыдущим достоверным участком.
|
||||
self.field_armed = False # Новое поле должно заново подтвердить отрицательную полуволну.
|
||||
return [] # Неизвестный интервал не превращается в фиктивный уровень нуля.
|
||||
high = (self.integral / self.known + 1) * (end - start) / 2 # Общий _emit ожидает эквивалентную активную длительность; преобразование сохраняет средний уровень −1…+1.
|
||||
event = self._emit(start, end, high) # Общая обработка формирует середину периода, частоты и фильтрованное значение.
|
||||
event['valid_fraction'] = coverage # Точность определения уровня видна в числовой аннотации и экспорте.
|
||||
return [event] # Формат списка совместим с обновлением двухуровневого канала.
|
||||
|
||||
def update(self, sample, gates): # Одно событие получает атомарный снимок четырёх цифровых входов.
|
||||
level = self.state(gates) # Только три штатные комбинации имеют определённый нормированный уровень стойки.
|
||||
if self.last is None: # Начальный уровень не является обнаруженным фронтом несущей.
|
||||
self.last, self.level, self.last_valid = sample, level, level # Состояние начинается с текущего снимка без выдуманного предыдущего импульса.
|
||||
if self.window is not None: # В режиме заданной несущей интегрирование начинается с области декодирования.
|
||||
self.origin = self.start = sample # Границы окон совпадают с прежним контрактом PwmChannel.
|
||||
return [] # Начальная точка ещё не содержит полного периода.
|
||||
if sample < self.last: # Равные номера допустимы для одновременных фронтов разных каналов.
|
||||
raise ValueError('Samples must be monotonic.') # Отрицательные интервалы нельзя интегрировать или выдавать в DSView.
|
||||
entry = level in (-1, 1) and level != self.last_valid # Начало ненулевого импульса распознаётся с учётом последнего достоверного состояния до dead-time.
|
||||
events = [] # Результаты содержат лишь полные периоды или окна.
|
||||
if self.window is not None: # Фиксированные окна сохраняют 0/100% и работают при отсутствии переключений.
|
||||
boundary = self.next_boundary() # Граница рассчитывается от начала, поэтому дробный период не накапливает ошибку округления.
|
||||
while boundary <= sample: # Одно цифровое событие может завершить несколько окон постоянного уровня.
|
||||
self._accumulate(boundary) # Учёт предыдущего состояния заканчивается точно на границе окна.
|
||||
events.extend(self._npc_emit(self.start, boundary)) # Неизвестные участки проверяются до формирования результата.
|
||||
self.start = boundary # Следующее окно начинается без перекрытия с предыдущим.
|
||||
self.integral = self.known = 0 # Площадь и известное время относятся только к новому окну.
|
||||
self.window_index += 1 # Номер следующей границы задаёт её положение в общей шкале отсчётов.
|
||||
boundary = self.next_boundary() # Повторное вычисление сохраняет точность дробной частоты.
|
||||
self._accumulate(sample) # Оставшаяся часть предыдущего уровня учитывается до изменения затворов.
|
||||
if entry: # Частота несущей измеряется по началам последовательных ненулевых импульсов одинакового знака.
|
||||
if self.pulse_sign == level: # Переход положительной полуволны в отрицательную не является коротким периодом несущей.
|
||||
self._carrier_edge(sample) # Измеренный период не подменяется частотой заданного окна.
|
||||
if self.window is None and self.start is not None: # Автоматический режим завершает период на одноимённом фронте следующего импульса.
|
||||
events.extend(self._npc_emit(self.start, sample)) # Огибающая получается из интеграла всех трёх уровней за полный период.
|
||||
else: # Первое появление другой полярности заново синхронизирует несущую этой полуволны.
|
||||
self.carrier_start, self.carrier_period = sample, None # Интервал через смену знака не принимается за измеренную частоту.
|
||||
self.pulse_sign = level # Следующий ненулевой импульс сравнивается с текущей полярностью.
|
||||
if self.window is None: # В автоматическом режиме начало и интеграл обновляются только на опорных фронтах.
|
||||
self.start = sample # Начальный неполный фрагмент полуволны исключается.
|
||||
self.integral = self.known = 0 # Новый период не наследует площадь предыдущего.
|
||||
self.level = level # Новое состояние действует только после текущего цифрового события.
|
||||
if level is not None: # Неизвестная коммутационная пауза не стирает информацию о последнем допустимом уровне.
|
||||
self.last_valid = level # Это исключает ложные импульсы при кратком выключении и возврате к прежней комбинации.
|
||||
return events # Адаптер получает тот же поток результатов, что для обычного ШИМ.
|
||||
|
||||
|
||||
class PhaseAnalysis: # Согласование времён A/B/C отделено от конкретного числа затворов в фазе.
|
||||
def __init__(self, samplerate): # Частота нужна для контроля устаревших оценок фазового периода.
|
||||
self.rate = samplerate # Все временные координаты остаются в отсчётах захвата.
|
||||
self.points, self.fields = {}, {} # Для интерполяции хранятся две последние точки каждой огибающей и последний полный период поля.
|
||||
self.last_vector = None # Одинаковый общий момент времени не должен выдавать повторный угол.
|
||||
|
||||
def add(self, channel, event): # Каждый восстановленный период обновляет только свою фазу.
|
||||
points = self.points.setdefault(channel, []) # Отсутствующая фаза не получает нулевой или выдуманный сигнал.
|
||||
if event.get('previous', True) is None: # Ядро явно разрывает огибающую после недостаточно определённого окна NPC.
|
||||
points.clear() # Интерполяция не должна соединять новую точку с данными до неизвестного участка.
|
||||
self.fields.pop(channel, None) # Межфазные углы также должны заново дождаться полного достоверного периода.
|
||||
points.append((event['midpoint'], event['value'])) # Угол вычисляется по восстановленным средним уровням, а не по мгновенным коммутациям.
|
||||
del points[:-2] # Ограниченная история сохраняет постоянный расход памяти на длинных записях.
|
||||
if event['field'] is not None: # Межфазные углы требуют наблюдавшихся полных электрических периодов.
|
||||
field = event['field'] # Результат содержит последнее восходящее пересечение и частоту данной фазы.
|
||||
self.fields[channel] = (field['end'], self.rate / field['frequency']) # Период хранится в отсчётах для вычисления сдвига в градусах.
|
||||
if any(len(self.points.get(ch, [])) < 2 for ch in range(3)): # Пространственный вектор нельзя восстановить из неподключённых или ещё не заполненных A/B/C.
|
||||
return None # Начальный участок записи не получает ложного θ.
|
||||
sample = min(self.points[ch][-1][0] for ch in range(3)) # Общая точка не выходит за наблюдавшееся время ни одной фазы.
|
||||
if sample <= max(self.points[ch][0][0] for ch in range(3)) or self.last_vector is not None and sample <= self.last_vector: # Требуется общий интервал и новое время, без экстраполяции остановившейся фазы.
|
||||
return None # Несогласованные точки не смешиваются в один вектор.
|
||||
values = [] # Три значения будут относиться к одному и тому же моменту времени.
|
||||
for ch in range(3): # Порядок A/B/C определяет положительное направление вращения.
|
||||
left, right = self.points[ch] # Две соседние точки ограничивают достоверный интервал данной фазы.
|
||||
values.append(left[1] + (right[1] - left[1]) * (sample - left[0]) / (right[0] - left[0])) # Линейная интерполяция устраняет сдвиг, вызванный разными моментами фронтов PWM A/B/C.
|
||||
alpha = (2 * values[0] - values[1] - values[2]) / 3 # Полное преобразование Кларка удаляет синфазную составляющую, включая добавку SVPWM.
|
||||
beta = (values[1] - values[2]) / math.sqrt(3) # Ортогональная ось соответствует последовательности A, B, C.
|
||||
self.last_vector = sample # Данный момент времени считается обработанным даже при почти нулевом векторе.
|
||||
if math.hypot(alpha, beta) < 0.02: # Ниже 2% нормированного уровня направление вектора не считается определённым.
|
||||
return dict(sample=sample, theta=None) # Разрыв явно запрещает соединять графиком углы через нулевую амплитуду.
|
||||
return dict(sample=sample, theta=math.degrees(math.atan2(beta, alpha)) % 360) # θ выражен в электрических градусах 0…360 относительно положительной оси A.
|
||||
|
||||
def differences(self, sample): # Положительный результат A→B означает запаздывание фазы B относительно A.
|
||||
angles = {} # Отсутствующая пара не получает выдуманный сдвиг 120 градусов.
|
||||
for first, second in ((0, 1), (1, 2), (2, 0)): # Измеряются все три запрошенные пары независимо.
|
||||
if first not in self.fields or second not in self.fields: # Каждой фазе пары нужен хотя бы один полный измеренный период.
|
||||
continue # Две подключённые фазы всё ещё могут дать одну доступную пару.
|
||||
a, b = self.fields[first], self.fields[second] # Последние одноимённые пересечения задают фазовые часы двух огибающих.
|
||||
if any(sample < item[0] or sample - item[0] > 2 * item[1] for item in (a, b)): # После двух периодов без нового измерения прежний сдвиг становится устаревшим.
|
||||
continue # Остановившийся или пропавший вход не сохраняет старое числовое значение.
|
||||
angle = 360 * ((sample - a[0]) / a[1] - (sample - b[0]) / b[1]) # Разность фазовых часов выражает запаздывание второй огибающей в градусах.
|
||||
angles['ABC'[first] + '→' + 'ABC'[second]] = (angle + 180) % 360 - 180 # Знаковый диапазон −180…180 различает запаздывание и опережение, включая обратную последовательность фаз.
|
||||
return angles # Результаты рассчитываются из записи и не предполагают заранее правильную трёхфазную систему.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Потоковый CIC-декодер однобитного сигма-дельта модулятора без зависимостей от GUI.""" # Ядро одинаково работает в DSView и автономных анализаторах.
|
||||
|
||||
|
||||
class SigmaDeltaFilter: # Каскад интеграторов и гребенчатых звеньев реализует фильтры sinc первого, второго и третьего порядка.
|
||||
def __init__(self, decimation=256, order=3, invert=False): # Децимация задаётся числом тактов на результат; 256 и sinc3 являются начальными настройками анализатора.
|
||||
if isinstance(decimation, bool) or not isinstance(decimation, int) or not 1 <= decimation <= 65536: # Ограничение 65536 тактов удерживает максимальное усиление sinc3 в пределах 49 бит.
|
||||
raise ValueError('Decimation must be an integer from 1 to 65536.') # Дробные и неположительные окна не определяют допустимую децимацию.
|
||||
if isinstance(order, bool) or not isinstance(order, int) or order not in (1, 2, 3): # Число интеграторов должно совпадать с числом гребенчатых звеньев.
|
||||
raise ValueError('Filter order must be 1, 2 or 3.') # Неизвестный порядок нельзя незаметно заменять sinc3.
|
||||
self.decimation = decimation # Каждый результат появляется после этого числа принятых битов.
|
||||
self.order = order # Порядок также определяет длительность начального установления фильтра.
|
||||
self.invert = bool(invert) # Инверсия применяется к битам до фильтрации и меняет знак восстановленного сигнала.
|
||||
self.gain = decimation ** order # Усиление CIC при постоянном входе равно R в степени порядка при дифференциальной задержке один.
|
||||
self.mask = (1 << self.gain.bit_length()) - 1 # Модуль степени двойки строго больше усиления сохраняет точный результат при переполнениях интеграторов.
|
||||
self.integrators = [0] * order # Нулевое начальное состояние не предполагает неизвестную предысторию захвата.
|
||||
self.delays = [0] * order # Каждый гребенчатый каскад хранит свой предыдущий вход на пониженной частоте.
|
||||
self.phase = 0 # Счётчик внутри окна исключает накопление дробной ошибки децимации.
|
||||
self.blocks = 0 # Первые order минус один результатов относятся к установлению и не выдаются пользователю.
|
||||
|
||||
def feed(self, bit): # Принимается ровно один уровень DATA на выбранном фронте CLK.
|
||||
if bit not in (0, 1): # Служебные значения неподключённых каналов sigrok не являются цифровыми уровнями.
|
||||
raise ValueError('Sigma-delta data must be a logic level 0 or 1.') # Ошибка назначения входов не должна превращаться в правдоподобную амплитуду.
|
||||
value = int(bit) ^ self.invert # Униполярное представление позволяет нормировать результат без знакового переполнения.
|
||||
for stage in range(self.order): # Последовательно обновляем интеграторы на полной частоте модулятора.
|
||||
self.integrators[stage] = (self.integrators[stage] + value) & self.mask # Модульная арифметика ограничивает память независимо от длительности записи.
|
||||
value = self.integrators[stage] # Следующий интегратор получает уже обновлённый выход предыдущего.
|
||||
self.phase += 1 # Считаем только фактически захваченные тактовые фронты.
|
||||
if self.phase < self.decimation: # Между границами децимации выходной отсчёт ещё не готов.
|
||||
return None # Неполные окна не дополняются вымышленными битами.
|
||||
self.phase = 0 # Новый блок начинается со следующего реального бита.
|
||||
for stage in range(self.order): # Гребенчатые звенья выполняются только на выходной частоте.
|
||||
previous = value # Задержка сохраняет вход текущего звена, а не его разность.
|
||||
value = (value - self.delays[stage]) & self.mask # Разности устраняют накопление интеграторов, включая их модульные переполнения.
|
||||
self.delays[stage] = previous # Это состояние используется через R входных тактов.
|
||||
self.blocks = min(self.order, self.blocks + 1) # Счётчик установления насыщается и не растёт вместе с записью.
|
||||
if self.blocks < self.order: # Для sincN пропускаем первые N минус один выходных блоков с неизвестной предысторией.
|
||||
return None # Частично установившийся фильтр не выдаёт ложное смещение постоянного сигнала.
|
||||
code = 2 * value - self.gain # Биполярный код принимает диапазон от минус R**N до плюс R**N без привязки к конкретному АЦП.
|
||||
return {'code': code, 'gain': self.gain, 'value': code / self.gain, 'density': value / self.gain} # Нормированная амплитуда и фильтрованная плотность единиц сохраняют точный масштаб CIC.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,110 @@
|
||||
"""Тонкий Python-порт общей CAN-карты UKSVEP на STM32F103RC.""" # Форматы, GPIO и расчёт неисправностей принадлежат C99 templates.
|
||||
from __future__ import annotations # Аннотации остаются совместимыми с исходниками Windows 7.
|
||||
import ctypes as ct # Передаёт скаляры и буферы единственной нативной реализации.
|
||||
from dataclasses import dataclass # Представляет готовые результаты C без протокольных вычислений.
|
||||
from .native import get_native_protocol, NativeProtocolUnavailable # Использует штатный поиск и жизненный цикл общей DLL.
|
||||
|
||||
@dataclass(frozen=True) # Метаданные одинаковы для всех представлений GUI.
|
||||
class Field: # Значения читаются из package.h-совместимой таблицы C.
|
||||
address: int # Нулевой индекс регистра modbus[128].
|
||||
maximum: int # Верхняя граница ввода в исходных единицах.
|
||||
initial: int # Значение предпросмотра, которое ещё не принято от платы.
|
||||
flags: int # 1 — параметр, 2 — команды, 4 — эмулируемое состояние.
|
||||
name: str # Общая подпись UTF-8 из C-профиля.
|
||||
|
||||
@dataclass(frozen=True) # Пакет отделён от физического CAN-драйвера.
|
||||
class Frame: # Готовые байты нельзя изменять через поля виджета после отправки.
|
||||
can_id: int # Полный 29-битный ID без аппаратного бита IDE.
|
||||
data: bytes # Восемь байтов payload, построенных C.
|
||||
|
||||
@dataclass(frozen=True) # Снимок содержит только реально присутствующие слова.
|
||||
class Packet: # Источник пакета отделяет ответ ВЭП от команды управляющего узла.
|
||||
from_device: bool # Истина означает ID данных ВЭП, а не собственное эхо команд.
|
||||
values: tuple[tuple[int, int], ...] # Пары адрес/значение, разрешённые маской присутствия C.
|
||||
|
||||
class UksvepCan: # Каждая панель использует общее нативное ядро без собственного codec.
|
||||
def __init__(self): # Привязывает только дополнительные экспорты профиля UKSVEP.
|
||||
self.lib = get_native_protocol().lib # Загрузка DLL и общий ABI уже проверены штатным портом.
|
||||
signatures = { # Типы описывают ABI, а не байтовый порядок wire-протокола.
|
||||
"uksvep_can_id": ([ct.c_uint, ct.c_uint], ct.c_uint32), # Возвращает маршрут для физического адреса и роли.
|
||||
"uksvep_can_encode": ([ct.c_uint, ct.c_uint, ct.c_uint, ct.POINTER(ct.c_uint16), ct.c_size_t, ct.POINTER(ct.c_uint8), ct.c_size_t], ct.c_size_t), # Кодирует до трёх слов в буфер caller.
|
||||
"uksvep_can_decode": ([ct.c_uint, ct.c_uint32, ct.POINTER(ct.c_uint8), ct.c_size_t, ct.POINTER(ct.c_uint16), ct.c_size_t], ct.c_int), # Возвращает плоский массив пар из C.
|
||||
"uksvep_field_count": ([], ct.c_uint), # Позволяет получить все поля без таблицы дублей Python.
|
||||
"uksvep_field_info": ([ct.c_uint, ct.c_uint, ct.POINTER(ct.c_uint16), ct.c_size_t], ct.c_int), # Возвращает четыре скаляра метаданных.
|
||||
"uksvep_field_name": ([ct.c_uint], ct.c_char_p), # C владеет постоянной строкой UTF-8.
|
||||
"uksvep_input_name": ([ct.c_uint], ct.c_char_p), # Назначения GPIO остаются общими с профилем.
|
||||
"uksvep_command_name": ([ct.c_uint], ct.c_char_p), # Пустое имя обозначает резервный бит R127.
|
||||
"uksvep_evaluate_inputs": ([ct.c_uint16, ct.c_uint16, ct.c_uint16, ct.POINTER(ct.c_uint16), ct.c_size_t], None), # Рассчитывает R1/R2 для эмуляции по алгоритму C.
|
||||
} # Символы добавлены без изменения существующего ABI версии 1.
|
||||
try: # Старая DLL должна давать понятную ошибку вместо тихого Python fallback.
|
||||
for name, (arguments, result) in signatures.items(): # Каждый экспорт получает точную сигнатуру до первого вызова.
|
||||
function = getattr(self.lib, name) # Отсутствие профиля в старой DLL обнаруживается здесь.
|
||||
function.argtypes = arguments # ctypes не угадывает размер указателей и целых.
|
||||
function.restype = result # Сохраняет 32-битные ID и указатели строк на 64-битном хосте.
|
||||
except AttributeError as error: # Формат wire не реализуется заново в аварийной ветви.
|
||||
raise NativeProtocolUnavailable("В общей DLL нет профиля UKSVEP. Пересоберите setprotocol.dll из templates.") from error # Поясняет требуемое действие пользователю.
|
||||
|
||||
@staticmethod # Валидация Python-аргумента предотвращает обрезание ctypes до C-типа.
|
||||
def _device(value): # Адрес хранится целым числом из четырёх перемычек.
|
||||
if not isinstance(value, int) or not 0 <= value <= 15: # Нельзя молча адресовать соседний узел по младшим битам.
|
||||
raise ValueError("DeviceID ВЭП должен быть 0…15") # Ошибка возникает до передачи CAN.
|
||||
return value # Валидный скаляр далее проверяется также C-профилем.
|
||||
|
||||
def can_id(self, device, from_device=False): # Доверяет вычисление маршрута общему C-профилю.
|
||||
return int(self.lib.uksvep_can_id(self._device(device), bool(from_device))) # Python не повторяет базу и смещение идентификатора.
|
||||
|
||||
def fields(self, device=0): # Получает подтверждённые поля выбранного ВЭП.
|
||||
device = self._device(device) # Проверяет адрес до создания начальных значений Mode.
|
||||
result = [] # Список содержит только готовые метаданные ABI.
|
||||
for index in range(self.lib.uksvep_field_count()): # Размер карты также принадлежит C.
|
||||
raw = (ct.c_uint16 * 4)() # Caller владеет явно ограниченным выходным буфером.
|
||||
if not self.lib.uksvep_field_info(index, device, raw, len(raw)): # Проверяет результат вызова вместо чтения частично готового массива.
|
||||
raise ValueError("Не удалось получить карту UKSVEP") # Ошибка не заменяется выдуманными defaults.
|
||||
name = self.lib.uksvep_field_name(index).decode("utf-8") # Декодирует только строковое представление общей подписи.
|
||||
result.append(Field(*map(int, raw), name)) # Не меняет единицы и адреса, возвращённые C.
|
||||
return tuple(result) # Карта не изменяется виджетами во время работы.
|
||||
|
||||
def names(self, kind): # Читает общие назначения входов либо команд.
|
||||
function = self.lib.uksvep_input_name if kind == "inputs" else self.lib.uksvep_command_name # Выбор относится к категории отображения, а не к codec.
|
||||
return tuple(function(bit).decode("utf-8") for bit in range(16)) # Резервные позиции остаются пустыми строками.
|
||||
|
||||
def frames(self, device, address, values, from_device=False): # Делит длинный снимок на штатные вызовы трёхсловного C-кодировщика.
|
||||
device = self._device(device) # Неверный адрес не может быть обрезан ctypes.
|
||||
values = tuple(values) # Фиксирует входной снимок до первой физической записи.
|
||||
if not values or not isinstance(address, int) or not 0 <= address < 128 or len(values) > 128 - address: # Проверяет Python-диапазон до преобразования unsigned.
|
||||
raise ValueError("Выберите непустой диапазон R0…R127") # Ошибка обнаруживается до формирования первого пакета.
|
||||
if any(not isinstance(value, int) or not 0 <= value <= 65535 for value in values): # Не обрезает отрицательные или переполненные слова.
|
||||
raise ValueError("Значения ВЭП должны быть uint16: 0…65535") # Поле пользователя не должно превращаться в другое число на проводе.
|
||||
result = [] # Все байты создаются до передачи, чтобы не отправить половину неверного снимка.
|
||||
for offset in range(0, len(values), 3): # Ограничение трёх слов задано существующим историческим форматом.
|
||||
part = values[offset:offset + 3] # UI-порт только разбивает контейнер, не строит маску/endian.
|
||||
source = (ct.c_uint16 * len(part))(*part) # Преобразует значения в явно типизированный вход C.
|
||||
target = (ct.c_uint8 * 8)() # Payload classic CAN ограничен восемью байтами.
|
||||
size = self.lib.uksvep_can_encode(device, bool(from_device), address + offset, source, len(part), target, len(target)) # Все поля wire заполняет C99.
|
||||
if size != 8: # Валидный UKSVEP кадр обязан иметь полный DLC.
|
||||
raise ValueError("C-профиль отклонил регистры UKSVEP") # Ошибка не скрывается частичной передачей.
|
||||
result.append(Frame(self.can_id(device, from_device), bytes(target))) # Возвращает готовый транспортно-независимый кадр.
|
||||
return tuple(result) # Физический writer принадлежит существующему BusTab.
|
||||
|
||||
def decode(self, device, can_id, data): # Вызывает общий decoder с ограниченными буферами.
|
||||
device = self._device(device) # Проверяет адрес до входа в ctypes.
|
||||
if not isinstance(can_id, int) or not 0 <= can_id <= 0x1FFFFFFF: # Исключает знаковое переполнение и биты вне extended ID.
|
||||
return None # Такое событие не принадлежит допустимому CAN-профилю.
|
||||
data = bytes(data) # Фиксирует входной payload на время вызова C.
|
||||
source = (ct.c_uint8 * len(data)).from_buffer_copy(data) # Caller владеет памятью до завершения синхронного разбора.
|
||||
target = (ct.c_uint16 * 8)() # Восемь скаляров вмещают направление и три адресные пары.
|
||||
count = self.lib.uksvep_can_decode(device, can_id, source, len(data), target, len(target)) # C выбирает ID, DLC, маску и границы карты.
|
||||
if count < 0: # Повреждённый пакет выбранного ВЭП не считается чужим.
|
||||
raise ValueError("Повреждённый CAN-кадр UKSVEP: DLC, маска или адрес") # UI может увеличить счётчик отказов RX.
|
||||
if count == 0: # Пакет другого адреса не меняет снимок.
|
||||
return None # Общая шина продолжает обрабатывать его независимо.
|
||||
pairs = tuple((int(target[2 + index * 2]), int(target[3 + index * 2])) for index in range(count)) # Переводит плоский ABI в готовые пары без повторного endian-разбора.
|
||||
return Packet(bool(target[0]), pairs) # Направление не позволяет выдать эхо TX за ответ платы.
|
||||
|
||||
def evaluate(self, inputs, alarm_mask, error_mask): # Предоставляет явный расчёт состояния эмулируемого ВЭП.
|
||||
values = (inputs, alarm_mask, error_mask) # Передаёт только исходные слова C-алгоритму ReadEnteres.
|
||||
if any(not isinstance(value, int) or not 0 <= value <= 65535 for value in values): # Защищает параметры от неявного uint16 обрезания.
|
||||
raise ValueError("Входы и маски должны быть uint16") # Неверный ввод не превращается в правдоподобную аварию.
|
||||
result = (ct.c_uint16 * 2)() # Буфер содержит R1 и R2, вычисленные C.
|
||||
self.lib.uksvep_evaluate_inputs(*values, result, len(result)) # Python не повторяет особые случаи заряд/разряд/24 В.
|
||||
return tuple(map(int, result)) # Результат используется только в исходящем эмулируемом снимке.
|
||||
@@ -468,6 +468,11 @@ class Rs485PacketParser:
|
||||
# Команды прошивки
|
||||
# ==========================================================================
|
||||
|
||||
def command_can_interface(name: str) -> str: # Расширение общего контракта выбирает PS CAN Linux, сохраняя protocol wire в templates.
|
||||
if name not in ('can0', 'can1'): raise ValueError('CAN-интерфейс должен быть can0 или can1') # Закрытый набор не допускает произвольных имён и внедрения дополнительных команд.
|
||||
return 'N' + name # Выбор интерфейса не включает драйвер и не передаёт кадры автоматически.
|
||||
|
||||
|
||||
def command_bitrate(kbps: str | int) -> str:
|
||||
"""Скорость шины CAN, кбит/с."""
|
||||
return "S%s" % kbps
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -44,3 +44,10 @@ CANDLE_G474_BOOT = { # Профиль воспроизводит CAN_BOOTLOADER
|
||||
"device_type": 7, "device": 15, "product": 0, "hardware": 0, # Идентичность ProtoCAN BOOT: Type 7, Device 15, Product 0 и аппаратная ревизия 0.
|
||||
"can_bitrate": 500_000, "uart_baud": 512_000, # Classic CAN работает на 500 кбит/с, USART1 PA9/PA10 — на 512000 бод, 8N1.
|
||||
} # Адреса и скорости относятся только к CANdle G474VET6 и не подменяют профиль USB BOOT G474RB.
|
||||
|
||||
UKSVEP_F103RC_BOOT = { # Профиль отдельного boot-проекта UKSVEP_ICE_22220.6 согласован с ports/stm32f103rc-uksvep/boot_profile.h.
|
||||
"app_base": 0x08008000, "app_size": 94 * 1024, # Начальные 32 КиБ заняты BOOT; приложение заканчивается перед существующей EEPROM-страницей 0x0801F800.
|
||||
"stack_base": 0x20000000, "stack_end": 0x2000C000, # DS5792 определяет для STM32F103RC 48 КиБ SRAM и восьмибайтовое выравнивание начального MSP.
|
||||
"device_type": 5, "product": 0x5606, "hardware": 1, # DeviceID задают активные нулём перемычки PA6/PA7/PC4/PC5; Product отличает ВЭП от других STM32.
|
||||
"can_bitrate": 250_000, "uart_baud": 115_200, # Загрузчик UKSVEP использует CAN PA11/PA12 и USART1 PA9 TX/PA10 RX на 115200 бод.
|
||||
} # Single-slot обновление сохраняет параметры; для запуска требуется постоянный manifest и CRC образа.
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,107 @@
|
||||
"""Единый неблокирующий порт CAN485 для USB CDC/UART и Ethernet TCP.""" # Протокол кадров остаётся в can485_board и переносимом C-ядре templates.
|
||||
from .qt_compat import QObject, Signal, QTimer, QSerialPort, QElapsedTimer # Qt elapsed timer ограничивает также короткое подтверждение восстановления UART-консоли.
|
||||
try: # Выбирает тот же Qt binding, который использует остальное приложение.
|
||||
from PySide6.QtNetwork import QTcpSocket, QAbstractSocket # TCP передаёт тот же поток строк, что физический USB-порт.
|
||||
except ImportError: # Старые системы используют доступный PySide2.
|
||||
from PySide2.QtNetwork import QTcpSocket, QAbstractSocket # Публичный контракт адаптера не зависит от версии Qt.
|
||||
|
||||
|
||||
class Can485Port(QObject): # Совместимая с BoardTab оболочка исключает копирование протокольных и экранных обработчиков.
|
||||
readyRead = Signal() # Уведомляет монитор о произвольном фрагменте, включая неполную строку.
|
||||
errorOccurred = Signal(object) # Использует существующий обработчик ошибок последовательного порта панели.
|
||||
connected = Signal() # TCP-подключение подтверждается асинхронно после фактического установления соединения.
|
||||
|
||||
def __init__(self, parent=None): # Создаёт закрытые endpoints без аппаратного ввода-вывода при импорте или открытии вкладки.
|
||||
super().__init__(parent) # QObject связывает жизненный цикл сокетов с владельцем панели.
|
||||
self.serial = QSerialPort(self); self.tcp = QTcpSocket(self); self.kind = 'USB'; self._closing = False; self._error = ''; self._endpoint = ''; self._console = False; self._console_ready = False; self._console_rx = bytearray(); self.console_restored=True # Состояние восстановления консоли проверяется аппаратным USB-тестом после закрытия.
|
||||
self.serial.readyRead.connect(self._serial_ready); self.tcp.readyRead.connect(self.readyRead) # USB-консоль ожидает подтверждение службы, затем передаёт bytes штатному parser строк.
|
||||
self.serial.errorOccurred.connect(self._serial_error); self.tcp.errorOccurred.connect(self._tcp_error); self.tcp.disconnected.connect(self._disconnected) # Потеря соединения останавливает циклическую отправку через обработчик панели.
|
||||
self.tcp.connected.connect(self._connected) # Начальная настройка выполняется только после подтверждённого TCP handshake.
|
||||
self.timeout = QTimer(self); self.timeout.setSingleShot(True); self.timeout.timeout.connect(self._timed_out) # Конечный срок TCP не блокирует GUI даже при отключённой плате.
|
||||
self._shared=None # Единая сборка позволяет CAN и Modbus одновременно использовать один физический FTDI.
|
||||
|
||||
def _connected(self): # Завершает фазу ожидания и разрешает запрос версии.
|
||||
self.timeout.stop(); self.connected.emit() # Запоздалый timeout больше не может закрыть действующую сессию.
|
||||
|
||||
def _tcp_error(self, error): # Преобразует ошибку TCP в общий контракт порта.
|
||||
if not self._closing: self._error = self.tcp.errorString(); self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # GUI получает исходную диагностическую строку сокета.
|
||||
|
||||
def _serial_error(self, error): # Сохраняет штатные коды QSerialPort, включая отсутствие ошибки.
|
||||
if not self._closing: self._error = self.serial.errorString(); self.errorOccurred.emit(error) # USB-обрыв проходит тот же cleanup, что сетевой.
|
||||
|
||||
def _disconnected(self): # FIN от платы завершает сессию даже без отдельного socket error.
|
||||
if not self._closing: self._error = 'Ethernet: соединение закрыто платой'; self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # Панель прекращает периодическую передачу и очищает подтверждённые настройки.
|
||||
|
||||
def _timed_out(self): # Срабатывает только для незавершённого TCP-подключения.
|
||||
if not self.isOpen(): self._error = 'Нет ответа CAN-службы Zynq за 5 секунд. Для USB-консоли нужны root shell и установленная zynq-can485d.'; self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # Ожидание отсутствующего сервиса либо login prompt не блокирует GUI.
|
||||
|
||||
def _serial_ready(self): # В режиме консоли отделяет готовность туннеля от простого открытия COM-порта.
|
||||
if self._closing: return # Во время короткого ожидания shell prompt данные читает cleanup, не вызывая повторные GUI callbacks.
|
||||
if not self._console: self.readyRead.emit(); return # Прямой USB CDC/UART сохраняет исходные bytes без буферной копии.
|
||||
self._console_rx.extend(bytes(self.serial.readAll())) # Shell и nc могут выдавать произвольные фрагменты строк.
|
||||
if not self._console_ready and b'#INFO Zynq CAN485 / templates' in self._console_rx: self._console_ready=True; self.timeout.stop(); self.connected.emit() # Команды CAN разрешаются только после реального ответа общего daemon.
|
||||
if self._console_ready: self.readyRead.emit() # После подтверждения весь поток, включая сохранённый заголовок, поступает штатному монитору.
|
||||
elif len(self._console_rx)>8192: del self._console_rx[:-4096] # До handshake хранит ограниченный хвост консольного вывода.
|
||||
|
||||
def start_console_tunnel(self, port=37579): # Использует уже открытую root UART-консоль как USB-канал к тому же TCP-порту службы.
|
||||
port=int(port) # Числовой аргумент проверяется до формирования команды shell.
|
||||
if not 1<=port<=65535: raise ValueError('TCP-порт CAN должен быть 1..65535') # Исключает неверный endpoint и внедрение shell-команд из параметра.
|
||||
self._console=True; self._console_ready=False; self._console_rx.clear(); self.console_restored=False; self.timeout.start(5000) # Открытый tty не выдаётся за подтверждённый CAN-сеанс.
|
||||
self.serial.write(f'\rstty -echo -icanon min 1 time 0; nc 127.0.0.1 {port}; stty sane\r'.encode('ascii')) # Штатный nc передаёт общий протокол без копии daemon и без перенастройки USB/Device Tree.
|
||||
|
||||
def connect_tcp(self, host, port): # Endpoint уже проверен UI, DNS и синхронное ожидание не требуются.
|
||||
self.close(); self.kind = 'Ethernet'; self._endpoint = f'{host}:{port}'; self._error = ''; self.timeout.start(5000); self.tcp.connectToHost(host, port) # Пять секунд ограничивают ожидание недоступного TCP-порта 37579.
|
||||
|
||||
def connect_console(self,name,port=37579): # Общий FTDI канал открывает отдельную CAN TCP сессию без захвата COM второй вкладкой.
|
||||
from .console_link_port import ConsoleLink # Общий менеджер делится с Modbus и сохраняет действующий USB host HDMI.
|
||||
self.close(); self.kind='USB'; self._endpoint=name; self._error=''; self.console_restored=False; self._shared=ConsoleLink.acquire(name,port,self); channel=self._shared; self.timeout.start(5000) # Начальное открытие остаётся отменяемым до подтверждения backend.
|
||||
channel.readyRead.connect(self.readyRead); channel.opened.connect(self._connected); channel.errorOccurred.connect(lambda error:self._shared_error(channel,error)) # Прикладные CAN строки и ошибки принадлежат только своему логическому каналу.
|
||||
|
||||
def _shared_error(self,channel,error): # Ошибка локального CAN backend не должна выдавать сообщение физического QtSerialPort другого протокола.
|
||||
if not self._closing: self._error=channel.errorString(); self.errorOccurred.emit(error) # Стандартный BoardTab cleanup завершает только CAN сессию.
|
||||
|
||||
def isOpen(self): # Штатные кнопки BoardTab учитывают реальное состояние выбранного канала.
|
||||
if self._shared is not None: return self._shared.active and self._shared.isOpen() # Общий COM не считается готовым CAN до OPEN конкретного loopback backend.
|
||||
return self.tcp.state() == QAbstractSocket.SocketState.ConnectedState if self.kind == 'Ethernet' else self.serial.isOpen() and (not self._console or self._console_ready) # Открытый USB tty с незавершённым nc handshake тоже не считается CAN-подключением.
|
||||
|
||||
def isConnecting(self): # Позволяет отменить незавершённую попытку подключения повторным нажатием кнопки.
|
||||
return self.timeout.isActive() # Таймер активен только в фазе TCP handshake.
|
||||
|
||||
def close(self): # Закрытие идемпотентно и не порождает рекурсивную ошибку панели.
|
||||
self._closing = True; self.timeout.stop(); self.tcp.abort() # Сначала отменяет deadline и сетевые сигналы старой сессии.
|
||||
if self._shared is not None: channel=self._shared; self._shared=None; channel.close(); self.console_restored=channel.link.console_restored; channel.deleteLater() # Освобождается логический CAN канал, тогда как Modbus продолжает использовать тот же FTDI.
|
||||
if self._console and self.serial.isOpen(): # Дожидается реального ответа UART, чтобы close не отбросил ещё не переданный Ctrl-C в драйвере Windows.
|
||||
self.serial.readAll(); self.serial.write(b'\x03'); self.serial.flush() # Удаляет старый поток статуса и немедленно передаёт сигнал завершения собственного foreground nc.
|
||||
if self.serial.bytesToWrite(): self.serial.waitForBytesWritten(150) # Ограничивает ожидание освобождения локальной выходной очереди.
|
||||
elapsed=QElapsedTimer(); elapsed.start(); response=bytearray() # Подтверждение восстановления shell имеет конечный срок 400 мс.
|
||||
while elapsed.elapsed()<400 and not self.console_restored: # Нельзя закрыть FTDI раньше передачи управляющего байта и возврата root prompt.
|
||||
self.serial.waitForReadyRead(40); response.extend(bytes(self.serial.readAll())); self.console_restored=response.rstrip(b'\r\n').endswith((b'# ',b'$ ')) # Настоящий prompt подтверждает завершение nc и выполнение stty sane.
|
||||
if not self.console_restored: self._error='Закрыт USB, но возврат Linux-консоли за 400 мс не подтверждён' # Не выдаёт простое закрытие COM за подтверждённое восстановление терминала.
|
||||
if self.serial.isOpen(): self.serial.close() # Не вызывает NotOpenError при повторном cleanup уже закрытого USB-порта.
|
||||
self._console=False; self._console_ready=False; self._console_rx.clear(); self._closing = False # Не оставляет старые данные handshake для следующего COM-порта.
|
||||
|
||||
def readAll(self): # Возвращает сырой QByteArray для существующего LineAssembler templates.
|
||||
if self._shared is not None: return self._shared.readAll() # Общий физический поток уже разделён на независимые CAN/Modbus сессии.
|
||||
if self._console: data=bytes(self._console_rx); self._console_rx.clear(); return data # Консольные bytes прочитаны единожды при поиске подтверждённого заголовка.
|
||||
return self.tcp.readAll() if self.kind == 'Ethernet' else self.serial.readAll() # Сегментация TCP/USB не влияет на протокол.
|
||||
|
||||
def write(self, data): # Длина результата позволяет обнаружить непринятую целиком команду.
|
||||
if self._shared is not None: return self._shared.write(data) # Канал кодирует только транспортное обрамление, не меняя исходные CAN команды.
|
||||
return self.tcp.write(data) if self.kind == 'Ethernet' else self.serial.write(data) # Порт не добавляет заголовков либо переводов строк к готовой команде.
|
||||
|
||||
def errorString(self): # Единая диагностика сохраняется до следующего соединения.
|
||||
return self._error or (self.tcp.errorString() if self.kind == 'Ethernet' else self.serial.errorString()) # Сообщение Qt не подменяется общим «нет связи».
|
||||
|
||||
def portName(self): # Нижняя строка монитора показывает фактический канал.
|
||||
if self._shared is not None: return self._shared.portName() # Общий COM остаётся видимым пользователю при одновременной работе двух протоколов.
|
||||
return self._endpoint if self.kind == 'Ethernet' else self.serial.portName() # Сетевое соединение отличается от COM-порта платы.
|
||||
|
||||
def setPortName(self, name): # Штатный USB-путь BoardTab выбирает физический порт без изменения своего кода.
|
||||
self.kind = 'USB'; self.serial.setPortName(name) # Следующие настройки применяются только к закрытому USB-порту.
|
||||
|
||||
def setBaudRate(self, value): return self.serial.setBaudRate(value) # Физический USB-UART требует согласованной скорости, USB CDC принимает line coding.
|
||||
def setDataBits(self, value): return self.serial.setDataBits(value) # Использует 8 бит данных штатной панели.
|
||||
def setParity(self, value): return self.serial.setParity(value) # Сохраняет отсутствие parity в общем контракте 8N1.
|
||||
def setStopBits(self, value): return self.serial.setStopBits(value) # Сохраняет один стоповый бит.
|
||||
def setFlowControl(self, value): return self.serial.setFlowControl(value) # RTS/CTS не задействуются для управления платой.
|
||||
def open(self, mode): return self.serial.open(mode) # Открытие USB выполняется штатным QSerialPort с исходной диагностикой ошибок.
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Общий владелец FTDI и независимые логические TCP-каналы CAN/Modbus поверх Linux shell.""" # Прикладные кодеки не зависят от способа совместного использования COM.
|
||||
from PySide6.QtCore import QObject, Signal, QTimer, QByteArray, QCoreApplication, QElapsedTimer, QIODevice # Один Qt event loop обслуживает все логические каналы без фоновых гонок.
|
||||
from PySide6.QtSerialPort import QSerialPort # Физический COM открывается ровно одним экземпляром менеджера.
|
||||
import shlex # Путь установленного либо явно выбранного тестового helper передаётся shell одним безопасным аргументом.
|
||||
import re # Подтверждённый root prompt отделяет shell от login/getty и запроса пароля после холодной загрузки.
|
||||
class ConsoleChannel(QObject): # Контракт readAll/write/close позволяет подключить существующие thin-порты протоколов.
|
||||
readyRead=Signal(); opened=Signal(); errorOccurred=Signal(object) # OPEN подтверждается loopback службой отдельно от открытия FTDI.
|
||||
def __init__(self,link,number,port,parent): # Канал принадлежит конкретному потребителю, физическая сессия — приложению.
|
||||
super().__init__(parent); self.link=link; self.number=number; self.port=port; self.active=False; self.closed=False; self.incoming=bytearray(); self.error='' # Соседние протоколы имеют независимые буферы и состояния.
|
||||
def isOpen(self): return not self.closed # Ожидающую handshake сессию можно отменить той же кнопкой отключения.
|
||||
def portName(self): return self.link.name # Панель показывает реальный COM, общий для обеих служб платы.
|
||||
def errorString(self): return self.error # Ошибка сохраняется до уничтожения конкретной логической сессии.
|
||||
def readAll(self): data=QByteArray(bytes(self.incoming)); self.incoming.clear(); return data # Каждый прикладной байт читается только своим протоколом.
|
||||
def write(self,data): # Возвращает длину принятых в очередь данных без подтверждения внешней операции.
|
||||
raw=bytes(data) # QByteArray и bytes преобразуются без изменения бинарного содержимого.
|
||||
if self.closed or not self.active: return -1 # Команда не уходит в неподтверждённый или уже закрытый канал.
|
||||
for offset in range(0,len(raw),512): # Короткая строка HEX позволяет чередовать CAN и Modbus при 115200 бод.
|
||||
if not self.link.send(f'D{self.number:02X}:'.encode('ascii')+raw[offset:offset+512].hex().encode('ascii')+b'\n'): return -1 # Переполнение физической очереди прекращает сессию без повтора команды.
|
||||
return len(raw) # Успех означает только локальную постановку в очередь QSerialPort.
|
||||
def close(self): # Закрытие протокола не отбирает общий COM у оставшихся потребителей.
|
||||
if self.closed: return # Идемпотентный cleanup безопасен при синхронных Qt error callbacks.
|
||||
self.closed=True; self.active=False; self.incoming.clear(); self.link.release(self) # Менеджер завершает shell helper только после последнего канала.
|
||||
def failed(self,message): # Ошибка одного backend не маскируется состоянием другого протокола.
|
||||
if self.closed: return # Запоздалое событие не возобновляет отключённую страницу.
|
||||
self.error=message; self.close(); self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # Потребитель очищает собственные незавершённые запросы.
|
||||
class ConsoleLink(QObject): # Один экземпляр на имя COM обслуживает сколько угодно независимых backend сессий.
|
||||
links={} # Реестр процесса сохраняет одного физического владельца между вкладками SETGUI.
|
||||
program='/opt/zynq-link/console_link.py' # Проверка staged-пакета может выбрать его точный локальный путь до постоянной установки helper.
|
||||
@classmethod # Потребитель запрашивает канал, не управляя временем жизни общей UART-консоли.
|
||||
def acquire(cls,name,port,parent=None): # Сетевой адрес не передаётся из UI: разрешены только две локальные службы платы.
|
||||
if int(port) not in (37579,502): raise ValueError('Общий USB поддерживает CAN 37579 и Modbus 502') # Разрешены штатный CAN и стандартный Modbus TCP, без произвольного loopback proxy.
|
||||
name=str(name).strip(); key=name.upper() if name.upper().startswith('COM') else name # Windows COM нечувствителен к регистру, Linux PTY сохраняет точное имя.
|
||||
link=cls.links.get(key) # Повторная вкладка присоединяется к уже открытому физическому каналу.
|
||||
if link is None: link=cls(name,key); cls.links[key]=link; QTimer.singleShot(0,link.start) # Асинхронный запуск даёт потребителю время подключить error/opened callbacks.
|
||||
number=next((value for value in range(1,256) if value not in link.used),None) # Номер не переиспользуется до закрытия физической сессии, поэтому поздний DATA старого клиента не попадёт новому.
|
||||
if number is None: raise RuntimeError('Исчерпаны USB-каналы платы') # Ограничение совпадает с однобайтовым номером общего протокола.
|
||||
channel=ConsoleChannel(link,number,int(port),parent); link.channels[number]=channel; link.used.add(number) # Множество использованных номеров защищает от смешения с поздними queued ответами закрытого backend.
|
||||
if link.ready: QTimer.singleShot(0,lambda:link.open_channel(channel)) # Существующий helper открывает только новый backend без повторной shell команды.
|
||||
return channel # Никакая прикладная команда ещё не отправляется в CAN или Modbus.
|
||||
def __init__(self,name,key): # Общая сессия переживает переключение и закрытие одной рабочей страницы.
|
||||
super().__init__(QCoreApplication.instance()); self.name=name; self.key=key; self.serial=QSerialPort(self); self.channels={}; self.used=set(); self.incoming=bytearray(); self.ready=False; self.closing=False; self.console_restored=True # QObject приложения освобождает менеджер до завершения Qt runtime, а история номеров изолирует поздние DATA.
|
||||
self.timer=QTimer(self); self.timer.setSingleShot(True); self.timer.timeout.connect(lambda:self.fail('Нет ответа общего USB-туннеля: проверьте root shell и /opt/zynq-link/console_link.py')) # Ошибка начального handshake имеет конечный срок 5 секунд.
|
||||
self.serial.readyRead.connect(self.receive); self.serial.errorOccurred.connect(self.serial_error) # Единственный readyRead читает физические байты для всех протоколов.
|
||||
def start(self): # FTDI открывается только после явного запроса первой страницы подключения.
|
||||
if self.closing or not self.channels: return # Закрытый до первого event loop запрос не занимает COM.
|
||||
self.serial.setPortName(self.name); self.serial.setBaudRate(115200); self.serial.setDataBits(QSerialPort.DataBits.Data8); self.serial.setParity(QSerialPort.Parity.NoParity); self.serial.setStopBits(QSerialPort.StopBits.OneStop); self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) # Параметры соответствуют проверенной root-консоли платы, независимо от внешней RS485.
|
||||
if not self.serial.open(QIODevice.OpenModeFlag.ReadWrite): self.fail(self.serial.errorString()); return # Занятый сторонним терминалом COM не перехватывается.
|
||||
self.console_restored=False; self.phase='shell'; self.login_sent=False; self.cursor_replied=False; self.timer.start(5000); self.send(b'\x15\r') # Сначала запрашивается фактический prompt: после холодного boot COM может ещё принадлежать getty, а не shell.
|
||||
def start_helper(self): # Команды tty разрешены только после подтверждения root shell известной платы.
|
||||
self.phase='helper'; self.incoming.clear(); self.send(("stty -echo -icanon -ixon -ixoff min 1 time 0; python3 "+shlex.quote(self.program)+"; stty sane; printf '\\n#ZYNQ-LINK-CLOSED\\n'\r").encode('ascii')) # Путь экранирован одним аргументом, завершение helper восстанавливает обычный терминал.
|
||||
def send(self,data): # Общая очередь Windows ограничена независимо от скорости прикладных клиентов.
|
||||
if self.closing or not self.serial.isOpen(): return False # Закрытый physical fd не накапливает команды для будущего запуска.
|
||||
if self.serial.bytesToWrite()+len(data)>262144 or self.serial.write(data)!=len(data): self.fail('Переполнена очередь общего USB; уменьшите поток CAN/Modbus'); return False # Предел 256 КиБ не допускает бесконечного отставания от UART.
|
||||
return True # Реальное подтверждение операции остаётся обязанностью прикладного протокола.
|
||||
def open_channel(self,channel): # Каждый backend соединяется отдельно внутри уже работающего общего helper.
|
||||
if not channel.closed and self.ready: self.send(f'O{channel.number:02X}:{channel.port}\n'.encode('ascii')) # Номер и порт сформированы из проверенных локальных чисел.
|
||||
def receive(self): # Произвольные USB фрагменты собираются в ограниченные ASCII строки обрамления.
|
||||
if self.closing: return # Во время короткого cleanup принятые байты проверяет finish, избегая рекурсии callback.
|
||||
self.incoming.extend(bytes(self.serial.readAll())) # Физический поток читается ровно одним QObject.
|
||||
if len(self.incoming)>16384: self.fail('Неверный поток общего USB-туннеля'); return # Ни shell, ни повреждённый backend не могут бесконечно накапливать данные.
|
||||
if not self.ready and self.phase=='shell': # Login и prompt не обязаны оканчиваться переводом строки, поэтому проверяются до разбора multiplex строк.
|
||||
if b'Password:' in self.incoming: self.fail('Консоль платы требует пароль: войдите через терминал и повторите подключение'); return # Пароль не угадывается и не отправляется автоматически.
|
||||
if b'\x1b[6n' in self.incoming and not self.cursor_replied: self.cursor_replied=True; self.send(b'\x1b[24;80R') # Factory autologin вызывает resize с ANSI запросом курсора; стандартный ответ 24 строки и 80 столбцов освобождает shell без ввода фиктивной команды.
|
||||
plain=re.sub(rb'\x1b(?:\[[0-?]*[ -/]*[@-~]|[78])',b'',bytes(self.incoming)) # ANSI save/restore cursor и resize не являются частью реального root prompt.
|
||||
if re.search(rb'(?:^|[\r\n])root@image_7020:[^\r\n]*#[ \t]*$',plain): self.start_helper(); return # Только фактическая root shell разрешает запуск общего helper после штатного factory autologin.
|
||||
if re.search(rb'login:[ \t]*$',plain) and not self.login_sent: self.login_sent=True; self.incoming.clear(); self.send(b'root\r') # Известное имя отправляется только getty, действительно ожидающему ввод; строка automatic login не вызывает лишнюю root команду.
|
||||
return # Текст getty не попадает в Modbus/CAN parser и не считается banner helper.
|
||||
while b'\n' in self.incoming: # Одно уведомление может содержать ответы нескольких логических каналов.
|
||||
raw,_,tail=self.incoming.partition(b'\n'); self.incoming[:]=tail; line=raw.rstrip(b'\r') # Терминальные CR не входят в HEX данные прикладных протоколов.
|
||||
if not self.ready: # Вывод shell и echo стартовой команды не становятся Modbus ответом.
|
||||
if line==b'#ZYNQ-LINK 1': self.ready=True; self.timer.stop(); [self.open_channel(channel) for channel in list(self.channels.values())] # Только настоящий banner разрешает OPEN backend каналов.
|
||||
continue # Старый консольный вывод не передаётся ни одному прикладному parser.
|
||||
if len(line)<3: continue # Пустые строки не влияют на состояние готовых каналов.
|
||||
try: number=int(line[1:3],16) # Однобайтовый номер позволяет выбрать единственного получателя.
|
||||
except ValueError: self.fail('Повреждено обрамление общего USB'); return # Ошибка multiplex framing закрывает физическую сессию без повторов.
|
||||
channel=self.channels.get(number) # Закрытый потребитель может оставить поздние байты, которые следует игнорировать.
|
||||
if channel is None: continue # Поздний DATA не создаёт сессию автоматически.
|
||||
if line[:1]==b'O' and len(line)==3: channel.active=True; channel.opened.emit() # Connected относится к установленному loopback соединению конкретной службы.
|
||||
elif line[:1]==b'C' and len(line)==3: channel.failed('Служба платы закрыла USB-канал') # FIN одного сервиса не отключает остальные протоколы.
|
||||
elif line[:1]==b'E' and line[3:4]==b':': channel.failed('Ошибка USB-канала службы: errno '+line[4:].decode('ascii','replace')) # Числовая причина Linux доступна рядом с соответствующей вкладкой.
|
||||
elif line[:1]==b'D' and line[3:4]==b':': # Прикладные данные раскодируются только после выбора активного канала.
|
||||
try: data=bytes.fromhex(line[4:].decode('ascii')) # Все управляющие значения байта остаются неизменными данными TCP потока.
|
||||
except (ValueError,UnicodeError): self.fail('Повреждены HEX данные общего USB'); return # Неисправное обрамление не проходит к Modbus C-кодеку как частичный успех.
|
||||
if len(channel.incoming)+len(data)>65536: channel.failed('Переполнен буфер USB-канала'); continue # Ограничение одного потребителя не мешает работе остальных.
|
||||
channel.incoming.extend(data); channel.readyRead.emit() # Существующий parser получает произвольный фрагмент исходного TCP.
|
||||
else: self.fail('Неизвестный ответ общего USB-туннеля'); return # Неожиданное возвращение shell не выдаётся за действующий протокольный поток.
|
||||
def serial_error(self,error): # Qt NoError не считается отказом физического FTDI.
|
||||
if not self.closing and self.serial.isOpen() and error not in (QSerialPort.SerialPortError.NoError,QSerialPort.SerialPortError.TimeoutError): self.fail(self.serial.errorString()) # Отключение кабеля завершает все каналы одной общей сессии.
|
||||
def release(self,channel): # Канал удаляется до посылки CLOSE, исключая повторную обработку поздних данных.
|
||||
self.channels.pop(channel.number,None) # Повторный cleanup не затрагивает другой номер сессии.
|
||||
if self.ready and not self.closing: self.send(f'C{channel.number:02X}\n'.encode('ascii')) # Backend получает штатный обрыв собственного TCP соединения.
|
||||
if not self.channels: self.finish() # Последняя страница возвращает обычную Linux-консоль и освобождает COM.
|
||||
def fail(self,message): # Физическая ошибка закрывает всех владельцев без автоматического повторного запуска.
|
||||
if self.closing: return # Синхронный QSerialPort error во время close не вызывает рекурсию.
|
||||
channels=list(self.channels.values()); self.closing=True # Сначала запрещаются новые записи всех потребителей.
|
||||
for channel in channels: channel.failed(message) # Каждая вкладка получает собственный cleanup и прекращает опрос.
|
||||
self.finish() # Общий UART освобождается после доставки ошибок всем потребителям.
|
||||
def finish(self): # Только последний потребитель завершает собственный foreground helper.
|
||||
self.closing=True; self.timer.stop() # Остальные Qt callbacks не читают tty во время подтверждения возврата shell.
|
||||
if self.serial.isOpen(): # Отсоединённый физический кабель не заставляет ждать несуществующее подтверждение.
|
||||
self.serial.readAll(); self.serial.write(b'\x03'); self.serial.flush() # ISIG остаётся включённым, поэтому Ctrl-C завершает только собственную foreground группу helper.
|
||||
if self.serial.bytesToWrite(): self.serial.waitForBytesWritten(150) # Очередь передачи ограничена коротким ожиданием фактической отправки Ctrl-C.
|
||||
elapsed=QElapsedTimer(); elapsed.start(); response=bytearray() # Консоль должна подтвердить выполнение stty sane за ограниченные 500 мс.
|
||||
while elapsed.elapsed()<500 and not self.console_restored: self.serial.waitForReadyRead(25); response.extend(bytes(self.serial.readAll())); self.console_restored=b'\n#ZYNQ-LINK-CLOSED\r' in response or b'\n#ZYNQ-LINK-CLOSED\n' in response # Собственный marker отличает реальный возврат shell от простого закрытия COM.
|
||||
self.serial.close() # Сторонний терминал может открыть COM после завершения последнего протокола.
|
||||
if self.links.get(self.key) is self: self.links.pop(self.key) # Новый запрос пользователя создаст новую чистую физическую сессию.
|
||||
self.deleteLater() # Удаление QObject выполняется штатным Qt event loop до выгрузки runtime.
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Асинхронный USB-порт шлюза и локальный TCP endpoint для существующих Modbus-клиентов.""" # Wire-кодек и проверка ответов выполняются только C-ядром templates.
|
||||
from collections import deque # Ограниченная последовательная очередь исключает перемешивание RTU-ответов разных клиентов.
|
||||
import ctypes as ct # Передаёт native-запрос в общую C-проверку до отправки по USB.
|
||||
from PySide6.QtCore import QObject, QIODevice, QTimer, Signal # Жизненный цикл и дедлайны остаются в GUI event loop без фоновых гонок.
|
||||
from PySide6.QtNetwork import QHostAddress, QTcpServer # Loopback связывает существующий Qt Modbus-клиент с единственным владельцем USB.
|
||||
from PySide6.QtSerialPort import QSerialPort # Выделенный USB CDC COM открывается только по действию пользователя.
|
||||
from ..modbus_gateway import GatewayCodec # Python не вычисляет CRC, MBAP, смещения конфигурации и правила корреляции.
|
||||
class ModbusGatewayPort(QObject): # Один экземпляр обслуживает настройки и опрос через один физический USB endpoint.
|
||||
state = Signal(str) # Сообщает текущую фазу подключения и подтверждённые ошибки транспорта.
|
||||
configuration = Signal(object) # Передаёт только прочитанную с Zynq конфигурацию ABI.
|
||||
connected = Signal(bool) # Позволяет UI закрыть встроенный опрос при потере USB.
|
||||
def __init__(self, parent=None, codec=None): # Тест может предоставить библиотеку конкретной проверяемой сборки.
|
||||
super().__init__(parent); self.codec = codec or GatewayCodec(); self.serial = QSerialPort(self); self.server = QTcpServer(self); self.queue = deque(); self.pending = None; self.buffers = {}; self.received = bytearray(); self.config = self.codec.default(); self.ready = False; self.closing = False # Все очереди и callbacks принадлежат одному Qt-потоку.
|
||||
self.direct=self.serial; self.tcp_transport=False # Прямой CDC сохраняет RTU, а общий FTDI канал переносит стандартный Modbus TCP к той же службе.
|
||||
self.timer = QTimer(self); self.timer.setSingleShot(True); self.timer.timeout.connect(lambda: self.fail('Тайм-аут USB; проверьте OTG-порт и службу шлюза на Zynq')) # После тайм-аута сессия закрывается, чтобы поздний RTU-ответ не подтвердил следующий запрос.
|
||||
self.serial.readyRead.connect(self._receive); self.serial.errorOccurred.connect(self._serial_error); self.server.newConnection.connect(self._accept) # Уведомления не запускают блокирующих read/waitForReadyRead.
|
||||
@property # Локальный endpoint назначает ОС после успешного чтения настроек платы.
|
||||
def port(self): return self.server.serverPort() # Нулевой порт до подключения не выдаётся за готовый канал опроса.
|
||||
def open(self, name, console=False): # Прямой CDC либо общий FTDI сначала подтверждают службу и читают настройки без внешних операций.
|
||||
self.close() # Прежняя сессия и её локальные клиенты завершаются до выбора нового физического канала.
|
||||
if console: # Linux host для HDMI сохраняется, а существующий FTDI разделяется с CAN.
|
||||
from .console_link_port import ConsoleLink # Общий менеджер является единственным владельцем COM для обеих страниц.
|
||||
channel=ConsoleLink.acquire(name,502,self); self.serial=channel; self.tcp_transport=True; channel.readyRead.connect(self._receive); channel.opened.connect(self.read_configuration); channel.errorOccurred.connect(lambda error:self.fail(channel.errorString()) if not self.closing else None) # Чтение конфигурации начинается только после OPEN стандартного Modbus TCP listener той же службы платы.
|
||||
self.state.emit('Общий USB FTDI: подключение к Modbus-службе Zynq…'); return True # Ожидающую сессию можно отменить, не закрывая соседний CAN.
|
||||
self.serial.setPortName(name); self.serial.setBaudRate(115200); self.serial.setDataBits(QSerialPort.DataBits.Data8); self.serial.setParity(QSerialPort.Parity.NoParity); self.serial.setStopBits(QSerialPort.StopBits.OneStop); self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) # Параметры прямого CDC не зависят от физической линии RS485.
|
||||
if not self.serial.open(QIODevice.OpenModeFlag.ReadWrite): self.state.emit(self.serial.errorString()); return False # Занятый CAN-монитором COM не перехватывается и не закрывается принудительно.
|
||||
self.serial.clear(); self.serial.setDataTerminalReady(True); self.state.emit('USB открыт; чтение настроек Zynq…'); self.read_configuration(); return True # DTR объявляет gadget готовность host к передаче данных.
|
||||
def close(self): # Вызывается при закрытии общей вкладки, ошибке и ручном отключении.
|
||||
self.closing = True; self.timer.stop(); self.server.close(); sockets = list(self.buffers); self.buffers.clear(); self.queue.clear(); self.pending = None; self.received.clear(); self.ready = False # Ни один старый запрос не переносится в следующую сессию USB.
|
||||
for socket in sockets: socket.abort(); socket.deleteLater() # Loopback-клиенты немедленно получают потерю соединения и прекращают опрос.
|
||||
if self.serial.isOpen(): self.serial.close() # Физический COM освобождается для других приложений.
|
||||
if self.tcp_transport: self.serial.deleteLater(); self.serial=self.direct; self.tcp_transport=False # Только логический FTDI канал возвращается к прямому порту; тестовый либо другой прямой I/O сохраняет свой контракт.
|
||||
self.closing = False; self.connected.emit(False) # UI отключает рабочие действия после очистки транспорта.
|
||||
def fail(self, message): # Ошибка не провоцирует повтор внешней команды записи.
|
||||
if self.closing: return # Синхронные serial error callbacks во время close не вызывают рекурсию.
|
||||
self.close(); self.state.emit(message) # Пользователь явно восстанавливает соединение после неопределённого результата записи.
|
||||
def _invalid_response(self,reason): # Диагностические bytes сохраняются до закрытия сессии, очищающего буфер.
|
||||
preview=bytes(self.received[:32]).hex(' ').upper(); suffix=' …' if len(self.received)>32 else ''; message=f'{self.serial.portName()}: {reason}. Получено {len(self.received)} байт; HEX: {preview}{suffix}.' # Предел 32 байта сохраняет полезное начало ответа без неограниченного роста сообщения GUI.
|
||||
if not self.ready and not self.tcp_transport: message+=' Для штатного FTDI включите «Linux-консоль FTDI»; прямой CDC/UART требует выделенной службы шлюза.' # Единый образ не отправляет бинарный RTU в консоль Linux при правильно выбранном общем транспорте.
|
||||
self.fail(message) # Строгая проверка ответа по-прежнему закрывает сессию без повтора внешней операции.
|
||||
def _serial_error(self, error): # Ошибки NoError и ожидания не закрывают исправный endpoint.
|
||||
if error not in (QSerialPort.SerialPortError.NoError, QSerialPort.SerialPortError.TimeoutError) and self.serial.isOpen(): self.fail(self.serial.errorString()) # Отсоединение USB прекращает и настройки, и встроенный опрос.
|
||||
def read_configuration(self): self._enqueue(self.codec.control(), None) # Чтение настроек не посылает запросы внешним устройствам.
|
||||
def apply_configuration(self, config): self._enqueue(self.codec.control(config), None, config) # Полная таблица отправляется одной атомарной командой управления.
|
||||
def _enqueue(self, request, socket, candidate=None): # Один незавершённый RTU запрос сохраняет однозначность ответа без Transaction ID.
|
||||
if not self.serial.isOpen(): self.state.emit('Сначала подключите USB Zynq'); return # Закрытый COM не накапливает команды на будущий запуск.
|
||||
if len(self.queue) >= 32: self.fail('Переполнена очередь Modbus; уменьшите частоту опроса'); return # Ограничивает память при слишком быстром loopback-клиенте.
|
||||
self.queue.append((request, socket, candidate)); self._next() # Кандидат настроек нужен только для достаточного дедлайна применения.
|
||||
def _next(self): # Следующая команда отправляется только после полного проверенного ответа предыдущей.
|
||||
if self.pending is not None or not self.queue: return # Исключает перемешивание данных опроса с чтением конфигурации.
|
||||
self.pending = self.queue.popleft(); request, socket, candidate = self.pending # Неподтверждённая запись остаётся ровно одной записью в очереди.
|
||||
if socket is not None and socket not in self.buffers: self.pending = None; self._next(); return # Запрос закрытого клиента удаляется до обращения к внешней шине.
|
||||
wire = self.codec.tcp_reply(request,bytes(request.pdu[:request.length])) if self.tcp_transport else self.codec.rtu(request); self.received.clear() # Общий FTDI передаёт MBAP, прямой CDC — RTU; оба формата собирает один native-кодек.
|
||||
if not wire or self.serial.write(wire) != len(wire): self.fail('Не удалось передать запрос по USB'); return # Ошибка локальной очереди записи не приводит к автоматическому повтору.
|
||||
timeout = max(self.config.timeout_ms, candidate.timeout_ms if candidate is not None else 0); self.timer.start(max(5000, timeout + 1500)) # Учитывает внешний тайм-аут платы, USB и небольшую очередь обслуживания.
|
||||
def _receive(self): # Границы USB transfer не считаются границами Modbus RTU ADU.
|
||||
chunk = bytes(self.serial.readAll()) # Читает уже доступные байты без ожидания.
|
||||
if self.pending is None: return # Посторонние байты вне транзакции не становятся ответом будущего запроса.
|
||||
self.received.extend(chunk); size = (self.codec.lib.smg_tcp_size if self.tcp_transport else self.codec.lib.smg_rtu_response_size)(bytes(self.received), len(self.received)) # Границы MBAP либо RTU, включая частную FC65, вычисляет только C-ядро.
|
||||
if size < 0 or len(self.received) > (260 if self.tcp_transport else 256) or (size > 0 and len(self.received) > size): self._invalid_response('Неверный формат или длина ответа USB Zynq'); return # Сохраняет диагностические bytes установленной панели при строгих native границах RTU/MBAP.
|
||||
if not size or len(self.received) < size: return # Незавершённый ADU остаётся в ограниченном буфере до следующего readyRead.
|
||||
request, socket, candidate = self.pending # Ответ принадлежит единственному исходящему запросу.
|
||||
try: pdu = self.codec.accept(request, self.received, tcp=self.tcp_transport) # Native проверка CRC/MBAP и корреляции обязательна для обоих вариантов физического USB.
|
||||
except ValueError as error: self._invalid_response(str(error)); return # Ошибки CRC, адреса и MBAP получают ту же диагностику физического канала и фактических bytes.
|
||||
self.timer.stop(); self.pending = None; self.received.clear() # Освобождает транзакцию только после проверки полного ответа.
|
||||
if socket is None: # Управление шлюзом обрабатывается отдельно от обычных регистров устройств.
|
||||
try: config = self.codec.read_config(pdu) # Отказ ARM при настройке UART остаётся видимой ошибкой применения.
|
||||
except ValueError as error: self.state.emit(str(error)); self._next(); return # Прежняя подтверждённая конфигурация остаётся действующей.
|
||||
self.config = config # Тайм-аут и маршруты подтверждены работающей службой платы.
|
||||
if not self.ready: # Локальный endpoint открывается после handshake с правильной службой, а не после простого открытия COM.
|
||||
if not self.server.listen(QHostAddress.SpecialAddress.LocalHost, 0): self.fail(self.server.errorString()); return # ОС выделяет свободный порт без конфликтов с другими приложениями.
|
||||
self.ready = True; self.connected.emit(True) # Встроенная панель может подключиться к готовому каналу опроса.
|
||||
self.configuration.emit(config); self.state.emit('USB Zynq подключён; настройки подтверждены платой') # UI не выдаёт локальный черновик за применённую таблицу.
|
||||
elif socket in self.buffers: socket.write(self.codec.tcp_reply(request, pdu)) # Исходные Transaction ID и Unit ID возвращаются клиенту через C builder.
|
||||
self._next() # Продолжает очередь после доставки ответа его владельцу.
|
||||
def _accept(self): # Loopback принимает только локальные Modbus-клиенты процесса или ПК.
|
||||
while self.server.hasPendingConnections(): # Несколько ожидающих подключений обрабатываются за одно уведомление Qt.
|
||||
socket = self.server.nextPendingConnection() # QObject TCP-сокета остаётся дочерним серверу.
|
||||
if len(self.buffers) >= 8: socket.abort(); socket.deleteLater(); continue # Лимит совпадает с числом TCP-клиентов ARM-службы.
|
||||
self.buffers[socket] = bytearray(); socket.readyRead.connect(lambda s=socket: self._tcp_read(s)); socket.disconnected.connect(lambda s=socket: self._tcp_closed(s)) # Каждая сессия имеет свой MBAP stream buffer.
|
||||
def _tcp_closed(self, socket): self.buffers.pop(socket, None); socket.deleteLater() # Запросы закрытого клиента удаляются из очереди перед отправкой по USB.
|
||||
def _tcp_read(self, socket): # Qt Modbus использует существующий TCP backend, сохраняя единый UI опроса.
|
||||
if socket not in self.buffers: return # Позднее queued notification после disconnect не обращается к удалённому буферу.
|
||||
buffer = self.buffers[socket]; buffer.extend(bytes(socket.readAll())) # Склеенные и частичные TCP ADU сохраняются без потери байтов.
|
||||
if len(buffer) > 1040: socket.abort(); return # Четыре максимальных ADU ограничивают память недоверенного локального клиента.
|
||||
while buffer: # Один read может содержать несколько законченных Modbus-запросов.
|
||||
size = self.codec.lib.smg_tcp_size(bytes(buffer), len(buffer)) # Границы MBAP проверяет тот же код, что ARM-служба.
|
||||
if size < 0: socket.abort(); return # Неизвестный Protocol ID и длина прекращают только эту TCP-сессию.
|
||||
if not size or len(buffer) < size: return # Неполный заголовок не считается ошибкой и ждёт следующий пакет.
|
||||
request = self.codec.decode_tcp(buffer[:size]); del buffer[:size]; error = self.codec.lib.smg_check(ct.byref(request)) # Неподдерживаемая функция либо ошибочный размер PDU не превращаются в неклассифицируемый USB stream.
|
||||
if error: socket.write(self.codec.tcp_reply(request,self.codec.exception(request,error))) # Возвращает стандартное исключение без какой-либо внешней операции.
|
||||
else: self._enqueue(request, socket) # Внешний запрос остаётся единственным, даже когда входной TCP разбит или склеен.
|
||||
@@ -79,6 +79,7 @@ class SlcanAdapter(QObject):
|
||||
frame_received = Signal(int, bytes)
|
||||
capture_received = Signal(int, bytes, bool, bool)
|
||||
connection_lost = Signal(str)
|
||||
protocol_error = Signal(str) # Сообщает явный отказ BEL вне настройки; принятие байтов COM не подтверждает выполнение команды адаптером.
|
||||
fd_received = Signal(int, bytes, bool, int, int) # Отдельный маршрут FD исключает ошибочный разбор кадра как Classic CAN.
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
@@ -222,6 +223,8 @@ class SlcanAdapter(QObject):
|
||||
if separator == 7: # ASCII BEL имеет код 7 и сообщает об отклонении последней команды.
|
||||
if self._waiting_command and self._command_reply is None: # Принимаем только первый ответ текущего этапа настройки.
|
||||
self._command_reply = False # Сохраняем отказ для проверки в последовательном обмене командами.
|
||||
elif not self._waiting_command and self._query_prefix is None: # Отказ диагностического запроса не приписывается передаче CAN; установку режима обслуживает собственное ожидание.
|
||||
self.protocol_error.emit("SLCAN: адаптер вернул отказ BEL; проверьте состояние CAN и очередь передачи") # BEL не содержит идентификатора кадра и причины, поэтому не выдаём его за доказательство отсутствия CAN ACK.
|
||||
continue # BEL не должен приклеиваться к следующему CAN-кадру.
|
||||
if self.options.fd and raw[:1] in (b"d", b"D", b"b", b"B") and self._receive_fd(raw): # Только корректная FD-строка забирается отдельным маршрутом; версия CANable может начинаться с HEX-буквы.
|
||||
continue # Даже FD с данными до восьми байтов не меняет тип при передаче подписчикам.
|
||||
@@ -286,7 +289,7 @@ class SlcanAdapter(QObject):
|
||||
lines = [] # Ответы V и N читают сведения, F дополнительно сбрасывает индикатор ошибки согласно Lawicel.
|
||||
canable = self.options.profile != "lawicel" # CANable возвращает V без буквенного префикса, а регистр E иногда без завершающего CR.
|
||||
if canable: # Отсутствие подтверждения настроек сообщается независимо от успешного чтения версии.
|
||||
lines.append(f"Профиль {self.options.profile}: команды конфигурации без ACK/NACK; применение настроек не подтверждается прошивкой.") # Диагностика не превращает ответ V в подтверждение предыдущих M/A/Y.
|
||||
lines.append(f"Профиль {self.options.profile}: команды настройки по COM без ответов CR/BEL; это не аппаратное подтверждение ACK на шине CAN.") # Отделяет отсутствие ответов протокола SLCAN от аппаратного ACK, который активный CAN-контроллер формирует самостоятельно.
|
||||
for command in (("V", "E") if canable else ("V", "N", "F")): # Набор фиксирован спецификацией выбранной прошивки.
|
||||
self._read() # Сначала выдаём уже накопленные CAN-кадры, не смешивая их с ответом запроса.
|
||||
self._query_prefix, self._query_reply = (b"" if canable else command.encode("ascii")), None # CANable не использует префикс V в строке версии.
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Асинхронный J-Link-порт: backup, запись, readback, проверка и отдельный reset/run.""" # Внешний процесс не блокирует цикл событий SETGUI.
|
||||
from pathlib import Path # Артефакты одной операции хранятся вместе в отдельном каталоге.
|
||||
import re # Итоговый запуск подтверждается выполненной командой g, а не одним кодом возврата.
|
||||
import tempfile # Уникальный каталог исключает перезапись предыдущей резервной копии.
|
||||
try: # Общая библиотека работает с современным Qt и существующими сборками Windows 7.
|
||||
from PySide6.QtCore import QObject, QProcess, QTimer, Signal # QProcess обслуживает stdout, завершение и отмену без потоковой блокировки.
|
||||
except ImportError: # PySide2 сохраняет тот же публичный контракт сигналов этого порта.
|
||||
from PySide2.QtCore import QObject, QProcess, QTimer, Signal # Никакой зависимости от внутреннего устройства SETGUI здесь нет.
|
||||
from set_devices.stm32_jtag import encode_hex, programming_script, start_script, verify_readback # Вся подготовка и проверка сосредоточены в переносимом модуле templates.
|
||||
|
||||
|
||||
class Stm32JtagProgrammer(QObject): # Владеет одной аппаратной операцией до полного завершения обоих этапов.
|
||||
progress = Signal(int, str) # Основная форма показывает этапы рядом с существующей полосой прогресса.
|
||||
finished = Signal(bool, str) # Единственный итог освобождает форму также при ошибке запуска executable.
|
||||
log = Signal(str) # Полный вывод Commander доступен в терминале и сохраняется на диск.
|
||||
|
||||
def __init__(self, parent=None): # Создание порта не открывает программатор и не обращается к USB.
|
||||
super().__init__(parent) # Время жизни порта соответствует его Qt-владельцу.
|
||||
self._process = None # None означает отсутствие запущенного Commander.
|
||||
self._active = False # Флаг защищает от повторного finished после errorOccurred и finished одного процесса.
|
||||
self._timer = QTimer(self) # Ограничивает зависшую внешнюю утилиту без ожидания в GUI.
|
||||
self._timer.setSingleShot(True) # На один этап приходится один предельный тайм-аут.
|
||||
self._timer.timeout.connect(lambda: self._stop('Истекло время ожидания J-Link; проверьте журнал')) # Остановка не объявляет частично записанный образ успешным.
|
||||
|
||||
@property # Форма может проверить занятость перед повторным нажатием или закрытием.
|
||||
def active(self): # Состояние охватывает программирование, независимую проверку и reset/run.
|
||||
return self._active # Между двумя процессами нельзя начать конкурирующую запись.
|
||||
|
||||
def start(self, executable, options, segments, backup_root): # Принимает уже проверенный неизменяемый план, подтверждённый пользователем.
|
||||
if self._active: raise ValueError('J-Link уже выполняет операцию') # Второй запуск не подменяет исходный путь backup и цель MCU.
|
||||
options.validate() # Защищает прямые вызовы порта вне стандартной формы.
|
||||
self._options, self._segments, self._executable = options, segments, str(executable) # Копирует параметры текущей операции до запуска QProcess.
|
||||
self._active, self._stop_reason = True, '' # Обозначает владение программатором и отсутствие запроса отмены.
|
||||
try: # Ошибки каталога и снимка должны завершать операцию штатным сигналом формы.
|
||||
root = Path(backup_root).resolve() # Выбранный каталог не зависит от текущей рабочей папки процесса GUI.
|
||||
root.mkdir(parents=True, exist_ok=True) # Создаёт только папку артефактов пользователя, без установки инструментов.
|
||||
self.folder = Path(tempfile.mkdtemp(prefix='stm32-jtag-', dir=root)) # Предыдущие backup остаются нетронутыми даже при повторной неудачной попытке.
|
||||
(self.folder / 'image.hex').write_text(encode_hex(segments), encoding='utf-8') # Фиксирует проверенные адресованные данные, исключая изменение исходника во время записи.
|
||||
self.log.emit(f'Артефакты J-Link и резервная копия: {self.folder}') # Путь известен оператору до первого аппаратного действия.
|
||||
self.progress.emit(0, 'Резервная копия, запись и чтение Flash') # Число отражает этап, поскольку Commander сам выводит ход программирования в терминал.
|
||||
self._launch('program', programming_script(self.folder, options)) # Сначала исполняется только этап с остановленным ядром и двумя SaveBin.
|
||||
except (OSError, ValueError) as error: self._finish(False, str(error)) # Отказ файловой системы не оставляет интерфейс заблокированным.
|
||||
|
||||
def _launch(self, stage, commands): # Каждый этап использует новый QProcess и собственный проверяемый command file.
|
||||
self._stage, self._output = stage, '' # Вывод текущего этапа нужен также для подтверждения факта команды запуска.
|
||||
script = self.folder / (stage + '.jlink') # Точный выполненный сценарий сохраняется рядом с backup.
|
||||
script.write_text(commands, encoding='utf-8') # UTF-8 без BOM совместим с установленным Commander.
|
||||
process = QProcess(self) # QObject-владелец удерживает процесс до обработки сигнала finished.
|
||||
self._process = process # Отмена и тайм-аут относятся только к текущему этапу.
|
||||
process.setProcessChannelMode(QProcess.MergedChannels) # Ошибки stderr сохраняются в том же порядке, что служебные сообщения stdout.
|
||||
process.readyReadStandardOutput.connect(self._read_output) # Строки отображаются по мере поступления без waitForFinished.
|
||||
process.errorOccurred.connect(self._process_error) # Отсутствующий executable и невозможность запуска дают понятный итог.
|
||||
process.finished.connect(self._process_finished) # Только штатное завершение запускает независимую проверку и следующий этап.
|
||||
self._timer.start(300000) # Пять минут на этап покрывают медленный JTAG; GUI продолжает обрабатывать отмену.
|
||||
process.start(self._executable, self._options.arguments() + ['-Log', str(self.folder / (stage + '-dll.log')), '-CommanderScript', str(script)]) # Путь, серийный номер и файл передаются отдельными аргументами без командной оболочки.
|
||||
|
||||
def _read_output(self): # Вызывается также при finished, чтобы не потерять последний фрагмент stdout.
|
||||
if self._process is None: return # Сигнал уже освобождённого этапа не должен обращаться к новому процессу.
|
||||
chunk = bytes(self._process.readAllStandardOutput()).decode('utf-8', errors='replace').replace('\b', '') # Управляющие backspace процентов Commander не ломают текстовый терминал.
|
||||
self._output += chunk # Полный вывод сохраняется для анализа результата и диагностики ошибки.
|
||||
if chunk: self.log.emit(chunk.rstrip()) # Отображает сообщения сразу, включая отсутствие питания и занятой J-Link.
|
||||
|
||||
def _process_error(self, error): # FailedToStart не сопровождается finished и требует самостоятельного завершения.
|
||||
if self._active and error == QProcess.FailedToStart: self._finish(False, 'Не удалось запустить J-Link: ' + self._process.errorString()) # Не оставляет активный таймер после неверного пути executable.
|
||||
|
||||
def _process_finished(self, code, status): # Последовательно выполняет readback-проверку и отдельный reset/run.
|
||||
if not self._active: return # Игнорирует повторное уведомление ранее завершённой ошибки.
|
||||
self._timer.stop() # Проверка локальных файлов не должна срабатывать как тайм-аут уже завершённого Commander.
|
||||
self._read_output() # Забирает остаток вывода перед сохранением журнала.
|
||||
try: # Все ошибки проверки и записи журнала превращаются в один finished(False).
|
||||
(self.folder / (self._stage + '.log')).write_text(self._output, encoding='utf-8') # Читаемый журнал содержит именно тот вывод, который видел оператор.
|
||||
process, self._process = self._process, None # Старый QObject больше не считается владельцем текущего этапа.
|
||||
process.deleteLater() # Удаление выполняется циклом Qt после выхода из обработчика finished.
|
||||
if self._stop_reason: raise ValueError(self._stop_reason) # Отмена никогда не переходит к командам reset/run.
|
||||
if code != 0 or status != QProcess.NormalExit: raise ValueError(f'J-Link завершился с ошибкой {code}; журнал: {self.folder}') # Успех требует нормального завершения внешней утилиты.
|
||||
if self._stage == 'program': # Даже код ноль не подтверждает, что интерактивное подключение реально дошло до записи.
|
||||
before, after = (self.folder / 'before.bin').read_bytes(), (self.folder / 'after.bin').read_bytes() # Отсутствующий SaveBin автоматически исключает ложный успех.
|
||||
verify_readback(self._segments, self._options, before, after) # Проверяет реальные байты записанных диапазонов и защищённую страницу UKSVEP.
|
||||
self.progress.emit(90, 'Flash проверена, сброс и запуск STM32') # Переход к запуску разрешён только после независимой проверки обоих снимков.
|
||||
self._launch('run', start_script(self._options)) # Новый Commander больше не стирает и не программирует Flash.
|
||||
else: # Второй этап должен дойти до явного возобновления исполнения ядра.
|
||||
if not re.search(r'J-Link>\s*g\s*(?:\r?\n|$)', self._output): raise ValueError('J-Link не подтвердил выполнение команды запуска; проверьте журнал') # Преждевременный выход с кодом ноль не превращается в сообщение о работающем MCU.
|
||||
self.progress.emit(100, 'Запись и проверка завершены, выполнен reset/run') # Проценты завершаются только после обоих этапов.
|
||||
self._finish(True, f'STM32 прошит через {self._options.interface}; Flash проверена, reset/run выполнен. Backup: {self.folder / "before.bin"}') # Итог содержит доступную для восстановления резервную копию.
|
||||
except (OSError, ValueError) as error: self._finish(False, str(error)) # При отказе проверки ядро не запускается новым этапом.
|
||||
|
||||
def cancel(self): # Вызывается штатной кнопкой отмены формы прошивки.
|
||||
self._stop('J-Link: операция отменена; частичная Flash требует повторной прошивки, резервная копия сохранена в каталоге операции') # Не обещает целый образ после прерывания аппаратной записи.
|
||||
|
||||
def _stop(self, reason): # Общая остановка обслуживает кнопку отмены и предельный тайм-аут.
|
||||
if not self._active: return # Повторная отмена завершённого порта ничего не меняет.
|
||||
self._stop_reason = reason # finished процесса должен сохранить причину вместо сообщения об обычном ненулевом коде.
|
||||
if self._process is not None: self._process.kill() # Останавливает только запущенный этим портом Commander, не затрагивая Keil и другие программаторы.
|
||||
else: self._finish(False, reason) # Ошибка между этапами также освобождает форму без дополнительного процесса.
|
||||
|
||||
def _finish(self, success, message): # Единственная точка завершения защищает форму от двойных уведомлений.
|
||||
if not self._active: return # Второй сигнал ошибки того же QProcess не открывает повторное окно.
|
||||
self._active = False # Новая операция разрешается только после окончательного результата текущей.
|
||||
self._timer.stop() # Поздний тайм-аут не должен отменить следующий пользовательский запуск.
|
||||
if self._process is not None: self._process.deleteLater() # FailedToStart не использует общий обработчик finished.
|
||||
self._process = None # Освобождает ссылку на завершённый QObject.
|
||||
self.finished.emit(success, message) # Владелец разблокирует форму и покажет итог стандартным механизмом SETGUI.
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Общие настройки STM32/J-Link для встраивания в существующий экран прошивки.""" # SETGUI предоставляет лишь выбранный образ и свой терминал.
|
||||
from pathlib import Path # Проверяет путь внешнего инструмента до подтверждения аппаратной операции.
|
||||
try: # Поддерживает обе используемые SETGUI ветви Qt без зависимости от gui_desktop.
|
||||
from PySide6.QtCore import QCoreApplication, QSettings, QStandardPaths, Signal # Настройки и каталог backup принадлежат текущему приложению.
|
||||
from PySide6.QtWidgets import QWidget, QFormLayout, QHBoxLayout, QVBoxLayout, QLineEdit, QComboBox, QSpinBox, QCheckBox, QPushButton, QFileDialog, QLabel # Использует обычные виджеты, доступные в Qt 5 и Qt 6.
|
||||
except ImportError: # Старые Windows-сборки сохраняют тот же публичный интерфейс панели.
|
||||
from PySide2.QtCore import QCoreApplication, QSettings, QStandardPaths, Signal # Не навязывает основному приложению версию Qt.
|
||||
from PySide2.QtWidgets import QWidget, QFormLayout, QHBoxLayout, QVBoxLayout, QLineEdit, QComboBox, QSpinBox, QCheckBox, QPushButton, QFileDialog, QLabel # Набор контролов одинаков для обеих реализаций.
|
||||
from set_devices.stm32_jtag import JLinkOptions, find_jlink, prepare_image # Общий план выполняет все проверки MCU, адресов и manifest до записи.
|
||||
from .stm32_jtag import Stm32JtagProgrammer # Порт QProcess отвечает за журнал, отмену и оба этапа операции.
|
||||
|
||||
|
||||
class Stm32JtagPanel(QWidget): # Встраивается в FirmwareTab вместо UART/CAN-настроек выбранного режима.
|
||||
progress = Signal(int, str) # Перенаправляет этапы в полосу прогресса основной формы.
|
||||
finished = Signal(bool, str) # Результат обрабатывается тем же методом, что остальные загрузчики.
|
||||
log = Signal(str) # Вывод J-Link направляется в существующий терминал прошивки.
|
||||
|
||||
def __init__(self, parent=None, settings=None): # Явное хранилище позволяет изолировать настройки в тестах и других приложениях.
|
||||
super().__init__(parent) # Панель следует обычному времени жизни владельца QWidget.
|
||||
self.settings = settings if settings is not None else QSettings() # Не подменяет тестовый профиль глобальным пользовательским реестром.
|
||||
self.programmer = Stm32JtagProgrammer(self) # Создание формы само по себе не подключает USB и не запускает Commander.
|
||||
self.programmer.progress.connect(self.progress.emit) # Прогресс проходит через общий публичный интерфейс панели.
|
||||
self.programmer.finished.connect(self.finished.emit) # Ошибки и успех освобождают внешнюю форму одинаковым способом.
|
||||
self.programmer.log.connect(self.log.emit) # Терминал получает и служебные строки, и сообщения SEGGER.
|
||||
if QCoreApplication.instance() is not None: QCoreApplication.instance().aboutToQuit.connect(self.programmer.cancel) # Закрытие приложения не оставляет скрытый процесс программирования без владельца.
|
||||
root = QVBoxLayout(self) # Все настройки образуют одну колонку внутри существующей левой панели.
|
||||
root.setContentsMargins(0, 0, 0, 0) # Внешние отступы уже заданы формой прошивки.
|
||||
hint = QLabel('J-Link через JTAG или SWD. Доступ к BOOT0 не требуется. Перед записью сохраняется полная резервная копия Flash.') # Оператор сразу видит назначение режима и способ первого программирования.
|
||||
hint.setWordWrap(True) # Описание помещается в узкую колонку рядом с терминалом.
|
||||
root.addWidget(hint) # Пояснение располагается над параметрами подключения.
|
||||
form = QFormLayout() # Подписи не смешиваются с UART-полями других загрузчиков.
|
||||
self.executable = QLineEdit(str(self._value('executable', find_jlink()))) # Позволяет использовать SEGGER из Keil либо отдельную установленную версию.
|
||||
executable_row = QHBoxLayout() # Кнопка выбора остаётся рядом с полным путём инструмента.
|
||||
executable_row.addWidget(self.executable) # Введённый путь передаётся QProcess отдельным аргументом.
|
||||
executable_button = QPushButton('Обзор…') # Поиск установленного инструмента не требует скачивания и установки.
|
||||
executable_button.clicked.connect(self._choose_executable) # Диалог выбирает только путь, без запуска программатора.
|
||||
executable_row.addWidget(executable_button) # Кнопка сохраняет естественную компактную ширину.
|
||||
form.addRow('J-Link Commander', executable_row) # Имя executable явно отделено от названия MCU.
|
||||
self.device = QComboBox() # Стандартные имена подходят базе устройств установленного Commander.
|
||||
self.device.setEditable(True) # Другой STM32 можно указать вручную вместе с фактическим объёмом Flash.
|
||||
self.device.addItems(['STM32F103RC', 'STM32F103C8', 'STM32F103ZE', 'STM32F407VE', 'STM32G474VE']) # Список охватывает используемые проектами templates семейства.
|
||||
self.device.setCurrentText(str(self._value('device', 'STM32F103RC'))) # По умолчанию выбирается текущая плата, ранее проверенная через JTAG.
|
||||
form.addRow('Микроконтроллер', self.device) # Полное имя цели видно до подтверждения записи.
|
||||
self.interface = QComboBox() # Интерфейс не зависит от UART-скорости верхней панели SETGUI.
|
||||
self.interface.addItems(['JTAG', 'SWD']) # Оба варианта используют J-Link, а не встроенный ROM Boot.
|
||||
self.interface.setCurrentText(str(self._value('interface', 'JTAG'))) # Исходное подключение текущей платы выполнено по JTAG.
|
||||
form.addRow('Интерфейс', self.interface) # Пользователь видит выбранный физический способ подключения.
|
||||
self.serial = QLineEdit(str(self._value('serial', ''))) # Не закрепляет серийный номер чужого программатора в общей библиотеке.
|
||||
self.serial.setPlaceholderText('Необязательно, если подключён один J-Link') # Несколько программаторов можно однозначно различить по серийному номеру.
|
||||
form.addRow('Серийный номер J-Link', self.serial) # Поле соответствует штатному параметру Commander -USB.
|
||||
self.speed = QSpinBox() # Частота задаётся числом с явными единицами.
|
||||
self.speed.setRange(1, 50000) # Допускает медленную плату и быстрые поддерживаемые подключения.
|
||||
self.speed.setValue(int(self._value('speed', 1000))) # Проверенная начальная частота 1000 кГц подходит текущему JTAG.
|
||||
self.speed.setSuffix(' кГц') # Исключает смешение TCK/SWCLK со скоростью COM в бодах.
|
||||
form.addRow('Частота JTAG/SWD', self.speed) # Значение передаётся напрямую в -speed.
|
||||
self.flash_size = QSpinBox() # Полный объём нужен для сохранения Flash перед любым программированием.
|
||||
self.flash_size.setRange(16, 4096) # Интервал покрывает поддерживаемый переносимым планом объём 16 КиБ..4 МиБ.
|
||||
self.flash_size.setValue(int(self._value('flash_size', 256))) # У STM32F103RC установлены 256 КиБ внутренней Flash.
|
||||
self.flash_size.setSuffix(' КиБ') # Длина SaveBin вычисляется из этого значения в байтах.
|
||||
form.addRow('Объём Flash', self.flash_size) # Для другого MCU оператор должен указать его фактический объём.
|
||||
self.uksvep = QCheckBox('UKSVEP F103RC: сохранить параметры и обновить CRC приложения') # Явный профиль разрешает специфические адреса manifest и регистров BKP.
|
||||
self.uksvep.setChecked(self.settings.value('firmware/stm32-jtag/uksvep', True, type=bool)) # Выбор сохраняется независимо от общего имени MCU.
|
||||
self.uksvep.setToolTip('Для другой платы снимите этот флажок. UKSVEP принимает полный app с 0x08008000 или установочный HEX boot+app.') # Объясняет действие профиля при осмысленном выборе оператором.
|
||||
form.addRow('Профиль платы', self.uksvep) # Обычные STM32 не наследуют защиту и служебную запись UKSVEP автоматически.
|
||||
default_backup = str(Path(QStandardPaths.writableLocation(QStandardPaths.AppLocalDataLocation)) / 'stm32-jtag') # Каталог остаётся записываемым также у установленного в Program Files приложения.
|
||||
self.backup_root = QLineEdit(str(self._value('backup_root', default_backup))) # Каждая операция создаёт внутри отдельную подпапку с образами и журналами.
|
||||
backup_row = QHBoxLayout() # Настройка архива не требует ручного редактирования путей Windows.
|
||||
backup_row.addWidget(self.backup_root) # Полный путь остаётся доступным для копирования оператором.
|
||||
backup_button = QPushButton('Обзор…') # Выбирает существующий базовый каталог резервных копий.
|
||||
backup_button.clicked.connect(self._choose_backup) # Диалог не изменяет Flash и настройки программатора.
|
||||
backup_row.addWidget(backup_button) # Действие располагается у соответствующего пути.
|
||||
form.addRow('Резервные копии', backup_row) # Backup включён всегда и не прячется в настройках UART.
|
||||
root.addLayout(form) # Готовая панель встраивается без собственной кнопки прошивки и повторной полосы прогресса.
|
||||
|
||||
def _value(self, name, default): # Все ключи принадлежат отдельному стабильному разделу прошивки J-Link.
|
||||
return self.settings.value('firmware/stm32-jtag/' + name, default) # Прежние UART/CAN-настройки не меняются.
|
||||
|
||||
def _choose_executable(self): # Пользователь явно указывает уже установленный J-Link Commander.
|
||||
path, _ = QFileDialog.getOpenFileName(self, 'J-Link Commander', self.executable.text(), 'J-Link (JLink.exe JLinkExe);;Все файлы (*)') # Расширение не ограничивает Linux-версию SEGGER.
|
||||
if path: self.executable.setText(path) # Отмена диалога сохраняет текущий путь.
|
||||
|
||||
def _choose_backup(self): # Позволяет хранить backup вне каталога установки GUI и исходников.
|
||||
path = QFileDialog.getExistingDirectory(self, 'Каталог резервных копий STM32', self.backup_root.text()) # Выбор каталога не запускает аппаратную операцию.
|
||||
if path: self.backup_root.setText(path) # Прежний архив остаётся выбранным при отмене диалога.
|
||||
|
||||
def prepare(self, image): # Основная форма вызывает проверку до стандартного подтверждения записи.
|
||||
executable = Path(self.executable.text().strip()) # Путь не интерпретируется как команда оболочки.
|
||||
if not executable.is_file(): raise ValueError('Выберите установленный J-Link Commander (JLink.exe)') # Отказывает до блокировки формы и открытия программатора.
|
||||
if not self.backup_root.text().strip(): raise ValueError('Выберите каталог резервных копий Flash') # Не сохраняет обязательный backup в непредсказуемый текущий каталог.
|
||||
options = JLinkOptions(self.device.currentText().strip(), self.interface.currentText(), self.speed.value(), self.serial.text().strip(), self.flash_size.value(), self.uksvep.isChecked()) # Фиксирует все параметры одновременно с неизменяемым образом.
|
||||
return str(executable), options, prepare_image(image, options), self.backup_root.text().strip() # Возвращает точный план для единственного последующего подтверждённого запуска.
|
||||
|
||||
def start(self, job): # Вызывается только после существующего подтверждения прошивки в FirmwareTab.
|
||||
for name, widget in (('executable', self.executable), ('serial', self.serial), ('backup_root', self.backup_root)): self.settings.setValue('firmware/stm32-jtag/' + name, widget.text().strip()) # Сохраняет пути и идентификатор без доступа к остальным настройкам GUI.
|
||||
for name, value in (('device', self.device.currentText()), ('interface', self.interface.currentText()), ('speed', self.speed.value()), ('flash_size', self.flash_size.value()), ('uksvep', self.uksvep.isChecked())): self.settings.setValue('firmware/stm32-jtag/' + name, value) # Следующий запуск формы восстанавливает фактическое последнее подключение.
|
||||
self.programmer.start(*job) # Процесс получает уже проверенный снимок, а не повторно читает изменяемые поля формы.
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Подготовка и независимая проверка J-Link-прошивки STM32 без зависимости от Qt.""" # Один план используют SETGUI и консольные клиенты templates.
|
||||
from dataclasses import dataclass # Неизменяемые настройки фиксируют назначение до первой записи Flash.
|
||||
from pathlib import Path # Пути передаются программатору как данные, без выполнения оболочкой.
|
||||
import os # Стандартные каталоги Windows позволяют найти уже установленный J-Link.
|
||||
import re # Ограничивает поля, попадающие в аргументы J-Link Commander.
|
||||
import shutil # Поиск PATH сохраняет возможность использовать установленный SEGGER вне Keil.
|
||||
import struct # Manifest UKSVEP содержит шесть little-endian слов uint32.
|
||||
import zlib # CRC32 IEEE совпадает с валидатором production-загрузчика UKSVEP.
|
||||
from firmware_image.core import HexSegment, parse_intel_hex, contiguous_image # Проверка формата и checksum использует каноническое C-ядро templates.
|
||||
from .mcu_profiles import UKSVEP_F103RC_BOOT # Адрес приложения, SRAM и Product берутся из общего профиля платы.
|
||||
|
||||
FLASH_BASE = 0x08000000 # Внутренняя Flash STM32 отображается с этого адреса в карте памяти Cortex-M.
|
||||
UKS_APP = UKSVEP_F103RC_BOOT['app_base'] # Начальные 32 КиБ UKSVEP занимает постоянный загрузчик.
|
||||
UKS_PARAMS = UKS_APP + UKSVEP_F103RC_BOOT['app_size'] # Следующая страница 0x0801F800 хранит параметры и исключается из записи.
|
||||
UKS_META = 0x0803F800 # Последняя страница 256-КиБ Flash содержит production-manifest, см. boot_profile.h UKSVEP.
|
||||
|
||||
|
||||
@dataclass(frozen=True) # Значения нельзя незаметно изменить между подтверждением формы и запуском процесса.
|
||||
class JLinkOptions: # Описывает конкретный STM32 и физическое подключение программатора.
|
||||
device: str = 'STM32F103RC' # Имя совпадает с базой устройств J-Link; корпусный суффикс T6 не требуется.
|
||||
interface: str = 'JTAG' # SWD использует тот же Commander и выбирается пользователем отдельно.
|
||||
speed_khz: int = 1000 # Частота TCK/SWCLK задаётся в кГц и не связана со скоростью UART.
|
||||
serial: str = '' # Пустое значение разрешает штатный выбор единственного подключённого J-Link.
|
||||
flash_kib: int = 256 # Размер всей Flash задаёт длину обязательных backup и readback.
|
||||
uksvep: bool = True # Только явно выбранный профиль UKSVEP включает защиту параметров и manifest.
|
||||
|
||||
def validate(self): # Отказ происходит до создания command file и доступа к плате.
|
||||
if not re.fullmatch(r'STM32[A-Za-z0-9_]+', self.device): raise ValueError('Укажите имя STM32 из базы J-Link') # Не допускает пробелы и команды вместо идентификатора MCU.
|
||||
if self.interface not in ('JTAG', 'SWD'): raise ValueError('Выберите JTAG или SWD') # Остальные интерфейсы не относятся к этой панели.
|
||||
if not 1 <= self.speed_khz <= 50000: raise ValueError('Частота JTAG/SWD должна быть 1..50000 кГц') # Верхняя граница соответствует доступному диапазону формы.
|
||||
if self.serial and not re.fullmatch(r'[0-9]+', self.serial): raise ValueError('Серийный номер J-Link должен содержать только цифры') # Идентификатор не должен включать дополнительные аргументы.
|
||||
if not 16 <= self.flash_kib <= 4096: raise ValueError('Размер Flash должен быть 16..4096 КиБ') # Ограничивает аппаратное чтение выбранным пользователем объёмом STM32.
|
||||
if self.uksvep and (self.device != 'STM32F103RC' or self.flash_kib != 256): raise ValueError('Профиль UKSVEP требует STM32F103RC и Flash 256 КиБ') # Не применяет карту F103RC и регистры BKP к другому семейству.
|
||||
|
||||
def arguments(self): # Возвращает отдельные аргументы QProcess без shell=True и строковой сборки команды.
|
||||
self.validate() # Публичный метод безопасен также вне GUI.
|
||||
args = ['-device', self.device, '-if', self.interface, '-JTAGConf', '-1,-1', '-speed', str(self.speed_khz), '-autoconnect', '1', '-ExitOnError', '1', '-NoGui', '1'] # Явная цепочка исключает интерактивный JTAGConf, преждевременно завершавший batch с кодом ноль.
|
||||
return (['-USB', self.serial] if self.serial else []) + args # Серийный номер закрепляет конкретный программатор при нескольких подключениях.
|
||||
|
||||
|
||||
def find_jlink(): # Обнаруживает существующий инструмент и ничего не скачивает в систему.
|
||||
found = shutil.which('JLink.exe') or shutil.which('JLinkExe') # PATH может содержать также одноимённый сборщик модулей Java JDK.
|
||||
candidates = ['C:/Keil_v5_41/ARM/Segger/JLink.exe', 'C:/Keil_v5/ARM/Segger/JLink.exe'] # Проверенные каталоги SEGGER в Keil имеют приоритет над неоднозначным PATH.
|
||||
for variable in ('ProgramFiles', 'ProgramFiles(x86)'): # SEGGER может быть установлен в 32- или 64-разрядный каталог Windows.
|
||||
if os.environ.get(variable): candidates.append(str(Path(os.environ[variable]) / 'SEGGER/JLink/JLink.exe')) # Не читает реестр и не требует прав администратора.
|
||||
if found and not any((Path(found).parent / name).is_file() for name in ('java.exe', 'java', 'jmod.exe', 'jmod')): candidates.append(found) # Инструмент из bin Java JDK не является программатором SEGGER и не должен выбираться автоматически.
|
||||
return next((str(Path(item)) for item in candidates if item and Path(item).is_file()), '') # Пустой результат предлагает явный выбор executable в форме.
|
||||
|
||||
|
||||
def prepare_image(image, options): # Проверяет выбранный ранее FirmwareImage и сохраняет разреженную адресацию HEX.
|
||||
options.validate() # Настройки проверяются до чтения внешних файлов и доступа к программатору.
|
||||
path = Path(image.path) # Образ может прийти как из формы, так и из Публикатора.
|
||||
if path.stat().st_size > 16 * 1024 * 1024: raise ValueError('Файл прошивки превышает 16 МиБ') # Ограничение покрывает 4 МиБ данных Intel HEX с текстовыми накладными расходами.
|
||||
raw = path.read_bytes() # Читает единственный снимок исходника, который затем сравнивается с подтверждённым выбором.
|
||||
if path.suffix.lower() == '.hex': # HEX уже содержит абсолютные адреса всех отдельных диапазонов Flash.
|
||||
flat, base = contiguous_image(raw, options.flash_kib * 1024) # Канонический parser проверяет checksum, пересечения и полный адресный размах.
|
||||
if flat != image.data or base != image.base_address: raise ValueError('Файл или адрес HEX изменён после выбора; выберите образ заново') # Не записывает новый файл под видом ранее выбранного образа и не смещает HEX.
|
||||
segments = parse_intel_hex(raw) # Разрывы остаются отсутствующими записями, а не превращаются в байты FF для loadfile.
|
||||
elif path.suffix.lower() == '.bin': # BIN требует явно указанного адреса, поскольку сам его не хранит.
|
||||
if raw != image.data: raise ValueError('BIN изменён после выбора; выберите образ заново') # Не использует устаревший SHA-256 формы для другого содержимого.
|
||||
segments = [HexSegment(image.base_address, raw)] # Адрес из формы относится к началу полезных байтов BIN.
|
||||
else: raise ValueError('Для J-Link выберите Intel HEX или BIN') # OUT/ELF не обходят единый проверяемый план записи Flash.
|
||||
if not segments or any(not part.data or part.start < FLASH_BASE or part.end >= FLASH_BASE + options.flash_kib * 1024 for part in segments): raise ValueError('Образ выходит за выбранный объём Flash STM32') # Не допускает запись RAM, option bytes или соседней памяти.
|
||||
if options.uksvep: # Защита и manifest относятся только к конкретному порту UKSVEP.
|
||||
if any(part.start < UKS_PARAMS + 2048 and part.end >= UKS_PARAMS for part in segments): raise ValueError('Образ затрагивает страницу параметров UKSVEP 0x0801F800') # Размер страницы F103RC равен 2048 байтам по профилю boot.
|
||||
app = [part for part in segments if part.start < UKS_PARAMS and part.end >= UKS_APP] # Находит приложение без включения загрузчика и служебной страницы.
|
||||
if app: # Запись одного app HEX также должна обновить CRC, иначе постоянный boot не разрешит запуск.
|
||||
if len(app) != 1 or app[0].start != UKS_APP or app[0].end >= UKS_PARAMS: raise ValueError('Для UKSVEP нужен полный непрерывный образ приложения с 0x08008000') # Не выдаёт фрагментарный патч или смешанный BIN за полное приложение.
|
||||
data = app[0].data # CRC считается по полезной длине приложения, без промежутков Flash.
|
||||
if len(data) < 8: raise ValueError('В приложении отсутствует таблица векторов Cortex-M3') # Два первых слова содержат MSP и Reset_Handler.
|
||||
sp, pc = struct.unpack_from('<II', data) # Векторы Cortex-M3 хранятся в little-endian.
|
||||
if sp & 7 or not 0x20000000 < sp <= UKSVEP_F103RC_BOOT['stack_end'] or not pc & 1 or not UKS_APP <= (pc & ~1) < UKS_APP + len(data): raise ValueError('Неверные векторы приложения UKSVEP') # Требует SRAM 48 КиБ, выравнивание стека и Thumb-вход внутрь образа.
|
||||
crc = zlib.crc32(data) & 0xFFFFFFFF # IEEE CRC32 совпадает с uks_boot_installed_valid() production-порта.
|
||||
manifest = struct.pack('<6I', 0x554B5356, len(data), crc, ~len(data) & 0xFFFFFFFF, ~crc & 0xFFFFFFFF, UKSVEP_F103RC_BOOT['product']) # Magic и шесть слов заданы UKS_META_MAGIC/UKS_META_WORDS в boot_port.c.
|
||||
existing = [part for part in segments if part.start < UKS_META + 2048 and part.end >= UKS_META] # Проверяет также явный manifest установочного HEX.
|
||||
if existing and (len(existing) != 1 or existing[0].start != UKS_META or existing[0].data != manifest): raise ValueError('Manifest UKSVEP не соответствует приложению') # Не скрывает неверную служебную запись готового комплекта.
|
||||
if not existing: segments.append(HexSegment(UKS_META, manifest)) # Для app-only JTAG формирует ту же запись активации, что UART/CAN boot.
|
||||
return tuple(sorted(segments, key=lambda item: item.start)) # Неизменяемый отсортированный снимок используется для loadfile и последующего readback.
|
||||
|
||||
|
||||
def encode_hex(segments): # Сохраняет только фактические диапазоны плана без заполнения защищённых разрывов.
|
||||
lines, upper = [], None # Старшие шестнадцать бит записываются при каждом переходе между областями 64 КиБ.
|
||||
def record(address, kind, data): # Стандартная строка Intel HEX содержит длину, адрес, тип, данные и дополнение суммы до нуля.
|
||||
body = bytes((len(data), address >> 8, address & 255, kind)) + data # Адрес внутри записи ограничен текущим окном 64 КиБ.
|
||||
return ':' + (body + bytes((-sum(body) & 255,))).hex().upper() # Проверяемая checksum делает снимок пригодным для независимого J-Link parser.
|
||||
for part in segments: # Каждый диапазон начинает адресованные данные без добавления промежутков.
|
||||
offset = 0 # Положение выражено в байтах текущего сегмента.
|
||||
while offset < len(part.data): # Не создаёт пустых записей в конце последнего блока.
|
||||
address = part.start + offset # Вычисляет абсолютный адрес следующей порции.
|
||||
if address >> 16 != upper: # Type 04 обновляет старшую половину линейного адреса.
|
||||
upper = address >> 16 # Последующие data-записи используют именно это окно.
|
||||
lines.append(record(0, 4, upper.to_bytes(2, 'big'))) # Порядок байтов type 04 задан Intel HEX и не зависит от Cortex-M.
|
||||
count = min(16, len(part.data) - offset, 0x10000 - (address & 0xFFFF)) # Строка не пересекает границу окна даже при невыровненном начале.
|
||||
lines.append(record(address & 0xFFFF, 0, part.data[offset:offset + count])) # Добавляет только реальные байты исходного диапазона.
|
||||
offset += count # Следующая строка продолжает непрерывные данные данного сегмента.
|
||||
return '\n'.join(lines + [record(0, 1, b'')]) + '\n' # EOF завершает UTF-8/ASCII-снимок с обычными переводами строк.
|
||||
|
||||
|
||||
def quote_path(path): # Экранирование J-Link command file отличается от аргументов системного процесса.
|
||||
value = Path(path).resolve().as_posix() # Абсолютные пути исключают зависимость backup от рабочего каталога.
|
||||
if any(char in value for char in ('"', '\r', '\n')): raise ValueError('Путь J-Link не должен содержать кавычки или переносы строк') # Не позволяет имени каталога стать дополнительной командой Commander.
|
||||
return '"' + value + '"' # Пробелы и кириллица остаются частью одного имени файла.
|
||||
|
||||
|
||||
def programming_script(folder, options): # Первый этап сохраняет backup и readback, оставляя ядро остановленным до независимой проверки.
|
||||
folder = Path(folder) # Все артефакты принадлежат отдельному каталогу одной операции.
|
||||
length = hex(options.flash_kib * 1024) # SaveBin принимает длину полного Flash в байтах.
|
||||
return '\n'.join(['ExitOnError 1', 'r', 'h', f'savebin {quote_path(folder / "before.bin")} 0x08000000 {length}', f'loadfile {quote_path(folder / "image.hex")}', f'savebin {quote_path(folder / "after.bin")} 0x08000000 {length}', 'q', '']) # Полного erase нет; loadfile программирует разреженный проверенный HEX с собственной Verify.
|
||||
|
||||
|
||||
def verify_readback(segments, options, before, after): # Проверяет аппаратные снимки независимо от кода возврата Commander.
|
||||
if len(before) != options.flash_kib * 1024 or len(after) != len(before): raise ValueError('J-Link не сохранил полный backup/readback Flash; запуск отменён') # Нулевой код интерактивно прерванного Commander не считается успехом.
|
||||
for part in segments: # Каждая реально записываемая область должна совпасть побайтно.
|
||||
offset = part.start - FLASH_BASE # Смещение внутри полного аппаратного снимка.
|
||||
if after[offset:offset + len(part.data)] != part.data: raise ValueError(f'Ошибка проверки Flash по адресу 0x{part.start:08X}; запуск отменён') # Не запускает MCU после неполного программирования.
|
||||
if options.uksvep: # Проверяет сохранность старых настроек именно там, где их хранит UKSVEP.
|
||||
offset = UKS_PARAMS - FLASH_BASE # Страница параметров находится между boot/app и manifest.
|
||||
if before[offset:offset + 2048] != after[offset:offset + 2048]: raise ValueError('Изменилась страница параметров UKSVEP; запуск отменён') # Проверка касается реальной Flash, а не только отсутствия области в HEX.
|
||||
|
||||
|
||||
def start_script(options): # Второй этап выполняется только после успешной проверки readback на хосте.
|
||||
commands = ['ExitOnError 1', 'r'] # Сброс снимает состояние RAM-алгоритма Flash, оставшееся после loadfile.
|
||||
if options.uksvep: commands += ['w4 0x4002101C 0x18000000', 'w4 0x40007000 0x00000100', 'w4 0x40006C24 0x00000000', 'w4 0x40006C28 0x00000000'] # F103RC: включение PWR/BKP, DBP bit8 и очистка DR9/DR10 убирают старый одноразовый запрос boot без стирания резервного домена.
|
||||
return '\n'.join(commands + ['g', 'q', '']) # Возобновляет выполнение после reset и освобождает программатор без повторной записи Flash.
|
||||
@@ -13,13 +13,14 @@ def decode_gas167(frame, node):
|
||||
result.protocol = "УМП %d / ProtoCAN GAS 167/v1" % node
|
||||
if not frame.ide or frame.rtr or frame.is_error:
|
||||
return result
|
||||
if result.id.msg_type not in (pc.MsgType.GENERAL_ADDRESS_SPACE,
|
||||
pc.MsgType.MODBUS_HOLDING):
|
||||
if result.id.msg_type not in (pc.MsgType.GENERAL_ADDRESS_SPACE, pc.MsgType.MODBUS): # Текущий ProtoCAN использует единый тип Modbus 0x5; отдельного MODBUS_HOLDING больше нет.
|
||||
return result
|
||||
if result.id.msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE:
|
||||
start = result.id.body
|
||||
else:
|
||||
start, _count = pc.split_modbus(result.id.body)
|
||||
start, _count, function = pc.split_modbus(result.id.body) # Современный Body возвращает также функцию, определяющую банк регистров.
|
||||
if function != pc.ModbusFunction.HOLDING: # Ручная карта GAS 167/v1 относится к прежнему банку Holding, а не к дискретам или входным регистрам Modbus.
|
||||
return result # Остальные банки сохраняют штатную расшифровку ProtoCAN без ошибочной подстановки адресов карты УМП.
|
||||
words = result.registers
|
||||
if words is None:
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Эталонные обмены SCI ROM и TI Flash Kernel A без настоящей платы.""" # Проверки не открывают COM и не вызывают внешнюю программу C2Prog.
|
||||
import struct # Формирует независимые тестовые SCI8-векторы с явно заданными словами и адресами.
|
||||
import pytest # Проверяет отказ при повреждениях и граничные размеры порций ядра TI.
|
||||
from protocan.c2000_sci import Session, decode, validate, ulitco_enter # Испытывает настоящий ctypes-порт общего C99-ядра.
|
||||
|
||||
|
||||
def sci8(address, words): # Готовит минимальную адресную таблицу для эталонных данных теста.
|
||||
pointer = struct.pack("<HH", address >> 16, address & 65535) # SCI8 хранит старшее слово адреса перед младшим.
|
||||
return b"\xaa\x08" + bytes(16) + pointer + struct.pack("<H", len(words)) + pointer + struct.pack("<" + "H" * len(words), *words) + bytes(2) # Вектор содержит ключ, резерв, entry, одну секцию и терминатор.
|
||||
|
||||
|
||||
@pytest.mark.parametrize("words", [1, 1024, 1025, 2048, 2050]) # Границы проверяют обычную порцию, остаток и дополнительный нулевой ACK официального TI kernel.
|
||||
def test_complete_echo_and_checksum_transcript(words): # Фактические ответы подтверждают порядок и неизменность всего приложения.
|
||||
kernel, image = sci8(0x9000, [0x1234, 0x5678]), sci8(0x328000, [0xA55A] * words) # RAM-ядро и Flash занимают разные разрешённые области.
|
||||
session, received, pauses, zero_acks = Session(kernel, image), bytearray(), [], 0 # Наблюдения не используют внутреннюю структуру C-контекста.
|
||||
for _ in range(len(kernel) + words + 100): # Конечный предел выявляет зацикливание автомата.
|
||||
state, tx, expected, delay = session.next() # Проверяет настоящий сформированный C-пакет.
|
||||
if state == 0: break # Успех допустим лишь после отправки нулевого терминатора приложения.
|
||||
if state == 2: pauses.append(delay); continue # Внешний транспорт выполняет только заданную алгоритмом задержку.
|
||||
if expected == 1: session.accept(tx) # ROM и autobaud должны вернуть точное эхо отправленного октета.
|
||||
else: # Kernel A подтверждает сумму октетов после erase/program, без побайтового эха приложения.
|
||||
received.extend(tx) # Снимок подтверждает отсутствие пропущенных, повторённых либо изменённых октетов SCI8.
|
||||
if expected: session.accept(struct.pack("<H", sum(tx) & 65535)) # Независимый эталон суммы следует опубликованному исходнику TI SendCheckSum.
|
||||
if not tx: zero_acks += 1 # Пустой остаток TI подтверждается отдельным нулевым словом.
|
||||
else: pytest.fail("Автомат SCI не завершился") # Отсутствие терминального перехода считается ошибкой протокола.
|
||||
assert bytes(received) == image # MCU получил ровно исходный Flash SCI8 без переименования или подмены содержимого.
|
||||
assert pauses == [3000, 3000] and session.progress == 100 # Порядок ожиданий совпадает с TI f05_DownloadImage.
|
||||
assert zero_acks == int(words > 1024 and words % 1024 == 0) # Обрабатывает особенность исходного TI Kernel A на кратной порции.
|
||||
|
||||
|
||||
def test_corrupt_echo_and_checksum_stop_transfer(): # Повреждённое подтверждение не должно разрешать следующий пакет.
|
||||
kernel, image = sci8(0x9000, [1]), sci8(0x328000, [2]) # Минимальные векторы отделяют проверку ответа от больших файлов.
|
||||
session = Session(kernel, image); session.next() # Первый пакет требует эхо ASCII A.
|
||||
with pytest.raises(ValueError, match="не совпали"): session.accept(b"B") # Чужой байт autobaud не принимается как связь с ROM.
|
||||
session = Session(kernel, image) # Независимый сеанс проверяет повреждение двухоктетной суммы Flash.
|
||||
while True: # Доходит до первой операции erase/header без использования внутренних фаз автомата.
|
||||
state, tx, expected, _ = session.next() # Паузы не требуют аппаратного ответа.
|
||||
if state == 2: continue # Номер стадии остаётся внутри C-ядра.
|
||||
if expected == 1: session.accept(tx) # Корректный ROM позволяет загрузить RAM-ядро.
|
||||
else: break # Первый двухоктетный ответ должен подтвердить заголовок приложения.
|
||||
with pytest.raises(ValueError, match="не совпали"): session.accept(b"\xff\xff") # Неверная сумма не разрешает программирование следующей секции.
|
||||
|
||||
|
||||
def test_ascii_decode_memory_ranges_and_monitor_command(): # Проверяет переносимость форматов и аппаратные границы до открытия порта.
|
||||
image = sci8(0x328000, [0x1234]) # Обычный Flash-вектор не пересекает CSM.
|
||||
ascii_image = b"\x02\n" + " ".join(f"{value:02X}" for value in image).encode("ascii") + b"\n\x03\n" # Формат соответствует выводу TI hex2000 -boot -sci8 -a.
|
||||
assert decode(ascii_image) == image and validate(image) == (0x328000, 4) # Сектор C имеет маску 0x0004 в TI Flash API.
|
||||
with pytest.raises(ValueError): decode(ascii_image[:-2]) # Усечённый ETX не должен оставить частично декодированное ядро.
|
||||
with pytest.raises(ValueError): validate(sci8(0x33FFF8, [0x1234])) # Область CSM-паролей запрещена независимо от имени файла.
|
||||
with pytest.raises(ValueError): validate(sci8(0x8000, [0x1234])) # RAM-образ не может стать Flash-приложением из-за выбранного loader.
|
||||
assert ulitco_enter() == bytes([247, 213, 255, 93, 0, 0, 1, 0, 93, 1]) # Эталон совпадает с десятиоктетным Monitor-запросом FLASH_NUM=93 текущей прошивки UlitCo.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user