14 Commits
ump-v4 ... main

Author SHA1 Message Date
0c4eab4615 Добавить общий конструктор CAN-кадров для Qt 2026-10-17 14:43:13 +03:00
5af8a8e9c8 Добавить общую Qt-консоль с фильтрами и экспортом 2026-10-17 14:43:12 +03:00
f613101124 Перезапускать импульс светодиода при каждом приёме пакета 2026-10-17 14:43:12 +03:00
cf65439400 Добавить выпуск и экспорт Flash-образов C2000 2026-10-17 14:43:11 +03:00
32e2fbedc0 Поддержать компактные записи логгера УМП v4
Разрешить 12-словные записи в запросах RTU и CAN. Распознавать статус v4 и декодировать оставшиеся сигналы с сохранением поддержки архивов v2/v3.
2026-10-12 12:25:52 +03:00
226c2ca599 chore(merge): объедини pysetterminal с main
Сохрани актуальный генератор firmware identity с экспортом всей папки сборки и временем UTC+3 при объединении веток.
2026-10-09 11:04:32 +03:00
f2956b54cd merge(deif): объедини Android-компоненты с main 2026-10-09 10:55:08 +03:00
eab8dc5e0a fix(firmware-build): support CCS artifact paths and bootstrap identity config 2026-10-08 17:57:04 +03:00
131f306131 fix(firmware-info): используй UTC+3 в именах папок
Переведи только время имени архива в UTC+3, сохрани UTC в метаданных и заголовке. Проверь переход на следующий день и копирование текущей папки.
2026-10-08 17:05:04 +03:00
4fc0ad4e14 feat(firmware-info): копируй папку текущей сборки целиком
Добавь каталог назначения последним аргументом postcompile CCS и Keil. Копируй только архив текущего запуска с сохранением имени папки, всех файлов и проверкой SHA-256. Обнови MD-команды и проверки выбора папки, вложенных файлов и ошибок экспорта.
2026-10-08 16:26:21 +03:00
da055c2787 feat(firmware-info): архивируй сборки по хешу и дате
Создай в postcompile CCS и Keil папку по commit исходников и UTC-времени precompile. Сохрани точные образы, manifest, SHA-256 и метаданные, сохраняя прежние выходные файлы. Опиши копирование каталогов в SETGUI и проверь разные даты, целостность и Git-метаданные.
2026-10-08 15:31:13 +03:00
8fc2e1b5cf fix(firmware-info): подключи Keil к JSON-схеме версии
Добавь prepare/seal для Keil через общий identity.py, образец конфигурации и команды Before/After Build. Для новой схемы не требуется отдельная release-конфигурация CMD или SETGUI. Проверь HEX/BIN, SHA-256, аргументы и пути с пробелами.
2026-10-08 15:13:34 +03:00
b5b2d1ae37 feat(firmware-info): добавь pre/post-build для CCS и Keil
Подключи общий генератор версии и SCI8-конвертацию через templates. Сохрани метаданные и SHA-256 готового BIN, совместимость с Python 3.8 и прежний SETGUI preflight. Опиши настройку CCS и локальное копирование результатов.
2026-10-08 14:58:27 +03:00
7dfa8bd94d Добавить общие компоненты DEIF для Android и интерактивные приборы
Реализовать SAM/DAM, циклическую передачу, heartbeat, привязки и диагностику. Перенести рисунок и управление MASTER/SLAVE из SETGUI, добавить полноэкранный режим и проверки совместимости кадров.
2026-10-04 23:32:49 +03:00
135 changed files with 3335 additions and 7484 deletions

View File

@@ -9,5 +9,5 @@ if(NOT WIN32)
endif()
add_library(candle SHARED candle.c candle_ctrl_req.c candle.def)
target_compile_definitions(candle PRIVATE UNICODE _UNICODE)
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32 cfgmgr32) # Configuration Manager сопоставляет интерфейсы CAN и DFU по родительскому USB-узлу.
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32)
target_include_directories(candle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -34,86 +34,6 @@
#include "candle_defs.h"
#include "candle_ctrl_req.h"
#include "ch_9.h"
#include <cfgmgr32.h> /* Сопоставляет интерфейсы CAN и DFU по общему родительскому USB-устройству Windows. */
static DWORD candle_dfu_parent(const wchar_t *path, DEVINST *parent) /* Возвращает родителя интерфейса, чтобы команда не попала в другой подключённый адаптер. */
{ /* Начало поиска физического USB-устройства по пути выбранного CAN-интерфейса. */
HDEVINFO devices = SetupDiCreateDeviceInfoList(NULL, NULL); /* Создаёт временный набор SetupAPI без перечисления посторонних устройств. */
if (devices == INVALID_HANDLE_VALUE) return GetLastError(); /* Сохраняет причину отказа создания набора Windows. */
SP_DEVICE_INTERFACE_DATA iface = {0}; /* Подготавливает описание интерфейса для открытия по точному пути. */
iface.cbSize = sizeof(iface); /* Передаёт SetupAPI размер структуры текущей разрядности процесса. */
SP_DEVINFO_DATA info = {0}; /* Получает идентификатор узла PnP выбранного интерфейса. */
info.cbSize = sizeof(info); /* Указывает размер структуры устройства согласно контракту SetupAPI. */
DWORD size = 0, error = ERROR_SUCCESS; /* Хранит требуемое число байтов и итоговый код Windows. */
if (!SetupDiOpenDeviceInterfaceW(devices, path, 0, &iface)) error = GetLastError(); /* Открывает только заданный путь, сохраняя ошибку исчезнувшего устройства. */
if (!error) SetupDiGetDeviceInterfaceDetailW(devices, &iface, NULL, 0, &size, NULL); /* Узнаёт объём буфера переменной длины для пути USB-интерфейса. */
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail = size ? malloc(size) : NULL; /* Выделяет буфер только после успешного определения его размера. */
if (!error && !detail) error = size ? ERROR_NOT_ENOUGH_MEMORY : GetLastError(); /* Отличает нехватку памяти от ошибки поиска описания. */
if (detail) { /* Обрабатывает описание только при наличии выделенного буфера. */
detail->cbSize = sizeof(*detail); /* Согласует размер заголовка с архитектурой Win32 или Win64. */
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, size, NULL, &info)) error = GetLastError(); /* Получает узел интерфейса из SetupAPI. */
else if (CM_Get_Parent(parent, info.DevInst, 0) != CR_SUCCESS) error = ERROR_NOT_FOUND; /* Получает общий родительский узел составного USB-адаптера. */
free(detail); /* Освобождает описание после получения идентификатора родителя. */
} /* Завершает работу с описанием интерфейса. */
SetupDiDestroyDeviceInfoList(devices); /* Освобождает набор SetupAPI при любом результате поиска. */
return error; /* Возвращает ноль при успехе либо причину отказа Windows. */
} /* Завершает сопоставление интерфейса с физическим USB-устройством. */
uint32_t __stdcall candle_dev_enter_dfu(const wchar_t *can_path, uint8_t status[6]) /* Возвращает код Windows и шестибайтовый DFU_GETSTATUS согласно DFU 1.1. */
{ /* Начало отдельного сеанса управляющего интерфейса обновления. */
if (!can_path || !status) return ERROR_INVALID_PARAMETER; /* Запрещает отсутствие точного пути адаптера или выходного буфера статуса. */
memset(status, 0, 6); /* Исключает использование неинициализированных полей ответа при ошибке USB. */
DEVINST wanted_parent = 0; /* Запоминает физический адаптер, выбранный пользователем до остановки CAN. */
DWORD error = candle_dfu_parent(can_path, &wanted_parent); /* Ищет родителя по сохранённому пути CAN, не заменяя его первым найденным устройством. */
if (error) return error; /* Прерывает переход, если выбранный адаптер уже исчез из PnP. */
GUID guid = {0xc25b4308, 0x04d3, 0x11e6, {0xb3, 0xea, 0x60, 0x57, 0x18, 0x9e, 0x64, 0x43}}; /* GUID интерфейса обновления из дескриптора CANable 2.5. */
HDEVINFO devices = SetupDiGetClassDevsW(&guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); /* Перечисляет только подключённые интерфейсы обновления CANable 2.5. */
if (devices == INVALID_HANDLE_VALUE) return GetLastError(); /* Возвращает отказ SetupAPI без попытки послать команду. */
error = ERROR_NOT_FOUND; /* Отсутствие интерфейса обновления не считается успешным входом в DFU. */
for (DWORD index = 0; ; ++index) { /* Проверяет интерфейсы до точного совпадения физического устройства. */
SP_DEVICE_INTERFACE_DATA iface = {0}; /* Создаёт независимое описание очередного DFU-интерфейса. */
iface.cbSize = sizeof(iface); /* Задаёт обязательный размер структуры SetupAPI. */
if (!SetupDiEnumDeviceInterfaces(devices, NULL, &guid, index, &iface)) { /* Завершает обход либо сообщает ошибку перечисления. */
if (GetLastError() != ERROR_NO_MORE_ITEMS) error = GetLastError(); /* Отличает штатный конец списка от отказа Windows. */
break; /* Выходит из обхода после исчерпания интерфейсов или ошибки. */
} /* Завершает проверку результата перечисления. */
DWORD size = 0; /* Хранит размер описания очередного интерфейса в байтах. */
SetupDiGetDeviceInterfaceDetailW(devices, &iface, NULL, 0, &size, NULL); /* Определяет буфер для полного пути интерфейса обновления. */
if (!size) { error = GetLastError(); break; } /* Не продолжает работу с неполным результатом SetupAPI. */
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail = malloc(size); /* Выделяет описание с достаточным местом для USB-пути. */
if (!detail) { error = ERROR_NOT_ENOUGH_MEMORY; break; } /* Не допускает обращения к отсутствующему буферу. */
detail->cbSize = sizeof(*detail); /* Сохраняет ABI описания интерфейса Windows. */
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, size, NULL, NULL)) { /* Получает путь, который можно передать CreateFileW. */
error = GetLastError(); free(detail); break; /* Сохраняет ошибку и освобождает буфер перед выходом. */
} /* Завершает чтение пути интерфейса. */
DEVINST parent = 0; /* Хранит родителя текущего кандидата на обновление. */
if (candle_dfu_parent(detail->DevicePath, &parent) || parent != wanted_parent) { /* Исключает каждый интерфейс другого физического адаптера. */
free(detail); continue; /* Освобождает описание неподходящего кандидата и продолжает поиск. */
} /* Завершает проверку принадлежности выбранному USB-адаптеру. */
HANDLE file = CreateFileW(detail->DevicePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); /* Эксклюзивно открывает интерфейс обновления выбранного устройства для WinUSB. */
free(detail); /* Путь больше не нужен после открытия системного дескриптора. */
if (file == INVALID_HANDLE_VALUE) { error = GetLastError(); break; } /* Не скрывает отсутствие доступа или занятость интерфейса. */
WINUSB_INTERFACE_HANDLE usb = NULL; /* Хранит отдельный WinUSB-сеанс интерфейса обновления без CAN bulk endpoint. */
error = ERROR_SUCCESS; /* Сбрасывает исходное отсутствие интерфейса после точного совпадения устройства. */
if (!WinUsb_Initialize(file, &usb)) error = GetLastError(); /* Проверяет наличие рабочего WinUSB-драйвера DFU runtime. */
USB_INTERFACE_DESCRIPTOR descriptor = {0}; /* Получает реальный номер и класс открытого интерфейса. */
if (!error && !WinUsb_QueryInterfaceSettings(usb, 0, &descriptor)) error = GetLastError(); /* Не предполагает, что произвольный WinUSB-интерфейс поддерживает DFU. */
if (!error && (descriptor.bInterfaceClass != 0xFE || descriptor.bInterfaceSubClass != 1 || descriptor.bInterfaceProtocol != 1)) error = ERROR_NOT_SUPPORTED; /* Проверяет класс приложения, подкласс DFU и runtime-протокол по DFU 1.1. */
ULONG timeout = 500, transferred = 0; /* Ограничивает управляющий USB-запрос 500 миллисекундами и учитывает длину ответа. */
if (!error && !WinUsb_SetPipePolicy(usb, 0, PIPE_TRANSFER_TIMEOUT, sizeof(timeout), &timeout)) error = GetLastError(); /* Устанавливает тайм-аут endpoint 0, чтобы отсутствующий ответ не блокировал программу. */
WINUSB_SETUP_PACKET packet = {0}; /* Обнуляет поля USB SETUP перед запросом перехода в загрузчик. */
packet.RequestType = 0x21; packet.Request = 0; packet.Value = 1000; packet.Index = descriptor.bInterfaceNumber; /* DFU_DETACH: class/interface OUT, запрос 0, ожидаемое время отключения 1000 мс. */
if (!error && !WinUsb_ControlTransfer(usb, packet, NULL, 0, &transferred, NULL)) error = GetLastError(); /* Отправляет DETACH и требует подтверждённой USB-транзакции. */
packet.RequestType = 0xA1; packet.Request = 3; packet.Value = 0; packet.Length = 6; /* DFU_GETSTATUS: class/interface IN, запрос 3, ответ ровно 6 байт по DFU 1.1. */
if (!error && !WinUsb_ControlTransfer(usb, packet, status, 6, &transferred, NULL)) error = GetLastError(); /* Проверяет результат до отложенного перехода прошивки через 300 мс. */
if (!error && transferred != 6) error = ERROR_INVALID_DATA; /* Не трактует укороченный ответ как подтверждение входа в загрузчик. */
if (usb) WinUsb_Free(usb); /* Освобождает WinUSB до появления загрузчика с другим VID/PID. */
CloseHandle(file); /* Освобождает файловый дескриптор после успеха и при любой ошибке USB. */
break; /* Не отправляет DETACH повторно и не переключается на другой адаптер при отказе. */
} /* Завершает поиск и обработку единственного выбранного адаптера. */
SetupDiDestroyDeviceInfoList(devices); /* Освобождает перечень интерфейсов при любом результате. */
return error; /* Статус прошивки возвращается отдельно от системной ошибки USB. */
} /* Завершает управление DFU runtime без записи flash или изменения образа. */
static bool candle_dev_interal_open(candle_handle hdev);
@@ -736,30 +656,6 @@ bool __stdcall DLL candle_dev_open(candle_handle hdev)
}
bool __stdcall DLL candle_dev_get_product(candle_handle hdev, wchar_t *product, uint32_t capacity) /* Читает USB Product String открытого адаптера в буфер UTF-16; capacity измеряется в wchar_t, включая завершающий ноль. */
{ /* Начало чтения отображаемого имени без изменения режима CAN. */
if (!product || !capacity) return false; /* Не пишет в отсутствующий либо пустой выходной буфер. */
product[0] = 0; /* Сохраняет пустую строку при любом отказе чтения дескриптора. */
candle_device_t *dev = (candle_device_t*)hdev; /* Получает уже открытый WinUSB-сеанс выбранного физического адаптера. */
if (!dev || !dev->winUSBHandle) return false; /* Не открывает устройство заново и не обращается к закрытому интерфейсу. */
USB_DEVICE_DESCRIPTOR descriptor = {0}; /* Получает индекс строки продукта из стандартного дескриптора устройства. */
ULONG received = 0; /* Хранит фактически полученное количество байтов управляющего ответа. */
if (!WinUsb_GetDescriptor(dev->winUSBHandle, 1, 0, 0, (PUCHAR)&descriptor, sizeof(descriptor), &received) || received != sizeof(descriptor)) return false; /* Тип 1 обозначает Device Descriptor по USB 2.0; неполный ответ не используется. */
UCHAR string_index = descriptor.iProduct; /* Для обычных адаптеров использует объявленный устройством индекс USB Product String. */
if (descriptor.idVendor == 0x1D50 && descriptor.idProduct == 0x606F) string_index = 2; /* В CANable Candlelight имя продукта имеет индекс 2; WinUSB может подменять iProduct индексом имени интерфейса, как описано в штатном примере проекта. */
if (!string_index) return false; /* Нулевой iProduct означает, что устройство не предоставляет имя продукта. */
UCHAR raw[256] = {0}; /* Буфер вмещает строковый дескриптор USB, чей однобайтовый bLength ограничен 255 байтами. */
USHORT language = 0x0409; /* Использует английский идентификатор USB как резервный вариант для устройств без списка языков. */
if (WinUsb_GetDescriptor(dev->winUSBHandle, 3, 0, 0, raw, sizeof(raw), &received) && received >= 4 && raw[0] >= 4 && raw[1] == 3) language = (USHORT)(raw[2] | ((USHORT)raw[3] << 8)); /* Тип 3 и индекс 0 возвращают список LANGID; выбирает первый язык, предоставленный адаптером. */
if (!WinUsb_GetDescriptor(dev->winUSBHandle, 3, string_index, language, raw, sizeof(raw), &received)) return false; /* Читает именно строку продукта на поддерживаемом устройством языке. */
if (received < 2 || raw[1] != 3 || raw[0] < 2 || raw[0] > received || (raw[0] & 1)) return false; /* Проверяет тип, размер и чётное число байтов UTF-16 перед копированием в выходной буфер. */
uint32_t count = (raw[0] - 2) / 2; /* Исключает двухбайтовый заголовок и переводит длину UTF-16LE из байтов в число кодовых единиц. */
if (count >= capacity) return false; /* Не обрезает индивидуальный номер в конце имени и оставляет место для завершающего нуля. */
for (uint32_t index = 0; index < count; ++index) product[index] = (wchar_t)(raw[2 + index * 2] | ((USHORT)raw[3 + index * 2] << 8)); /* Явно собирает кодовые единицы UTF-16LE без предположений о выравнивании массива байтов. */
product[count] = 0; /* Завершает строку для безопасного чтения через ctypes и отображения в Qt. */
return count > 0; /* Пустое имя оставляет интерфейсу стандартную резервную подпись адаптера. */
} /* Завершает получение настоящего имени из прошивки USB-адаптера. */
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us)
{
return candle_ctrl_get_timestamp(hdev, timestamp_us);
@@ -927,38 +823,6 @@ bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch)
return candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_RESET, 0);
}
bool __stdcall DLL candle_channel_start_configured(candle_handle hdev, uint8_t ch, uint32_t flags) /* Новый вход сохраняет прежний ABI start и позволяет отключать HW_TIMESTAMP. */
{ /* Явно выбранные Python-флаги должны попасть в прошивку без автоматического дополнения. */
return candle_ctrl_set_device_mode((candle_device_t*)hdev, ch, CANDLE_DEVMODE_START, flags); /* Передаём точную маску после проверки возможностей на уровне транспорта. */
} /* Завершение запуска с явно заданными параметрами. */
bool __stdcall DLL candle_channel_get_data_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap) /* Экспортируем чтение FD-пределов для расчёта тайминга на любой частоте CAN. */
{ /* Общая форма capability позволяет повторно использовать валидатор номинального тайминга. */
return candle_ctrl_data_capability((candle_device_t*)hdev, ch, cap); /* USB-запрос возвращает реальные пределы выбранного канала. */
} /* Завершение получения возможностей быстрой фазы. */
bool __stdcall DLL candle_channel_set_termination(candle_handle hdev, uint8_t ch, uint32_t state) /* Значения 0 и 1 соответствуют OFF и ON стандарта gs_usb. */
{ /* Проверка исключает отправку неопределённого состояния в прошивку. */
return state <= 1 && candle_ctrl_termination((candle_device_t*)hdev, ch, &state, true); /* Выполняем запись только для допустимого состояния. */
} /* Завершение переключения терминатора. */
bool __stdcall DLL candle_channel_get_termination(candle_handle hdev, uint8_t ch, uint32_t *state) /* Чтение состояния не меняет топологию CAN. */
{ /* Используем тот же формат 32-битного значения, что и при записи. */
return candle_ctrl_termination((candle_device_t*)hdev, ch, state, false); /* false выбирает USB_DIR_IN и GET_TERMINATION. */
} /* Завершение чтения терминатора. */
bool __stdcall DLL candle_channel_get_diagnostics(candle_handle hdev, uint8_t ch, uint32_t *state, uint32_t *rxerr, uint32_t *txerr) /* Сохраняем счётчики, которые старый get_state отбрасывал. */
{ /* Диагностика выполняется только для уже открытого устройства. */
candle_device_state_t data = {0}; /* Полная структура протокола содержит три слова: состояние, RX и TX. */
if (!candle_ctrl_get_state((candle_device_t*)hdev, ch, &data)) { /* Не возвращаем нулевые счётчики как будто запрос был успешным. */
return false; /* Причина USB-отказа уже записана управляющим запросом. */
} /* Дальнейшее копирование относится только к подтверждённому ответу. */
*state = data.state; /* Сохраняем состояние Error-active, Warning, Passive, Bus-off, Stopped или Sleeping. */
*rxerr = data.rxerr; /* Счётчик ошибок приёма измеряется в единицах CAN-контроллера. */
*txerr = data.txerr; /* Счётчик ошибок передачи помогает отличать отсутствие ACK от ошибок приёма. */
return true; /* Все три выходных значения относятся к одному USB-ответу. */
} /* Завершение расширенной диагностики канала. */
/* Write len bytes from buf to the OUT pipe, aborting after 300 ms.
* Writes are serialised by writeMutex in CandleApiInterface so dev->txEvent
* is never accessed by two threads simultaneously. */
@@ -1153,13 +1017,14 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
}
dev->rxurbs[urb_num].pending = false;
const DWORD header_size = 12; /* Заголовок gs_usb занимает 12 байт в Classic и FD; короткий FD может не содержать восьми байтов данных. */
if (bytes_transfered < header_size) { /* До чтения flags и DLC проверяем наличие полного заголовка. */
/* Minimum: classic CAN header (12 bytes) + at least 8 data bytes = 20 bytes */
static const DWORD classic_min = sizeof(candle_frame_t) - 4;
if (bytes_transfered < classic_min) {
candle_prepare_read(dev, urb_num);
candle_logf(L"fd read too small urb=%u bytes=%lu min=%lu",
urb_num,
bytes_transfered,
header_size); /* Минимальная длина здесь относится к заголовку, а payload проверяется после определения формата. */
classic_min);
dev->last_error = CANDLE_ERR_READ_SIZE;
return false;
}
@@ -1182,11 +1047,6 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
if (is_fd_frame) {
/* can_dlc is at byte offset 8 in both classic and FD wire frames. */
const uint8_t raw_dlc = dev->rxurbs[urb_num].buf[8];
if (raw_dlc > 15) { /* FD DLC является четырёхбитным кодом и не может содержать значения выше 15. */
candle_prepare_read(dev, urb_num); /* Повреждённый USB-пакет не должен останавливать последующий приём. */
dev->last_error = CANDLE_ERR_READ_SIZE; /* Неверный DLC считается ошибкой формата входного кадра. */
return false; /* Не превращаем повреждённый DLC в пустой успешный FD-кадр. */
} /* Далее длина безопасно вычисляется по таблице CAN FD. */
const DWORD data_len = candle_dlc_to_len(raw_dlc);
const DWORD min_size = 12 + data_len; /* header + data, without timestamp */
@@ -1217,11 +1077,6 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
}
/* else: timestamp stays zero from memset above */
} else {
if (bytes_transfered < sizeof(candle_frame_t) - sizeof(uint32_t)) { /* Classic-пакет обязан содержать весь восьмибайтовый буфер данных, даже при меньшем DLC. */
candle_prepare_read(dev, urb_num); /* Возобновляем соответствующий URB после отклонения неполного пакета. */
dev->last_error = CANDLE_ERR_READ_SIZE; /* Не допускаем чтение неполученных данных из локальной структуры. */
return false; /* Повреждённый Classic-пакет не попадает в Python через FD API. */
} /* Минимальный размер Classic теперь проверен отдельно от коротких FD. */
/* Classic CAN frame — copy into FD struct, fixing the timestamp position */
candle_frame_t classic;
DWORD copy_len = (bytes_transfered < sizeof(classic)) ? bytes_transfered : sizeof(classic);

View File

@@ -3,8 +3,6 @@
; прикладные регистры SETProtocol.
EXPORTS
candle_dev_get_product ; Имя продукта из USB-дескриптора прошивки для списка адаптеров SETGUI.
candle_dev_enter_dfu ; Переход через отдельный DFU runtime-интерфейс того же физического USB-адаптера.
candle_list_scan
candle_list_free
candle_list_length
@@ -33,13 +31,3 @@ EXPORTS
candle_frame_data
candle_frame_timestamp_us
candle_dev_last_error
candle_channel_start_configured ; Точная маска запуска с управляемыми аппаратными метками.
candle_channel_get_data_capabilities ; Пределы тайминга быстрой фазы CAN FD.
candle_channel_set_termination ; Переключение встроенного терминатора.
candle_channel_get_termination ; Чтение состояния встроенного терминатора.
candle_channel_get_diagnostics ; Состояние и счётчики ошибок CAN.
candle_channel_set_data_timing ; Тайминг фазы данных CAN FD.
candle_fd_frame_send ; Отправка полного кадра CAN FD до 64 байт.
candle_fd_frame_read ; Приём FD и Classic без обрезания данных.
candle_fd_frame_type ; Различение RX, TX echo и ошибок FD.
candle_fd_frame_id ; Получение CAN ID из структуры FD.

View File

@@ -246,9 +246,7 @@ bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len);
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev);
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state);
wchar_t * __stdcall DLL candle_dev_get_path(candle_handle hdev);
bool __stdcall DLL candle_dev_get_product(candle_handle hdev, wchar_t *product, uint32_t capacity); /* Возвращает USB Product String в UTF-16; capacity включает завершающий ноль и измеряется в wchar_t. */
bool __stdcall DLL candle_dev_open(candle_handle hdev);
uint32_t __stdcall DLL candle_dev_enter_dfu(const wchar_t *can_path, uint8_t status[6]); /* Переводит выбранный физический адаптер в DFU и возвращает Win32-код плюс ответ DFU_GETSTATUS. */
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us);
bool __stdcall DLL candle_dev_close(candle_handle hdev);
bool __stdcall DLL candle_dev_free(candle_handle hdev);
@@ -261,11 +259,6 @@ bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, can
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate);
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags);
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch);
bool __stdcall DLL candle_channel_start_configured(candle_handle hdev, uint8_t ch, uint32_t flags); /* Запускаем с точной маской без принудительного включения аппаратных меток. */
bool __stdcall DLL candle_channel_get_data_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap); /* Возвращаем пределы тайминга быстрой фазы FD. */
bool __stdcall DLL candle_channel_set_termination(candle_handle hdev, uint8_t ch, uint32_t state); /* Состояние терминатора: 0 — выключен, 1 — включён. */
bool __stdcall DLL candle_channel_get_termination(candle_handle hdev, uint8_t ch, uint32_t *state); /* Чтение не меняет подключение встроенного резистора. */
bool __stdcall DLL candle_channel_get_diagnostics(candle_handle hdev, uint8_t ch, uint32_t *state, uint32_t *rxerr, uint32_t *txerr); /* Передаём состояние CAN и оба счётчика ошибок без потери данных ответа. */
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame);
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms);

View File

@@ -39,10 +39,8 @@ enum {
CANDLE_TIMESTAMP_GET = 6,
/* 7: IDENTIFY, 8: GET_USER_ID, 9: SET_USER_ID (not used here) */
CANDLE_BREQ_DATA_BITTIMING = 10,
CANDLE_BREQ_BT_CONST_EXT = 11, /* Расширенные пределы номинальной и быстрой фазы согласно gs_usb. */
CANDLE_BREQ_SET_TERMINATION = 12, /* Запрос gs_usb 12 переключает встроенный резистор окончания шины. */
CANDLE_BREQ_GET_TERMINATION = 13, /* Запрос gs_usb 13 читает текущее состояние терминатора. */
CANDLE_BREQ_GET_STATE = 14, /* Стандартный GET_STATE имеет номер 14; номер 12 принадлежит терминатору. */
/* 11: SET_TERMINATION, not used here */
CANDLE_BREQ_GET_STATE = 12,
CANDLE_BREQ_BUS_OFF_RECOVERY = 32,
};
@@ -239,22 +237,3 @@ bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel)
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
return rc;
}
bool candle_ctrl_data_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *cap) /* Структура gs_usb BT_CONST_EXT содержит десять общих и восемь быстрых полей uint32. */
{ /* Используем массив, поскольку протокол содержит только однородные 32-битные значения без отступов. */
uint32_t fields[18] = {0}; /* 18 полей по 4 байта образуют стандартный ответ размером 72 байта. */
bool rc = usb_control_msg(dev->winUSBHandle, CANDLE_BREQ_BT_CONST_EXT, USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE, channel, 0, fields, sizeof(fields)); /* Запрашиваем пределы именно выбранного CAN-канала. */
if (rc) { /* Выходная структура должна меняться только после успешного USB-ответа. */
memcpy(cap, fields, 2 * sizeof(uint32_t)); /* Маска функций и частота CAN общие для обеих фаз. */
memcpy(&cap->tseg1_min, &fields[10], 8 * sizeof(uint32_t)); /* Поля DTSEG1…DBRP_INC заменяют номинальные пределы структуры capability. */
} /* При отказе вызывающий код получит false и не использует нулевые пределы. */
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_BITTIMING_CONST; /* Сохраняем диагностический код чтения аппаратных ограничений. */
return rc; /* Успех означает наличие полной структуры пределов быстрой фазы. */
} /* Завершение запроса расширенных возможностей FD. */
bool candle_ctrl_termination(candle_device_t *dev, uint8_t channel, uint32_t *state, bool write) /* Переключение терминатора использует стандартные SET=12 и GET=13. */
{ /* Состояние передаётся одним 32-битным словом согласно протоколу gs_usb. */
bool rc = usb_control_msg(dev->winUSBHandle, write ? CANDLE_BREQ_SET_TERMINATION : CANDLE_BREQ_GET_TERMINATION, (write ? USB_DIR_OUT : USB_DIR_IN)|USB_TYPE_VENDOR|USB_RECIP_INTERFACE, channel, 0, state, sizeof(*state)); /* Номер CAN-канала передаётся в wValue, а wIndex равен нулю по спецификации. */
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE; /* Ошибка настройки доступна Python через candle_dev_last_error. */
return rc; /* Не выдаём успешное переключение при отказе USB-контроллера. */
} /* Завершение управления встроенным окончанием шины. */

View File

@@ -51,6 +51,4 @@ bool candle_ctrl_set_data_bittiming(candle_device_t *dev, uint8_t channel, candl
bool candle_ctrl_get_timestamp(candle_device_t *dev, uint32_t *current_timestamp);
bool candle_ctrl_get_state(candle_device_t *dev, uint8_t channel, candle_device_state_t *data);
bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel);
bool candle_ctrl_data_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *cap); /* Читаем пределы быстрой фазы CAN FD отдельным запросом расширенной структуры. */
bool candle_ctrl_termination(candle_device_t *dev, uint8_t channel, uint32_t *state, bool write); /* Передаём состояние терминатора: 0 — выключен, 1 — включён. */

View File

@@ -0,0 +1,87 @@
"""Проверяет совместимость postbuild, экспорт двух образов и отказ от устаревшего Flash OUT.""" # Фиксирует контракт общего выпуска без зависимости от установленного TI-компилятора.
import json # Формирует паспорт текущей сборки для проверки архива и контрольных сумм.
from pathlib import Path # Создаёт независимые пути проекта, компилятора и результатов в каждом тесте.
import subprocess # Использует штатное исключение ненулевого кода проектного сборщика.
import sys # Подключает общий модуль из каталога tools текущей копии templates.
import tempfile # Изолирует проверочные файлы от реальных прошивок и каталогов GUI.
import unittest # Организует проверки публичного контракта postbuild стандартной библиотекой Python.
from unittest.mock import patch # Подменяет только внешние инструменты, оставляя настоящее архивирование и SHA-256.
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools')) # Находит проверяемую реализацию относительно теста без путей конкретной рабочей станции.
import c2000_postbuild as c2000 # Вызывает тот же postbuild, который запускается штатным postcompile.bat.
from identity import verify # Проверяет байты скопированных образов по настоящему manifest.
class PostbuildTests(unittest.TestCase): # Проверяет общую цепочку от готового OUT до экспортированной папки выпуска.
def setUp(self): # Для каждой проверки создаёт отдельный проект с типичной структурой CCS.
temporary = tempfile.TemporaryDirectory() # Все файлы сценария принадлежат временному каталогу теста.
self.addCleanup(temporary.cleanup) # Освобождает только каталог данной проверки при любом результате.
self.root = Path(temporary.name) # Корень не зависит от реальной прошивки и установленного templates.
self.build = self.root / 'project/Debug' # Проверяет соглашение о расположении сборщика рядом с конфигурациями CCS.
self.build.mkdir(parents=True) # Подготавливает рабочий каталог внешних инструментов.
self.compiler = self.root / 'compiler' # Позволяет проверить передачу компилятора текущего проекта вместо фиксированного пути.
(self.compiler / 'bin').mkdir(parents=True) # Воспроизводит структуру установленного C2000 compiler.
(self.compiler / 'bin/hex2000.exe').write_bytes(b'fake tool') # Проверка наличия инструмента реальна, а выполнение будет подменено.
self.source = self.root / 'Bin/Firmware.out' # Основной OUT размещён вне Debug, как в рабочем проекте пользователя.
self.source.parent.mkdir() # Создаёт отдельный каталог исходного COFF и итогового Flash COFF.
self.source.write_bytes(b'RAM image') # Даёт узнаваемые байты для проверки, что основной образ не подменён Flash-вариантом.
self.metadata = self.build / 'metadata.json' # Один паспорт используется обоими вариантами текущего выпуска.
self.metadata.write_text(json.dumps({'build_id': 'NOGIT000', 'built_at_utc': '2026-10-14T13:00:00+00:00', 'source': {'commit': None}, 'version': [1, 2, 3]}), encoding='utf-8') # Фиксированное время проверяет выбор папки по prebuild, а не времени файлов.
self.output = self.build / 'firmware' # Локальный выпуск получает оба образа и их сопроводительные файлы.
self.export = self.root / 'GUI' # Экспорт изолирован от настоящего GUI и сетевых ресурсов.
def fake_tool(self, command, **kwargs): # Имитирует границу внешних инструментов, оставляя реальную логику упаковки и проверки.
if '--postbuild' in command: # Вложенная Flash-сборка обязана использовать режим без повторного Make.
self.assertEqual(Path(command[command.index('--compiler') + 1]), self.compiler.resolve()) # Проверяет выбор текущей версии TI compiler.
self.assertEqual(Path(command[command.index('--build-dir') + 1]), self.build.resolve()) # Проверяет использование текущих объектов и generated-паспорта.
image = Path(command[command.index('--output') + 1]) # Результат должен создаваться по переданному временному пути.
self.assertNotEqual(image.parent, self.source.parent) # Запрещает выдавать существующий OUT из Bin за результат текущего вызова.
image.write_bytes(b'current Flash image') # Создаёт узнаваемый новый COFF для сравнения всех копий.
image.with_suffix('.map').write_text('codestart 0033fff6', encoding='utf-8') # Воспроизводит обязательный второй результат сборщика.
else: # Обычная SCI8-конверсия должна продолжать использовать основной RAM OUT.
expected = 'Firmware_flash.out' if '_flash_rs.bin' in command[command.index('-o') + 1] else self.source.name # Только отдельная RS-конверсия должна брать Flash COFF.
self.assertEqual(Path(command[1]).name, expected) # Проверяет, что EEPROM BIN и Flash RS BIN создаются из разных исходных образов.
Path(command[command.index('-o') + 1]).write_bytes(b'EEPROM image') # Выпускает HEX/BIN с отличимыми байтами основного варианта.
Path(command[command.index('-map') + 1]).write_text('RAM map', encoding='utf-8') # Сохраняет карты, необходимые настоящему архиватору.
return subprocess.CompletedProcess(command, 0) # Подтверждает только имитируемый инструмент, не подменяя проверки выпуска.
def run_postbuild(self): # Вызывает публичную функцию с теми же путями, что передаёт BAT из CCS.
return c2000.postbuild(self.compiler, self.build, str(self.source.with_suffix('')), self.metadata, self.output, self.export) # Включает настоящий экспорт и проверку manifest.
def test_project_without_flash_builder_keeps_eeprom_workflow(self): # Старые проекты должны работать без новых настроек или Flash-файлов.
with patch.object(c2000.subprocess, 'run', side_effect=self.fake_tool) as calls: # Подменяет только TI Hex Utility.
result = self.run_postbuild() # Выполняет полный прежний выпуск EEPROM.
self.assertEqual(calls.call_count, 2) # Для HEX и BIN выполнены два вызова; дополнительного сборщика не было.
self.assertEqual(self.source.read_bytes(), b'RAM image') # Основной OUT не изменён упаковкой.
self.assertFalse(self.source.with_name('Firmware_flash.out').exists()) # Отсутствие builder не создаёт фиктивный Flash-образ.
self.assertEqual(verify(result, Path(str(result) + '.manifest.json'))['size_bytes'], len(b'EEPROM image')) # Проверяет настоящее содержимое EEPROM BIN и его паспорт.
def test_flash_and_eeprom_share_current_export_with_separate_checksums(self): # Новый выпуск должен содержать оба варианта под своими именами.
(self.build.parent / 'build_flash.py').write_text('# Сборщик проекта подключён явно.\n', encoding='utf-8') # Наличие сценария включает дополнительный шаг postbuild.
with patch.object(c2000.subprocess, 'run', side_effect=self.fake_tool) as calls: # Flash и hex2000 имитируются на границе процессов.
self.run_postbuild() # Архиватор и экспорт работают без подмены.
self.assertEqual(calls.call_count, 4) # Один Flash-вызов, две EEPROM-конверсии и отдельный RS BIN подтверждают отсутствие рекурсивного Make.
folder = self.export / 'NOGIT000_2026-10-14_16-00-00_UTC+3' # Имя следует времени prebuild с переводом в московский UTC+3.
for directory in (self.source.parent, self.output, folder): # Проверяет копию для CCS, плоский выпуск и архив для GUI.
self.assertEqual((directory / 'Firmware_flash.out').read_bytes(), b'current Flash image') # Каждая копия содержит новый Flash COFF.
self.assertTrue((directory / 'Firmware_flash.map').is_file()) # Карта Flash не теряется при сохранении или экспорте.
for name in ('Firmware_flash.out', 'Firmware_NOGIT000.bin', 'Firmware_flash_rs.bin'): # Контрольные суммы каждого варианта должны проверяться независимо.
verify(folder / name, folder / (name + '.manifest.json')) # Использует реальные SHA-256 экспортированных байтов.
self.assertEqual((folder / self.source.name).read_bytes(), b'RAM image') # Архив сохраняет исходный RAM OUT вместе с новым Flash OUT.
def test_failed_or_incomplete_builder_cannot_export_stale_flash(self): # Ошибка или отсутствие карты должны остановить выпуск до экспорта старого COFF.
(self.build.parent / 'build_flash.py').write_text('# Подключён неисправный сборщик для проверки отказа.\n', encoding='utf-8') # Включает путь Flash-проверки.
stale = self.source.with_name('Firmware_flash.out') # В Bin уже есть образ от предыдущей успешной сборки.
stale.write_bytes(b'old Flash image') # Отличимые байты выявят ошибочную повторную публикацию.
for failure in (subprocess.CalledProcessError(1, 'builder'), None): # Проверяет ненулевой код и ложный успех без обязательных результатов.
with self.subTest(failure=failure): # Даёт отдельную диагностику двум независимым отказам.
with patch.object(c2000.subprocess, 'run', side_effect=failure) as calls: # Сборщик либо падает, либо не создаёт файлов.
with self.assertRaises((subprocess.CalledProcessError, ValueError)): # Оба отказа должны выйти наружу как ошибка postbuild.
self.run_postbuild() # Не разрешает продолжение EEPROM-конверсии после сбоя Flash.
self.assertEqual(calls.call_count, 1) # После отказа Flash ни hex2000, ни новый сборщик не запускаются.
self.assertEqual(stale.read_bytes(), b'old Flash image') # Предыдущий образ сохраняется, но не считается новым результатом.
self.assertFalse(self.export.exists()) # Неполная сборка не создаёт выпуск для GUI.
if __name__ == '__main__': # Позволяет запускать проверки отдельно или через unittest discover.
unittest.main() # Возвращает ненулевой код при нарушении контракта упаковки или экспорта.

View File

@@ -0,0 +1,111 @@
"""Tests for preserving exact firmware bytes under pre-build provenance folders."""
import json
from pathlib import Path
import sys
import tempfile
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
from identity import archive_build, build_folder_name, export_build, verify
class ArchiveTests(unittest.TestCase):
def metadata(self, stamp="2026-10-08T12:30:00+00:00"):
return {"source": {"commit": "a1b2c3d4" + "0" * 32, "dirty": False},
"templates": {"commit": None, "dirty": None},
"build_id": "NOGIT000", "built_at_utc": stamp,
"version": [1, 2, 3]}
def test_snapshot_preserves_image_and_records_exact_sha(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image = root / "firmware.bin"
payload = bytes(range(256))
image.write_bytes(payload)
metadata = root / "metadata.json"
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
result = archive_build(image, metadata, root / "out")
folder = Path(result["directory"])
self.assertEqual(folder.name, "a1b2c3d4_2026-10-08_15-30-00_UTC+3")
self.assertEqual((folder / image.name).read_bytes(), payload)
self.assertEqual(image.read_bytes(), payload)
self.assertEqual(json.loads((folder / "firmware_identity.json").read_text()), self.metadata())
manifest = folder / (image.name + ".manifest.json")
self.assertEqual(verify(folder / image.name, manifest)["size_bytes"], 256)
(folder / image.name).write_bytes(b"corrupted")
with self.assertRaises(ValueError):
verify(folder / image.name, manifest)
def test_prebuild_times_keep_two_builds_separate(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.hex", root / "metadata.json"
image.write_bytes(b":00000001FF\r\n")
folders = []
for stamp in ("2026-10-08T12:30:00+00:00", "2026-10-08T12:31:00+00:00"):
metadata.write_text(json.dumps(self.metadata(stamp)), encoding="utf-8")
folders.append(Path(archive_build(image, metadata, root / "out")["directory"]))
self.assertNotEqual(folders[0], folders[1])
self.assertTrue(all((folder / image.name).is_file() for folder in folders))
def test_export_copies_current_folder_only(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.bin", root / "metadata.json"
image.write_bytes(b"exact current build")
# A folder with a later timestamp must not be selected for this build.
metadata.write_text(json.dumps(self.metadata("2026-10-08T12:40:00+00:00")), encoding="utf-8")
archive_build(image, metadata, root / "local")
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
result = archive_build(image, metadata, root / "local", export=root / "exported")
copied = Path(result["exported_directory"])
self.assertEqual([p.name for p in (root / "exported").iterdir()],
["a1b2c3d4_2026-10-08_15-30-00_UTC+3"])
self.assertEqual((copied / image.name).read_bytes(), image.read_bytes())
self.assertTrue((copied / "firmware_identity.json").is_file())
self.assertTrue((copied / (image.name + ".sha256")).is_file())
verify(copied / image.name, copied / (image.name + ".manifest.json"))
def test_complete_folder_export_keeps_nested_files_and_old_destination(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.bin", root / "metadata.json"
image.write_bytes(b"image")
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
folder = Path(archive_build(image, metadata, root / "local")["directory"])
(folder / "notes").mkdir()
(folder / "notes" / "release.txt").write_text("release notes", encoding="utf-8")
old = root / "exported" / "old_build"
old.mkdir(parents=True)
(old / "keep.txt").write_text("keep", encoding="utf-8")
copied = export_build(folder, root / "exported", image.name)
self.assertEqual((copied / "notes" / "release.txt").read_text(), "release notes")
self.assertEqual((old / "keep.txt").read_text(), "keep")
with self.assertRaises(ValueError):
export_build(folder, folder / "recursive", image.name)
def test_offset_normalizes_to_utc_plus_three_and_dirty_is_visible(self):
metadata = self.metadata("2026-10-08T15:30:00+03:00")
metadata["source"]["dirty"] = True
self.assertEqual(build_folder_name(metadata), "a1b2c3d4+_2026-10-08_15-30-00_UTC+3")
def test_folder_date_rolls_over_at_utc_plus_three_midnight(self):
metadata = self.metadata("2026-10-08T22:30:00+00:00")
self.assertEqual(build_folder_name(metadata), "a1b2c3d4_2026-10-09_01-30-00_UTC+3")
self.assertEqual(metadata["built_at_utc"], "2026-10-08T22:30:00+00:00")
def test_archive_without_git_has_explicit_placeholder(self):
metadata = self.metadata()
metadata["source"]["commit"] = None
self.assertEqual(build_folder_name(metadata), "NOGIT000_2026-10-08_15-30-00_UTC+3")
def test_invalid_provenance_cannot_be_a_folder_name(self):
metadata = self.metadata()
metadata["source"]["commit"] = "../outside"
with self.assertRaises(ValueError):
build_folder_name(metadata)
metadata = self.metadata("2026-10-08T12:30:00")
with self.assertRaises(ValueError):
build_folder_name(metadata)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,119 @@
"""Convert a CCS C2000 OUT to SCI8 HEX/BIN and seal its pre-build identity.
Uses only the standard library, TI hex2000 and the shared identity module.
No network, SETGUI, or project-specific paths are required.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import re
import subprocess
import sys # Запускает проектный сборщик тем же Python, которым выполняется общий postbuild.
import tempfile
from identity import archive_build, seal, verify
def build_flash_variant(compiler: Path, build_dir: Path, source: Path, stage: Path) -> tuple: # Возвращает файлы дополнительного Flash-образа только для проектов с явным сборщиком.
builder = build_dir.resolve().parent / "build_flash.py" # Соглашение CCS: Debug/Release расположены рядом с проектным сценарием сборки Flash.
if not builder.is_file(): # Отсутствие сценария оставляет прежний выпуск EEPROM без дополнительных требований к проекту.
return () # Пустой список не добавляет Flash-файлы в архив обычного проекта.
image = stage / (source.stem + "_flash.out") # Отдельное имя и временный каталог исключают подмену RAM-образа и использование устаревшего Flash-файла.
subprocess.run([sys.executable, str(builder), "--postbuild", "--compiler", str(compiler.resolve()), "--build-dir", str(build_dir.resolve()), "--output", str(image)], cwd=build_dir, check=True) # Режим postbuild использует уже собранные объекты и запрещает повторный запуск Make.
files = (image, image.with_suffix(".map")) # Карта линковки сопровождает COFF и позволяет проверить адрес аппаратной точки входа.
for file in files: # Оба результата должны быть выпущены текущим вызовом сборщика.
if not file.is_file() or not file.stat().st_size: # Успешный код процесса без непустого результата не считается успешной сборкой.
raise ValueError(f"Flash builder produced no file: {file}") # Ошибка останавливает postbuild до архивирования или экспорта неполного выпуска.
return files # Передаёт проверенные файлы вызывающему шагу для подписи и сохранения.
def postbuild(compiler: Path, build_dir: Path, artifact: str,
metadata: Path, output: Path, export: Path = None) -> Path:
# CCS may include the configured output directory in this macro.
# Resolve the input separately; output filenames must use only its basename.
artifact_path = Path(artifact)
if not artifact or artifact_path.name in ("", ".", ".."):
raise ValueError("artifact must name an OUT file without its extension")
artifact = artifact_path.name
if any(c in artifact for c in '\\/:*?"<>|'):
raise ValueError("invalid artifact file base name")
source_base = (artifact_path if artifact_path.is_absolute()
else build_dir / artifact_path)
source = Path(str(source_base) + ".out").resolve()
tool = compiler.resolve() / "bin" / "hex2000.exe"
if not tool.is_file():
raise FileNotFoundError(f"C2000 Hex Utility not found: {tool}")
if not source.is_file():
raise FileNotFoundError(f"compiled OUT not found: {source}")
metadata = metadata.resolve()
settings = json.loads(metadata.read_text(encoding="utf-8"))
build_id = settings["build_id"]
if not isinstance(build_id, str) or not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", build_id):
raise ValueError("invalid pre-build identity; run precompile before building")
if build_id == "NOGIT000":
print("[firmware-build] Git identity unavailable: using NOGIT000")
output = output.resolve()
output.mkdir(parents=True, exist_ok=True)
# Keep failed conversions and incomplete manifests out of the final folder.
with tempfile.TemporaryDirectory(prefix=".firmware-", dir=output) as temporary:
stage = Path(temporary)
hex_image = stage / (artifact + ".hex")
bin_image = stage / (artifact + "_" + build_id + ".bin")
flash_files = build_flash_variant(compiler, build_dir, source, stage) # Сначала собирает дополнительный Flash-вариант; его ошибка не допускает выпуск нового BIN.
for image, mode in ((hex_image, "-i"), (bin_image, "-b")):
subprocess.run([str(tool), str(source), "-boot", "-sci8",
"-map", str(image.with_suffix(image.suffix + ".map")),
"-o", str(image), mode], check=True)
if not image.is_file() or not image.stat().st_size:
raise ValueError(f"hex2000 produced no image: {image}")
seal(bin_image, metadata)
verify(bin_image, bin_image.with_suffix(bin_image.suffix + ".manifest.json"))
if flash_files: # Проекты со сборщиком получают независимую проверку точных байтов Flash-образа.
seal(flash_files[0], metadata) # Использует тот же паспорт prebuild для второго варианта этой сборки.
verify(flash_files[0], flash_files[0].with_suffix(".out.manifest.json")) # Проверяет размер и SHA-256 перед сохранением результатов.
serial_image = stage / (artifact + "_flash_rs.bin") # Отдельный SCI8 BIN содержит Flash-адреса и предназначен только для новой команды записи по RS.
serial_map = serial_image.with_suffix('.bin.map') # Карта конверсии позволяет отличить Flash-адреса от EEPROM/RAM BIN.
subprocess.run([str(tool), str(flash_files[0]), '-boot', '-sci8', '-map', str(serial_map), '-o', str(serial_image), '-b'], check=True) # Конвертирует именно Flash COFF с загрузочными адресами секций.
if not serial_image.is_file() or not serial_image.stat().st_size: # Пустая конверсия не должна попасть в выпуск либо каталог GUI.
raise ValueError(f'hex2000 produced no Flash RS image: {serial_image}') # Передаёт ошибку конверсии общему postcompile.
seal(serial_image, metadata) # Привязывает RS BIN к той же версии, ревизии и времени текущего prebuild.
verify(serial_image, serial_image.with_suffix('.bin.manifest.json')) # Проверяет точные байты перед копированием в Bin и архив.
flash_files += (serial_image, serial_map, serial_image.with_suffix('.bin.manifest.json'), serial_image.with_suffix('.bin.sha256')) # Включает транспортный BIN, карту и контрольные файлы в общий выпуск Flash.
for file in flash_files: # Сохраняет COFF и карту также рядом с обычным OUT для выбора в CCS.
destination = source.parent / file.name # Каталог исходного OUT задаёт место назначения без путей конкретного изделия в templates.
destination.write_bytes(file.read_bytes()) # Копирует только результаты успешного текущего вызова, сохраняя исходный RAM-образ.
for item in stage.iterdir():
item.replace(output / item.name)
result = output / bin_image.name # BIN остаётся основным образом EEPROM и возвращаемым результатом общего postbuild.
flash_archive = tuple(output / file.name for file in flash_files) # После переноса из временного каталога берёт актуальные Flash-файлы из firmware.
if flash_files: # Добавляет сопроводительные файлы только при выполненной сборке Flash.
flash_archive += (output / (flash_files[0].name + ".manifest.json"), output / (flash_files[0].name + ".sha256")) # Архив и GUI получают отдельный паспорт и контрольную сумму Flash COFF.
print(f"[firmware-build] Flash OUT: {source.parent / flash_files[0].name}") # Показывает пользователю CCS конкретный файл для прошивки внутренней Flash через JTAG.
archive_build(result, metadata, output, ( # Сохраняет файлы текущей сборки в архиве и передаёт весь архив в каталог GUI.
source, # Копирует исходный COFF-файл OUT с символами отладки под его именем для загрузки через JTAG из GUI.
output / hex_image.name,
output / (hex_image.name + ".map"),
output / (bin_image.name + ".map"), *flash_archive), export=export) # Экспортирует EEPROM и Flash в одной папке текущего выпуска.
print(f"[firmware-build] BIN: {result}")
print(f"[firmware-build] Manifest: {result}.manifest.json")
print(f"[firmware-build] SHA-256: {result}.sha256")
return result
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
for name in ("compiler", "build-dir", "metadata", "output"):
parser.add_argument("--" + name, type=Path, required=True)
parser.add_argument("--artifact", required=True)
parser.add_argument("--export", type=Path)
try:
postbuild(**vars(parser.parse_args()))
except (OSError, ValueError, KeyError, TypeError, subprocess.CalledProcessError) as error:
parser.exit(1, f"[firmware-build] ERROR: {error}\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -7,10 +7,13 @@ protocol encoding lives here: the generated header contains scalar constants.
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
import hashlib
import json
import os
import re
import shutil
import tempfile
from pathlib import Path
import subprocess
@@ -36,12 +39,14 @@ def git_state(repository: Path) -> dict:
def write_atomic(path: Path, text: str):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_text(text, encoding="utf-8", newline="\n")
# Built-in open supports newline on Python versions before 3.10.
with open(temporary, "w", encoding="utf-8", newline="\n") as stream:
stream.write(text)
temporary.replace(path)
def prepare(repository: Path, templates: Path, config: Path, output: Path) -> dict:
settings = json.loads(config.read_text(encoding="utf-8"))
settings = json.loads(config.read_text(encoding="utf-8-sig"))
version = settings["version"]
if (len(version) != 3 or any(type(v) is not int for v in version)
or not all(0 <= v <= limit for v, limit in zip(version, (255, 255, 999)))):
@@ -102,6 +107,73 @@ def verify(image: Path, manifest: Path) -> dict:
return metadata
def build_folder_name(metadata: dict) -> str:
"""Use the firmware commit and pre-build time rendered in UTC+3."""
source = metadata.get("source") or {}
dependency = metadata.get("templates") or {}
commit = source.get("commit")
if commit is not None:
if not isinstance(commit, str) or not re.fullmatch(r"[0-9a-fA-F]{40,64}", commit):
raise ValueError("invalid firmware commit in pre-build metadata")
identity = commit[:8].lower()
if source.get("dirty") or dependency.get("dirty"):
identity += "+"
else:
identity = metadata.get("build_id", "NOGIT000")
if not isinstance(identity, str) or not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", identity):
raise ValueError("invalid build ID in pre-build metadata")
stamp_text = metadata.get("built_at_utc")
if not isinstance(stamp_text, str):
raise ValueError("missing pre-build UTC timestamp")
stamp = datetime.fromisoformat(stamp_text.replace("Z", "+00:00"))
if stamp.tzinfo is None:
raise ValueError("pre-build timestamp must include a timezone")
stamp = stamp.astimezone(timezone(timedelta(hours=3)))
return identity + "_" + stamp.strftime("%Y-%m-%d_%H-%M-%S_UTC+3")
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()
if export == folder or folder in export.parents:
raise ValueError("export directory must be outside the current build folder")
destination = export / folder.name
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)
verify(destination / image_name, destination / (image_name + ".manifest.json"))
print(f"[firmware-build] Exported folder: {destination}")
return destination
def archive_build(image: Path, metadata_path: Path, output: Path = None,
extra_files=(), export: Path = None) -> dict:
"""Keep a byte-identical build snapshot, alongside the existing flat output."""
image, metadata_path = image.resolve(), metadata_path.resolve()
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
name = build_folder_name(metadata)
output = (output if output is not None else image.parent / "firmware").resolve()
output.mkdir(parents=True, exist_ok=True)
destination = output / name
# Seal and verify staged copies before publishing files in the build folder.
with tempfile.TemporaryDirectory(prefix=".archive-", dir=output) as temporary:
stage = Path(temporary)
shutil.copy2(image, stage / image.name)
for file in extra_files:
file = Path(file)
shutil.copy2(file, stage / file.name)
shutil.copy2(metadata_path, stage / "firmware_identity.json")
seal(stage / image.name, stage / "firmware_identity.json")
verify(stage / image.name, stage / (image.name + ".manifest.json"))
destination.mkdir(parents=True, exist_ok=True)
for file in stage.iterdir():
file.replace(destination / file.name)
print(f"[firmware-build] Folder: {destination}")
result = {"directory": str(destination), "file": image.name}
if export is not None:
result["exported_directory"] = str(export_build(destination, export, image.name))
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
@@ -114,9 +186,14 @@ def main():
p = sub.add_parser("verify")
p.add_argument("--image", type=Path, required=True)
p.add_argument("--manifest", type=Path, required=True)
p = sub.add_parser("archive")
p.add_argument("--image", type=Path, required=True)
p.add_argument("--metadata", dest="metadata_path", type=Path, required=True)
p.add_argument("--output", type=Path)
p.add_argument("--export", type=Path)
args = vars(parser.parse_args())
command = args.pop("command")
result = {"prepare": prepare, "seal": seal, "verify": verify}[command](**args)
result = {"prepare": prepare, "seal": seal, "verify": verify, "archive": archive_build}[command](**args)
print(json.dumps(result, ensure_ascii=False))

View File

@@ -28,7 +28,7 @@
| `OFF` / `ON` | постоянно выключен / включён |
| `STARTUP` | три коротких импульса, затем постоянно включён |
| `WORK` | 100 мс включён, 900 мс выключен |
| `ACTIVITY` | 50/50 мс |
| `ACTIVITY` | один цикл: 100 мс включён, 900 мс выключен, затем остаётся выключенным |
| `WARNING` | 250/250 мс |
| `ERROR` | два импульса и пауза |
| `CRITICAL` | три импульса и пауза |
@@ -86,6 +86,20 @@ for (;;) {
не начинает рисунок заново. Для нового импульса события служит
`LedIndicator_RestartMode`.
Для индикации приёма вызывайте `LedIndicator_TriggerActivity` либо
`LedIndicator_TriggerActivityAt` на каждый принятый пакет. Свечение занимает
10% секундного цикла: 100 мс включён и 900 мс выключен. Новый приём немедленно
перезапускает фазу свечения, даже внутри предыдущего импульса или паузы;
очереди циклов нет. При интервалах между пакетами меньше 100 мс LED может
гореть непрерывно до истечения 100 мс после последнего пакета. Без нового
события `ACTIVITY` не повторяется. Настраиваемая таблица `patterns` по-прежнему
позволяет изменить длительности и повторение для других применений.
```c
LedIndicator_TriggerActivityAt(&led, 0U, received_ms); /* Каждый приём запускает цикл канала 0 от миллисекундной метки события. */
LedIndicator_ProcessAt(&led, now_ms); /* Главный цикл обслуживает окончание свечения без задержек и ожидания следующего пакета. */
```
## STM32F103, STM32F4, STM32G431 и STM32G474
Порт `ports/stm32-hal` принимает конкретный `TIM_HandleTypeDef *`; библиотека

View File

@@ -18,7 +18,7 @@ static const LedIndicator_Pattern g_default_patterns[LED_INDICATOR_MODE_COUNT] =
{{0U}, LEVELS_1, 1U, 1U, 1U},
{{100U, 100U, 100U, 100U, 100U, 100U}, LEVELS_101010, 6U, 0U, 1U},
{{100U, 900U}, LEVELS_10, 2U, 1U, 0U},
{{50U, 50U}, LEVELS_10, 2U, 1U, 0U},
{{100U, 900U}, LEVELS_10, 2U, 0U, 0U}, /* Один цикл приёма длится 1000 мс: 100 мс свечения составляют 10%, затем 900 мс паузы и выключение. */
{{250U, 250U}, LEVELS_10, 2U, 1U, 0U},
{{100U, 100U, 100U, 700U}, LEVELS_1010, 4U, 1U, 0U},
{{100U, 100U, 100U, 100U, 100U, 700U}, LEVELS_101010, 6U, 1U, 0U}
@@ -220,6 +220,16 @@ uint8_t LedIndicator_RestartMode(LedIndicator *instance, size_t channel,
instance->port.now_ms(instance->port.context));
}
uint8_t LedIndicator_TriggerActivityAt(LedIndicator *instance, size_t channel, uint32_t now_ms) /* Каждый пакет начинает ACTIVITY заново, даже если предыдущая вспышка ещё продолжается. */
{ /* Использует общую проверку канала и принудительный сброс фазы в миллисекундной временной базе. */
return LedIndicator_RestartModeAt(instance, channel, LED_INDICATOR_MODE_ACTIVITY, now_ms); /* Штатный однократный шаблон завершает цикл выключенным уровнем без свободного повторения. */
} /* Завершает запуск одного цикла приёма без ожидания окончания свечения либо паузы. */
uint8_t LedIndicator_TriggerActivity(LedIndicator *instance, size_t channel) /* Предоставляет тот же перезапуск по событию с использованием часов аппаратного порта. */
{ /* Сохраняет поведение варианта At для приложения без собственного передаваемого времени. */
return LedIndicator_RestartMode(instance, channel, LED_INDICATOR_MODE_ACTIVITY); /* Часы порта считываются штатным API один раз на зарегистрированный приём. */
} /* Завершает неблокирующий запуск цикла приёма с аппаратным источником времени. */
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms)
{
size_t i;

View File

@@ -114,6 +114,12 @@ void LedIndicator_Process(LedIndicator *instance);
/** Обновляет выходы с явно переданной меткой времени. */
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms);
/** Начинает один цикл ACTIVITY на каждый приём; новый вызов немедленно перезапускает свечение, не создавая очередь. */
uint8_t LedIndicator_TriggerActivityAt(LedIndicator *instance, size_t channel, uint32_t now_ms); /* Стандартный цикл: 100 мс включён, 900 мс выключен; время задаётся в миллисекундах. */
/** Использует часы аппаратного порта для такого же перезапуска цикла при каждом приёме. */
uint8_t LedIndicator_TriggerActivity(LedIndicator *instance, size_t channel); /* Вызывать из контекста Process; передачу событий из ISR обеспечивает приложение. */
LedIndicator_Mode LedIndicator_GetMode(const LedIndicator *instance, size_t channel);
#ifdef __cplusplus

View File

@@ -85,6 +85,26 @@ int main(void)
CHECK(LedIndicator_SetMode(&indicator, 3U, LED_INDICATOR_MODE_ON) == 0U);
CHECK(LedIndicator_GetMode(&indicator, 0U) == LED_INDICATOR_MODE_WORK);
puts("led indicator tests passed");
CHECK(patterns[LED_INDICATOR_MODE_ACTIVITY].duration_ms[0] == 100U && patterns[LED_INDICATOR_MODE_ACTIVITY].duration_ms[1] == 900U && patterns[LED_INDICATOR_MODE_ACTIVITY].repeat == 0U); /* Фиксирует согласованные 10% свечения и один цикл на приём в канонической таблице. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 2500U) != 0U && fake.outputs[0] == 1U); /* Приём сразу включает канал независимо от фазы прежнего WORK. */
LedIndicator_ProcessAt(&indicator, 2599U); CHECK(fake.outputs[0] == 1U); /* До истечения 100 мс светодиод продолжает гореть. */
LedIndicator_ProcessAt(&indicator, 2600U); CHECK(fake.outputs[0] == 0U); /* Ровно через 100 мс начинается оставшаяся пауза 900 мс. */
LedIndicator_ProcessAt(&indicator, 3499U); CHECK(fake.outputs[0] == 0U); /* Конец первого периода не порождает дополнительную вспышку. */
LedIndicator_ProcessAt(&indicator, 3500U); CHECK(fake.outputs[0] == 0U); /* Однократный ACTIVITY завершается выключенным уровнем на границе 1000 мс. */
LedIndicator_ProcessAt(&indicator, 4500U); CHECK(fake.outputs[0] == 0U); /* Без новых пакетов свободное периодическое мигание отсутствует. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 5000U) != 0U); /* Начинает новый приём после долгого отсутствия обмена. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 5050U) != 0U); /* Второй пакет приходит внутри текущей фазы свечения. */
LedIndicator_ProcessAt(&indicator, 5100U); CHECK(fake.outputs[0] == 1U); /* Старый срок выключения больше не действует: отсчёт перезапущен новым пакетом. */
LedIndicator_ProcessAt(&indicator, 5149U); CHECK(fake.outputs[0] == 1U); /* Новый пакет получает собственные полные 100 мс свечения. */
LedIndicator_ProcessAt(&indicator, 5150U); CHECK(fake.outputs[0] == 0U); /* Новая фаза паузы начинается ровно от последнего приёма. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 5400U) != 0U && fake.outputs[0] == 1U); /* Приём во время паузы сразу включает LED вместо ожидания границы старого периода. */
LedIndicator_ProcessAt(&indicator, 5500U); CHECK(fake.outputs[0] == 0U); /* Повторный запуск в паузе не создаёт очередь пропущенных циклов. */
fake.now = 7000U; CHECK(LedIndicator_TriggerActivity(&indicator, 1U) != 0U && fake.outputs[1] == 1U); /* Проверяет вариант событийного API с часами аппаратного порта и другим каналом. */
fake.now = 7100U; LedIndicator_Process(&indicator); CHECK(fake.outputs[1] == 0U && fake.outputs[0] == 0U); /* Каналы не перезапускают друг другу фазы и не создают фоновую активность. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, UINT32_MAX - 49U) != 0U); /* Начинает импульс непосредственно перед переполнением 32-битного времени. */
LedIndicator_ProcessAt(&indicator, 49U); CHECK(fake.outputs[0] == 1U); /* Через переполнение прошло 99 мс, поэтому свечение ещё активно. */
LedIndicator_ProcessAt(&indicator, 50U); CHECK(fake.outputs[0] == 0U); /* Через переполнение прошло ровно 100 мс, и LED гаснет без зависания. */
CHECK(LedIndicator_TriggerActivityAt(NULL, 0U, 0U) == 0U && LedIndicator_TriggerActivityAt(&indicator, 3U, 0U) == 0U); /* Ошибочный экземпляр и канал не вызывают запись чужого GPIO. */
puts("led indicator tests passed: receive 100/900 ms, immediate retrigger, no repeat, timer wrap"); /* Показывает прохождение новых событийных сценариев вместе с прежней диагностикой. */
return 0;
}

View File

@@ -1,6 +1,6 @@
# Сборка библиотеки protocan_boot. Состав исходников и публичные include-пути задают
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
# запускать их следует через CTest из каталога сборки.
# Сборка библиотеки protocan_boot. Состав исходников и публичные include-пути задают
# подключение к проекту потребителя. Файл также собирает и регистрирует хостовые проверки;
# запускать их следует через CTest из каталога сборки.
cmake_minimum_required(VERSION 3.13)
project(protocan_boot C)
@@ -17,31 +17,10 @@ else()
target_compile_options(protocan_boot PRIVATE -Wall -Wextra -Wpedantic)
endif()
option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON)
option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON)
if(PCAN_BOOT_BUILD_TESTS)
enable_testing()
add_executable(test_pcan_boot tests/test_pcan_boot.c)
target_link_libraries(test_pcan_boot PRIVATE protocan_boot)
add_test(NAME pcan_boot COMMAND test_pcan_boot)
endif()
if(PCAN_BOOT_BUILD_TESTS)
add_executable(test_f28335_can_boot tests/test_f28335_can_boot.c src/pcan_boot.c ports/tms320f28335/f28335_boot_store.c)
target_compile_definitions(test_f28335_can_boot PRIVATE PCAN_BOOT_WIDE_BYTES)
target_include_directories(test_f28335_can_boot PRIVATE tests/f28335_stubs)
add_test(NAME f28335_can_boot COMMAND test_f28335_can_boot)
endif()
if(PCAN_BOOT_BUILD_TESTS)
add_executable(test_f28335_application tests/test_f28335_can_boot.c src/pcan_boot.c ports/tms320f28335/f28335_boot_store.c)
target_compile_definitions(test_f28335_application PRIVATE PCAN_BOOT_WIDE_BYTES F28335_BOOT_LOADER=0)
target_include_directories(test_f28335_application PRIVATE tests/f28335_stubs)
add_test(NAME f28335_application COMMAND test_f28335_application)
foreach(width native wide)
add_executable(test_f28335_store_${width} tests/test_f28335_boot_store.c ports/tms320f28335/f28335_boot_store.c)
if(width STREQUAL "wide")
target_compile_definitions(test_f28335_store_${width} PRIVATE PCAN_BOOT_WIDE_BYTES)
endif()
add_test(NAME f28335_store_${width} COMMAND test_f28335_store_${width})
endforeach()
endif()

View File

@@ -97,8 +97,7 @@ typedef struct {
} pcan_boot_port_t;
```
Обязательны `send`, `erase_slot`, `write_slot`; в A/B также `set_pending_slot`.
В single-slot этот callback необязателен и может завершать активацию образа.
Обязательны `send`, `erase_slot`, `write_slot` и `set_pending_slot`.
`authorize` проверяет политику до стирания (тип, версия, anti-rollback).
`verify_image` выполняет платформенную проверку подписанного контейнера после
CRC32. Если callbacks отсутствуют, соответствующие дополнительные проверки
@@ -176,12 +175,5 @@ ctest --test-dir build --output-on-failure
- включить watchdog и rollback при неподтверждённом запуске;
- согласовать фильтры CAN для `MsgType=0x9..0xD`.
Ядро поддерживает платформенные порты; особенности интеграции перечислены ниже.
## F28335 / большая EEPROM
[Порт M95M02](ports/tms320f28335/README.md) подключён в `tms_periph/periph_167`.
Использует отдельный Flash-загрузчик и два слота EEPROM по 130560 байт.
Реализованы readback/CRC, проверка SCI8, журнал метаданных, пробный запуск,
CONFIRM/откат, PING/discovery и сервисный паспорт. Первичная установка Flash-
загрузчика и выбор Flash boot описаны в README порта.
Проект пока не подключён ни к одной конкретной плате; это самостоятельный
шаблон для интеграции в будущие загрузчики приборов SET.

View File

@@ -63,10 +63,6 @@ BlockIndex = 0x0000…0xFFFF
| `0x09` | `REBOOT` | 0 | отсутствует | активная сессия |
| `0x0A` | `ABORT` | 0 | отсутствует | активная сессия |
| `0x0B` | `QUERY_PROGRESS` | 0 | отсутствует | активная сессия |
| `0x0C` | `PING` | 0 | отсутствует | любое |
| `0x0D` | `CAPABILITIES` | 0 | отсутствует | любое, при поддержке порта |
| `0x0E` | `DEVICE_NAME` | 1 | номер страницы имени | любое, при поддержке порта |
| `0x0F` | `MEMORY_INFO` | 1 | номер страницы памяти | любое, при поддержке порта |
### `BEGIN_IMAGE`
@@ -85,8 +81,7 @@ DATA[4..7] FirmwareVersion, uint32 little-endian
```
До `ERASE` прибор обязан получить обе части метаданных и проверить размер,
тип изделия и аппаратную ревизию. Проверка версии/anti-rollback выполняется
только при наличии соответствующей политики платформы (callback authorize).
тип изделия, аппаратную ревизию, версию и политику anti-rollback.
## `BOOT_STATUS`
@@ -186,16 +181,3 @@ Boot metadata должна атомарно хранить:
5. ПМ выполняет `ERASE` и передаёт `BOOT_DATA_A` либо `BOOT_DATA_B`.
6. ПМ выполняет `VERIFY`, затем `COMMIT` и `REBOOT`.
7. Новое приложение после самопроверки выполняет `CONFIRM`.
## F28335 M95M02
Реализованы отдельный Flash-загрузчик, A/B по 130560 байт, журнал, пробный запуск
и откат. Подробная раскладка памяти, сервисных ответов и первичная установка:
[README порта](../ports/tms320f28335/README.md).
Общий discovery: Route=0, DeviceType=7, DeviceID=15, MsgType=D, Body=0, DLC=0.
Он только запрашивает IDENTIFY; операции изменения всегда адресные.
ENTER_BOOT может отложить ACK до готовности отдельного загрузчика после reset.
CONFIRM с ненулевой session может обрабатываться приложением без ENTER_BOOT.
`pcan_boot_poll` обслуживает задержку discovery и тайм-аут сессии, если он задан
портом. Старым портам с timeout=0 периодический вызов нужен только для discovery.

View File

@@ -11,13 +11,6 @@
#include <stddef.h>
#include <stdint.h>
/* C28x has 16-bit addressable chars; wire octets use the low eight bits. */
#if defined(__TMS320C28XX__) || defined(PCAN_BOOT_WIDE_BYTES)
typedef unsigned int pcan_boot_byte_t;
#else
typedef uint8_t pcan_boot_byte_t;
#endif
#ifdef __cplusplus
extern "C" {
#endif
@@ -48,11 +41,7 @@ typedef enum {
PCAN_BOOT_CMD_CONFIRM = 0x08U,
PCAN_BOOT_CMD_REBOOT = 0x09U,
PCAN_BOOT_CMD_ABORT = 0x0AU,
PCAN_BOOT_CMD_QUERY_PROGRESS = 0x0BU,
PCAN_BOOT_CMD_PING = 0x0CU,
PCAN_BOOT_CMD_CAPABILITIES = 0x0DU,
PCAN_BOOT_CMD_DEVICE_NAME = 0x0EU,
PCAN_BOOT_CMD_MEMORY_INFO = 0x0FU
PCAN_BOOT_CMD_QUERY_PROGRESS = 0x0BU
} pcan_boot_command_t;
typedef enum {
@@ -77,8 +66,7 @@ typedef enum {
PCAN_BOOT_STATE_READY_TO_ERASE,
PCAN_BOOT_STATE_RECEIVING,
PCAN_BOOT_STATE_VERIFIED,
PCAN_BOOT_STATE_FAILED,
PCAN_BOOT_STATE_COMMITTED
PCAN_BOOT_STATE_FAILED
} pcan_boot_state_t;
typedef struct {
@@ -86,47 +74,36 @@ typedef struct {
uint32_t image_crc32;
uint32_t firmware_version;
uint16_t product_type;
pcan_boot_byte_t hardware_revision_min;
pcan_boot_byte_t hardware_revision_max;
uint8_t hardware_revision_min;
uint8_t hardware_revision_max;
} pcan_boot_manifest_t;
typedef struct {
pcan_boot_byte_t device_type;
pcan_boot_byte_t device_id;
uint8_t device_type;
uint8_t device_id;
uint16_t product_type;
pcan_boot_byte_t hardware_revision;
uint8_t hardware_revision;
uint32_t firmware_version;
pcan_boot_byte_t active_slot;
uint8_t active_slot;
/** 1 for an in-place/single-slot target, 2 for A/B. Zero keeps legacy A/B. */
pcan_boot_byte_t slot_count;
uint8_t slot_count;
/** Writable image capacity. Zero uses PCAN_BOOT_SLOT_SIZE. */
uint32_t max_image_size;
pcan_boot_byte_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */
uint32_t session_timeout_ms; /**< 0 disables timeout for legacy ports. */
uint8_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */
} pcan_boot_config_t;
typedef struct {
bool (*send)(void *user, uint32_t can_id, const pcan_boot_byte_t *data, pcan_boot_byte_t dlc);
bool (*erase_slot)(void *user, pcan_boot_byte_t slot);
bool (*write_slot)(void *user, pcan_boot_byte_t slot, uint32_t offset,
const pcan_boot_byte_t *data, pcan_boot_byte_t length);
bool (*send)(void *user, uint32_t can_id, const uint8_t *data, uint8_t dlc);
bool (*erase_slot)(void *user, uint8_t slot);
bool (*write_slot)(void *user, uint8_t slot, uint32_t offset,
const uint8_t *data, uint8_t length);
bool (*authorize)(void *user, const pcan_boot_manifest_t *manifest);
bool (*verify_image)(void *user, pcan_boot_byte_t slot,
bool (*verify_image)(void *user, uint8_t slot,
const pcan_boot_manifest_t *manifest);
/** A/B metadata, or optional single-slot activation after verification. */
bool (*set_pending_slot)(void *user, pcan_boot_byte_t slot,
bool (*set_pending_slot)(void *user, uint8_t slot,
const pcan_boot_manifest_t *manifest);
bool (*confirm_running_slot)(void *user);
void (*reboot)(void *user);
/** ENTER: 0 ready, 1 deferred ACK after reset, 2 denied. NULL = legacy ready. */
unsigned int (*enter_boot)(void *user, pcan_boot_byte_t session);
bool (*can_reboot)(void *user);
/** Eight-octet service response, addressed by command + page. */
bool (*service_info)(void *user, pcan_boot_byte_t command,
pcan_boot_byte_t page, pcan_boot_byte_t *out);
/** Optional precise result, replacing legacy boolean verify_image. */
pcan_boot_status_t (*verify_status)(void *user, pcan_boot_byte_t slot,
const pcan_boot_manifest_t *manifest);
} pcan_boot_port_t;
typedef struct {
@@ -138,13 +115,11 @@ typedef struct {
uint32_t running_crc32;
uint32_t bytes_received;
uint16_t next_block;
pcan_boot_byte_t session_id;
pcan_boot_byte_t target_slot;
pcan_boot_byte_t last_status;
uint8_t session_id;
uint8_t target_slot;
uint8_t last_status;
bool have_image;
bool have_compat;
uint32_t now_ms, last_activity_ms, discovery_at;
bool discovery_pending;
} pcan_boot_t;
/** Инициализирует экземпляр. Все обязательные callbacks должны быть заданы. */
@@ -159,19 +134,16 @@ void pcan_boot_abort(pcan_boot_t *boot);
* Возвращает true, если кадр принадлежал загрузочному сервису этого прибора.
*/
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc);
/** Call regularly with a wrapping monotonic millisecond clock. */
void pcan_boot_poll(pcan_boot_t *boot, uint32_t now_ms);
const uint8_t *data, uint8_t dlc);
/** Переносимые операции с 29-битным ProtoCAN ID. */
uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
pcan_boot_byte_t device_type, pcan_boot_byte_t device_id,
pcan_boot_byte_t msg_type, uint16_t msg_body);
pcan_boot_byte_t pcan_boot_id_route(uint32_t can_id);
pcan_boot_byte_t pcan_boot_id_device_type(uint32_t can_id);
pcan_boot_byte_t pcan_boot_id_device_id(uint32_t can_id);
pcan_boot_byte_t pcan_boot_id_msg_type(uint32_t can_id);
uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
uint8_t device_type, uint8_t device_id,
uint8_t msg_type, uint16_t msg_body);
uint8_t pcan_boot_id_route(uint32_t can_id);
uint8_t pcan_boot_id_device_type(uint32_t can_id);
uint8_t pcan_boot_id_device_id(uint32_t can_id);
uint8_t pcan_boot_id_msg_type(uint32_t can_id);
uint16_t pcan_boot_id_msg_body(uint32_t can_id);
#ifdef __cplusplus

View File

@@ -1,156 +0,0 @@
# F28335 + M95M02: CAN, отдельный загрузчик и A/B
Реализация состоит из Flash-загрузчика `periph_167/Bootloader`, переносимого
`f28335_boot_store.c`, аппаратного `f28335_boot_hw.c` и адаптера
`f28335_can_boot.c`. Приложение подключает их через `Source/Internal/can_boot.c`.
В сборке `EEPROM_M95M02=0` CAN Boot отключён. Для установленной M95M02 — значение 1.
## Адрес и формат
Device Type **3**, Device ID **7**, имя **F28335-M95M02**, Product **0x2833**, HW **1**.
В приложении FW берётся из `firmware_version.h`, формат `major<<24 | minor<<16 | patch`;
для версии 1.1.2 это `0x01010002`. Загрузчик сообщает версию подтверждённого образа
из журнала, при отсутствии такого образа — 0. Версия кандидата передаётся в BEGIN_COMPAT.
CAN **250000 бит/с**, Extended ID, окно **16 блоков по 8 октетов**.
ENTER_BOOT для session=1: `0x13790102`, STATUS: `0x1B7C0102`.
Образ — SCI8 BIN (`AA 08`), чётная длина от 24 до **130560 байт** включительно.
Числа в протоколе little endian. Адреса внутри SCI8: сначала старшее 16-битное
слово, затем младшее; каждое слово little endian. В C28x `char` 16-битный,
поэтому `pcan_boot_byte_t` хранит октет в младших 8 битах.
## Разметка EEPROM, байтовые адреса
| Область | Начало | Конец включительно | Размер |
|---|---:|---:|---:|
| Журнал 0 | 0x00000 | 0x000FF | 256 |
| Журнал 1 | 0x00100 | 0x001FF | 256 |
| Slot A | 0x00200 | 0x1FFFF | 130560 |
| Slot B | 0x20000 | 0x3FDFF | 130560 |
| Резерв | 0x3FE00 | 0x3FEFB | 252 |
| Параметры | 0x3FEFC | 0x3FFFD | 258 |
| Последнее слово | 0x3FFFE | 0x3FFFF | 2 |
В каждой странице журнала первые 64 октета содержат magic `FAB2`, версию 1,
sequence, active/pending/attempted, два manifest, CRC32 первых 56 октетов и
commit marker в октетах 60..63. Сначала обнуляется marker запасной страницы,
затем записывается тело с проверкой чтением, последним — marker. Предыдущая
страница остаётся действующей до полной публикации новой. Выбор sequence
учитывает переполнение u32. После ошибки публикации журнал перечитывается;
ошибка чтения блокирует последующие записи.
## Сценарий обновления и отката
1. Приложение принимает ENTER_BOOT, останавливает управление нагрузкой, выставляет 4 мА и выдерживает
секундный STOP, затем сохраняет session в NOINIT RAM 0x50..0x57. Watchdog выполняет reset.
ACK ENTER_BOOT отправляет уже Flash-загрузчик; приложение ничего не стирает.
2. BEGIN_IMAGE/BEGIN_COMPAT проверяют размер, Product, HW и сессию. Неактивный
слот выбирается автоматически; при первом образе это A. ERASE инвалидирует
только его manifest. Полного стирания EEPROM не требуется.
3. Последовательные DATA_A/B записываются с немедленным readback. VERIFY
проверяет потоковый CRC32, CRC32 EEPROM и полную таблицу SCI8.
Разрешена загрузка только в CPU word RAM `[0x8000,0xE000)`: максимум 64 секции,
без пересечений, усечений и хвостовых данных; entry должен попасть в секцию.
Запись в Flash, периферию, стек, RAM загрузчика и handoff запрещена.
4. COMMIT повторно проверяет образ и публикует pending в журнале. Подтверждённый
слот сохраняется. Повтор COMMIT безопасен. REBOOT выполняется после физического
CAN ACK ответа. Потеря ACK/тайм-аут передачи отменяет запрос reset.
5. Перед первым запуском кандидата загрузчик сохраняет attempted=1, затем
проверяет и копирует SCI8 в RAM. Следующий reset без CONFIRM сбрасывает pending
и запускает прежний active. Если прежнего образа нет — остаётся CAN recovery.
6. Новое приложение включает watchdog в начале запуска. До подтверждения
прерывания не кормят watchdog вместо основного цикла. После READY, 100 проходов
main и 5 секунд выполняется автоматический CONFIRM. Неудача сохранения
подтверждения через 10 секунд прекращает кормление watchdog, вызывая откат.
Адресная команда CONFIRM с ненулевой session также разрешена после READY.
CONFIRM в загрузчике и до первого запуска кандидата отклоняется.
PING, IDENTIFY и паспорт доступны без ENTER_BOOT. QUERY_PROGRESS/ABORT привязаны
к сессии. Сессия в RAM истекает через 30 секунд без подходящих команд/данных.
После reset передачу начинают заново; параметры и подтверждённый образ остаются.
ABORT после COMMIT не отменяет опубликованный pending. Для его пробного запуска
можно открыть новую сессию и отправить REBOOT. Pending запрещено перезаписывать.
Повторы ENTER_BOOT, метаданных, ERASE до первого блока, VERIFY и COMMIT учитывают
потерю ответа. Broadcast никогда не разрешает запись.
## Сервисные команды
Запрос: MsgType=9, Body=`session<<8 | command`. IDENTIFY/PING/паспорт не требуют сессии.
Ответ паспорта: MsgType=D, Body=`page<<8 | command`, DLC=8.
| Код | Имя | DLC запроса | Ответ |
|---:|---|---:|---|
| 01 | IDENTIFY | 0 | Product:u16, HW:u8, protocol:u8, FW:u32; Body=1 |
| 0C | PING | 0 | protocol, state, active, target, session, slot_count, last_status, 0 |
| 0D | CAPABILITIES | 0 | flags:u32, slot_capacity:u32 |
| 0E | DEVICE_NAME | 1 (page) | 8 ASCII-октетов имени; последний фрагмент с NUL |
| 0F | MEMORY_INFO | 1 (page) | Страницы ниже |
MEMORY_INFO 0: EEPROM bytes:u32, page bytes:u16, mode:u8 (0 app/1 boot), running:u8.
Страница 1: адреса A:u32, B:u32. Страница 2: active, pending, attempted, storage_healthy,
session_timeout_ms:u32. Страница 3: parameters_offset:u32, parameters_bytes:u32.
Слот FF означает отсутствие образа/запущенного приложения.
CAPABILITIES bits 0..11: ping, discovery, identify, name, memory, A/B, confirm,
rollback, separate loader, session timeout, readback, SCI8 validation.
Приложение, запущенное старым SPI ROM без handoff загрузчика, сообщает только
базовые сервисы (0x1F) и отклоняет ENTER_BOOT.
Обнаружение: MsgType=D, Body=0, DLC=0, Route=0. Адресный запрос или общий
DeviceType=7/DeviceID=15. Ответ IDENTIFY приходит с адреса прибора через
`2*(1+16*DeviceType+DeviceID)` мс. При нескольких одинаковых адресах конфликт
остаётся: каждому физическому устройству нужен уникальный адрес.
Python API: `protocan.can_boot.CanBootServices` — запросы и разбор паспорта;
`CanBootTransfer` — загрузка. Кнопка «Пинг» доступна в режиме ProtoCAN Boot
окна прошивки SETGUI. Для ENTER/VERIFY/COMMIT клиент ждёт до 15 секунд.
## Аппаратная интеграция
MBOX3 — адресные входящие Boot; MBOX4 — ответы; MBOX6 — точный broadcast discovery.
MBOX0..2 сохраняют legacy/UMP, MBOX5 остаётся heartbeat. ISR помещает сообщения
в очередь на 31 кадр. Чужие адреса, RTR и неподходящие типы отбрасываются до очереди.
При переполнении/потере mailbox сессия отменяется; повторная запись начинается заново.
SPI-драйвер загрузчика имеет ограничение ожидания байта и WIP, 24-битный адрес,
разбиение по страницам 256 байт и восстановление конфигурации SPI приложения.
Он работает только при остановленном Timer2. Обычный Seeprom_write приложения
ограничен областью параметров; старый RS EXTEND 0x44 запрещён для A/B.
Загрузчик исполняется из внутренней Flash A 0x338000..0x33FF7F, entry 0x33FFF6.
Его RAM — 0xE000..0xFFFF; стек — 0x400..0x7FF. Приложение M95M02 использует
0x8000..0xD7FF для кода, 0xD800..0xDFFF для констант/инициализации,
0xE000..0xFFFF для .ebss после перехода в C runtime. Linker резервирует
NOINIT handoff в обоих проектах. CAN не содержит операций записи внутренней Flash.
## Первичная установка
1. Собрать приложение с EEPROM_M95M02=1 и обновлённым F28335.cmd; получить SCI8 BIN.
Старые BIN/OUT автоматически не заменены. Приложение должно содержать интеграцию
READY/CONFIRM, иначе на следующем reset оно останется неподтверждённым кандидатом.
2. Собрать отдельный загрузчик TI C2000 COFF compiler:
`Bootloader/build.ps1 -CompilerRoot <путь-к-ti-cgt-c2000>`.
Результат — `Bootloader/build/F28335_CAN_AB.out` и MAP.
3. Один раз установить этот OUT через JTAG и выбрать аппаратный Flash boot
согласно схеме платы. Не загружать поверх него старый application OUT с
секцией codestart во Flash.
4. После reset пустая EEPROM или старая разметка автоматически ведут в CAN recovery.
Передать новое приложение через ProtoCAN Boot, дождаться пробного запуска и
проверить MEMORY_INFO page 2: pending=FF, active=A. Затем проверить обновление в B.
Старая SPI-ROM разметка с AA08 по адресу 0 несовместима с новым журналом.
Первое обновление заменяет её; параметры по прежнему адресу сохраняются.
Загрузчик не выполняет скрытую миграцию и не меняет boot pins программно.
## Проверки и границы проверенного
`python c/protocan-boot/tests/run_tests.py` (переменная CC — clang/gcc) проверяет
ядро, приложение/загрузчик, wide-octet режим, журнал, обрыв публикации на каждом
из 69 возможных положений, пробный запуск, CONFIRM, откат, SCI8 и границы памяти.
Python-тесты проверяют клиентские запросы, паспорт, фильтрацию ответов и лимит A/B.
Проверка синтаксиса использует настоящие DSP2833x headers. Сборка TI/CCS,
раскладка итогового MAP и физические CAN/SPI/reset на плате ещё не подтверждены.
CRC32 защищает от повреждения, криптографической подписи и anti-rollback по
версии в этом порту нет. Удалённый CONFIRM подтверждает готовность приложения,
но не заменяет проверку датчиков/силовой части на стенде.
Аппаратный формат SPI boot описан в [TI Boot ROM Reference Guide](https://e2e.ti.com/cfs-file/__key/communityserver-discussions-components-files/171/2742.Boot-ROM.pdf).
В A/B-схеме ROM выбирает Flash, а разбор EEPROM выполняет этот загрузчик.

View File

@@ -1,181 +0,0 @@
/* Shared by the application and the separate Flash bootloader. */
/* Аппаратный слой: общая RAM между reset, watchdog и SPI EEPROM.
* Один исходник компилируется и в приложение, и в отдельный Flash-загрузчик.
* В отличие от store.c этот файл требует настоящих регистров F28335. */
#include "DSP2833x_Device.h"
#include "f28335_boot_platform.h"
#include "f28335_can_boot.h"
/* Must be NOINIT in BOTH linkers, outside ROM scratch and application payload. */
#pragma DATA_SECTION(boot_handoff,".boot_shared")
/* 8 слов NOINIT по адресу 0x50, одинаково зарезервированных обоими linker-файлами.
* [0..1] magic; [2] запрошенная session; [3] запущенный слот; [4..6] служебные
* поля; [7] XOR-контроль. Это кратковременная передача контекста через watchdog
* reset, а не замена энергонезависимого журнала EEPROM. */
static volatile unsigned int boot_handoff[8];
/* Проверяем magic и XOR прежде, чем трактовать RAM как запрос входа в boot.
* После включения питания содержимое NOINIT может быть произвольным. */
static unsigned int handoff_valid(void)
{
unsigned int i,sum=0;
for(i=0;i<8;++i) sum^=boot_handoff[i];
return boot_handoff[0]==0xB283 && boot_handoff[1]==0x4D7C && sum==0xA95B;
}
/* Сначала снимаем признак действительности, затем заполняем поля и контроль.
* Первый magic записываем последним; volatile сохраняет обращения к общей RAM.
* request=0 означает обычный запуск, ненулевая session — явный ENTER_BOOT. */
static void handoff_set(unsigned int request,unsigned int running)
{
unsigned int i,sum=0;
boot_handoff[0]=0; boot_handoff[1]=0x4D7C;
boot_handoff[2]=request; boot_handoff[3]=running;
boot_handoff[4]=0xF335; boot_handoff[5]=0x0CCA; boot_handoff[6]=1;
sum=0xB283;
for(i=1;i<7;++i) sum^=boot_handoff[i];
boot_handoff[7]=sum^0xA95B;
boot_handoff[0]=0xB283; /* publish last */
}
/* Приложение узнаёт свой слот из handoff, а не предполагает, что оно active:
* во время пробного запуска выполняется pending, active ещё указывает на старый образ. */
unsigned int F28335Boot_RunningSlot(void)
{ return handoff_valid()?boot_handoff[3]:F28335_AB_NONE; }
unsigned int F28335Boot_RequestedSession(void)
{ return handoff_valid() && boot_handoff[2]<256?boot_handoff[2]:0; }
/* Одновременно публикуем выбранный слот и потребляем запрос ENTER_BOOT:
* он не должен повторяться при следующем обычном reset. */
void F28335Boot_SetRunning(unsigned int slot) { handoff_set(0,slot); }
/* Timer1 должен давать 1 мс на InterruptCount и в приложении, и в загрузчике.
* Тайм-ауты используют разность unsigned-счётчиков для переноса через wrap. */
uint32_t F28335Boot_Now(void) { return CpuTimer1.InterruptCount; }
/* Вызвать сразу после InitSysCtrl, который штатно отключает watchdog.
* Сбрасываем разрешение кормления из ISR до полной инициализации CAN-сервиса.
* Для приложения, запущенного нашим загрузчиком, включаем watchdog с делителем. */
void F28335Boot_StartupWatchdog(void)
{
F28335CanBoot_EarlyInit();
if(F28335Boot_RunningSlot()<2) {
EALLOW; SysCtrlRegs.WDKEY=0x55; SysCtrlRegs.WDKEY=0xAA;
SysCtrlRegs.WDCR=0x002F; EDIS;
}
}
/* Платформенный Quiesce останавливает управление выходами перед reset.
* Session сохраняется до включения watchdog; ненулевую session загрузчик
* использует для отложенного ACK ENTER_BOOT. В бесконечном цикле watchdog
* намеренно не обслуживается — выход из функции должен произойти через reset. */
void F28335Boot_Reset(unsigned int session)
{
F28335Boot_Quiesce();
DINT;
handoff_set(session,F28335_AB_NONE);
EALLOW; SysCtrlRegs.WDCR=0x0028; EDIS;
for(;;) { }
}
/* Main context only. Legacy Timer2 EEPROM driver must be idle/stopped. Save
* its SPI configuration: it changes the character width during transactions. */
typedef struct { unsigned int ccr,ctl,brr,tx,rx,ct,pri; } boot_spi_saved;
static void spi_cs(unsigned int high)
{ if(high) GpioDataRegs.GPASET.bit.GPIO19=1; else GpioDataRegs.GPACLEAR.bit.GPIO19=1; }
/* Доступ разрешён только из main при остановленном Timer2 старого SPI-драйвера.
* Это проверка занятости в однопоточном приложении, не универсальный mutex.
* Сохраняем режим SPI, поскольку штатный драйвер меняет разрядность передачи.
* CS сначала поднимаем, затем переключаем SPI на 8-битные транзакции. */
static bool spi_take(boot_spi_saved *s)
{
volatile unsigned int discard;
if(!CpuTimer2Regs.TCR.bit.TSS) return false;
s->ccr=SpiaRegs.SPICCR.all; s->ctl=SpiaRegs.SPICTL.all; s->brr=SpiaRegs.SPIBRR;
s->tx=SpiaRegs.SPIFFTX.all; s->rx=SpiaRegs.SPIFFRX.all;
s->ct=SpiaRegs.SPIFFCT.all; s->pri=SpiaRegs.SPIPRI.all;
EALLOW;
GpioCtrlRegs.GPAMUX2.bit.GPIO16=1; GpioCtrlRegs.GPAMUX2.bit.GPIO17=1;
GpioCtrlRegs.GPAMUX2.bit.GPIO18=1; GpioCtrlRegs.GPAMUX2.bit.GPIO19=0;
GpioDataRegs.GPASET.bit.GPIO19=1; GpioCtrlRegs.GPADIR.bit.GPIO19=1;
GpioCtrlRegs.GPAQSEL2.bit.GPIO17=3;
EDIS;
SpiaRegs.SPICCR.all=7; SpiaRegs.SPICTL.all=0x0E; /* master, phase=1, IRQ off */
SpiaRegs.SPIBRR=CLKMULT*6+1;
SpiaRegs.SPIFFTX.all=0x8000; SpiaRegs.SPIFFRX.all=0; SpiaRegs.SPIFFCT.all=0;
SpiaRegs.SPIPRI.all=0x10; SpiaRegs.SPICCR.all=0x87;
/* Снимаем оставшийся RX/INT_FLAG от прежней операции, чтобы первый обмен
* не принял старое содержимое RXBUF за ответ EEPROM. */
discard=SpiaRegs.SPIRXBUF; (void)discard;
return true;
}
/* Выход и при успехе, и при ошибке: CS не должен остаться активным.
* Настройки возвращаются при удержании SPI в reset; исходный SPISWRESET
* восстанавливается последним, после остальных регистров. */
static void spi_release(const boot_spi_saved *s)
{
volatile unsigned int discard;
spi_cs(1); discard=SpiaRegs.SPIRXBUF; (void)discard;
SpiaRegs.SPICCR.all=s->ccr&0x7FU; SpiaRegs.SPICTL.all=s->ctl;
SpiaRegs.SPIBRR=s->brr; SpiaRegs.SPIFFTX.all=s->tx; SpiaRegs.SPIFFRX.all=s->rx;
SpiaRegs.SPIFFCT.all=s->ct; SpiaRegs.SPIPRI.all=s->pri; SpiaRegs.SPICCR.all=s->ccr;
}
/* У F28335 8-битный символ для передачи выравнивается в старшие биты TXBUF.
* Полученный октет читается из младших битов RXBUF. Счётчик budget ограничивает
* ожидание даже при неработающем Timer1; это число итераций, а не миллисекунды. */
static bool spi_octet(unsigned int tx,unsigned int *rx)
{
uint32_t budget=100000UL;
SpiaRegs.SPITXBUF=(tx&255U)<<8;
while(!SpiaRegs.SPISTS.bit.INT_FLAG) if(--budget==0) return false;
*rx=SpiaRegs.SPIRXBUF&255U; return true;
}
/* Команда EEPROM и 24-битный байтовый адрес, старший октет адреса первым.
* CS удерживается вызывающей функцией на всю команду и полезные данные. */
static bool spi_prefix(unsigned int command,uint32_t address)
{
unsigned int discard;
return spi_octet(command,&discard) && spi_octet((unsigned int)(address>>16),&discard) &&
spi_octet((unsigned int)(address>>8),&discard) && spi_octet((unsigned int)address,&discard);
}
/* RDSR (05h): WIP в бите 0 означает незавершённую внутреннюю запись EEPROM.
* Ожидание ограничено и 50 мс, и числом опросов на случай остановки tick.
* После каждого чтения статуса CS снимается, в том числе при ошибке SPI. */
static bool spi_ready(void)
{
unsigned int status,discard; uint32_t started=F28335Boot_Now(),budget=20000UL;
do {
spi_cs(0);
if(!spi_octet(5,&discard) || !spi_octet(255,&status)) { spi_cs(1); return false; }
spi_cs(1);
if(!(status&1U)) return true;
} while(--budget && (uint32_t)(F28335Boot_Now()-started)<50UL);
return false;
}
/* Читать разрешено всю физическую EEPROM, включая параметры. Проверка
* через вычитание защищает от переполнения адреса. spi_release обязателен
* после успешного захвата SPI независимо от результата последующего обмена. */
bool F28335Boot_Read(void *u,uint32_t address,pcan_boot_byte_t *p,unsigned int n)
{
boot_spi_saved saved; unsigned int i,value; bool ok; (void)u;
if(address>0x40000UL || n>0x40000UL-address || !spi_take(&saved)) return false;
ok=spi_ready();
spi_cs(0); if(ok) ok=spi_prefix(3,address);
for(i=0;ok && i<n;++i) { ok=spi_octet(255,&value); p[i]=value; }
spi_release(&saved); return ok;
}
/* Нижний аппаратный барьер записи: разрешены журнал и слоты до 0x3FE00.
* Резерв, параметры и последнее слово нельзя изменить даже ошибочным вызовом
* этой функции. Защита активного слота дополнительно выполняется в store.c. */
bool F28335Boot_Write(void *u,uint32_t address,const pcan_boot_byte_t *p,unsigned int n)
{
boot_spi_saved saved; unsigned int i,chunk,discard; bool ok; (void)u;
/* This driver cannot overwrite parameters, even if a caller is defective. */
if(address>0x3FE00UL || n>0x3FE00UL-address || !spi_take(&saved)) return false;
ok=spi_ready();
while(ok && n) {
/* Нельзя пересекать страницу одной командой WRITE: EEPROM может завернуть
* адрес внутри страницы. Для каждого фрагмента заново выдаём WREN (06h),
* WRITE (02h) и ждём снятия WIP. Сравнение readback выполняет верхний слой. */
chunk=256U-(unsigned int)(address&255U); if(chunk>n) chunk=n;
spi_cs(0); ok=spi_octet(6,&discard); spi_cs(1); /* WREN */
spi_cs(0); if(ok) ok=spi_prefix(2,address);
for(i=0;ok && i<chunk;++i) ok=spi_octet(p[i],&discard);
spi_cs(1); if(ok) ok=spi_ready();
n-=chunk; p+=chunk; address+=chunk;
}
spi_release(&saved); return ok;
}

View File

@@ -1,17 +0,0 @@
#ifndef F28335_BOOT_PLATFORM_H
#define F28335_BOOT_PLATFORM_H
#include "f28335_boot_store.h"
#ifndef F28335_BOOT_LOADER
#define F28335_BOOT_LOADER 0
#endif
bool F28335Boot_Read(void *u,uint32_t address,pcan_boot_byte_t *p,unsigned int n);
bool F28335Boot_Write(void *u,uint32_t address,const pcan_boot_byte_t *p,unsigned int n);
uint32_t F28335Boot_Now(void);
void F28335Boot_Reset(unsigned int session);
unsigned int F28335Boot_RunningSlot(void);
unsigned int F28335Boot_RequestedSession(void);
void F28335Boot_SetRunning(unsigned int slot);
void F28335Boot_StartupWatchdog(void);
/* Application hook must stop outputs before reset. No EEPROM operations here. */
void F28335Boot_Quiesce(void);
#endif

View File

@@ -1,254 +0,0 @@
/* Хранилище A/B без зависимости от регистров F28335.
* Здесь находятся правила сохранения образа при сбое питания и выбора слота.
* CAN отвечает за порядок команд; этот слой повторно защищает active/pending
* и границы EEPROM, даже если вызывающий код передал неверные параметры.
* Адрес EEPROM измеряется в октетах, адрес назначения SCI8 — в словах CPU. */
#include "f28335_boot_store.h"
#include <string.h>
/* Формат журнала фиксирован: u32 little endian. Нельзя сохранять C-структуру
* через memcpy: выравнивание и sizeof на C28x отличаются от обычного ПК. */
static uint32_t bs_get32(const pcan_boot_byte_t *p)
{ return (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24); }
static void bs_put32(pcan_boot_byte_t *p, uint32_t v)
{ unsigned int i; for(i=0;i<4;++i) p[i]=(pcan_boot_byte_t)((v>>(8*i))&255U); }
/* Потоковый CRC32: первый вызов получает crc=0, следующие — предыдущий результат.
* Внутренняя инверсия позволяет считать один CRC по нескольким порциям EEPROM.
* Маска 255 исключает старшие биты 16-битного представления октета на C28x.
* Это проверка повреждения данных, а не криптографическая подпись. */
uint32_t f28335_boot_crc(uint32_t crc, const pcan_boot_byte_t *p, unsigned int n)
{ unsigned int i,b; crc=~crc; for(i=0;i<n;++i) { crc^=p[i]&255U;
for(b=0;b<8;++b) crc=(crc>>1)^((crc&1U)?0xEDB88320UL:0); } return ~crc; }
static uint32_t bs_base(unsigned int slot) { return slot?F28335_AB_SLOT_B:F28335_AB_SLOT_A; }
/* Принимаем запись только при совпадении magic, версии, marker и CRC.
* Дополнительно проверяем смысл номеров слотов: pending не может быть active.
* Наличие правильного marker без правильного CRC не разрешает запуск. */
static bool bs_record_valid(const pcan_boot_byte_t *p)
{
return bs_get32(p)==0x32424146UL && bs_get32(p+4)==1 &&
bs_get32(p+60)==0x51AFC03DUL && bs_get32(p+56)==f28335_boot_crc(0,p,56) &&
(p[12]<2 || p[12]==F28335_AB_NONE) &&
(p[13]<2 || p[13]==F28335_AB_NONE) && p[14]<=1 &&
(p[13]==F28335_AB_NONE || p[13]!=p[12]);
}
/* Карта записи, смещения в октетах:
* 0..3 magic FAB2; 4..7 версия; 8..11 sequence; 12 active; 13 pending;
* 14 attempted; 15 резерв; 16..35 manifest A; 36..55 manifest B;
* 56..59 CRC первых 56 октетов; 60..63 marker завершённой публикации.
* Manifest: size, CRC, FW (по u32), Product (u16), HW min/max, 4 октета резерва. */
static void bs_decode(f28335_boot_store_t *s, const pcan_boot_byte_t *p)
{
unsigned int i; s->sequence=bs_get32(p+8); s->active=p[12];
s->pending=p[13]; s->attempted=p[14];
for(i=0;i<2;++i) {
const pcan_boot_byte_t *q=p+16+i*20;
s->image[i].image_size=bs_get32(q); s->image[i].image_crc32=bs_get32(q+4);
s->image[i].firmware_version=bs_get32(q+8);
s->image[i].product_type=(uint16_t)(q[12]|(q[13]<<8));
s->image[i].hardware_revision_min=q[14]; s->image[i].hardware_revision_max=q[15];
}
}
/* Читаем обе копии журнала. Нераспознанная разметка допускает CAN recovery
* с active=NONE; ошибка чтения запрещает запись, поскольку реальный active неизвестен.
* Эта функция не преобразует старый SCI8 по адресу 0 в новую A/B-разметку. */
bool f28335_boot_open(f28335_boot_store_t *s, const f28335_boot_io_t *io)
{
pcan_boot_byte_t a[64],b[64]; bool va,vb;
if(!s || !io || !io->read || !io->write) return false;
memset(s,0,sizeof(*s)); s->io=*io; s->active=s->pending=F28335_AB_NONE;
if(!io->read(io->user,0,a,64) || !io->read(io->user,256,b,64)) { s->io_fault=true; return false; }
va=bs_record_valid(a); vb=bs_record_valid(b);
if(!va && !vb) return true;
/* Сравнение sequence по модулю 2^32: разность менее половины диапазона
* считается более новой записью. При равенстве выбирается страница 1.
* Соседние публикации отличаются на единицу, в том числе после переполнения. */
s->page=vb && (!va || (uint32_t)(bs_get32(b+8)-bs_get32(a+8))<0x80000000UL)?1:0;
bs_decode(s,s->page?b:a); s->valid=true; return true;
}
/* Публикация выполняется в другую страницу, текущую действующую не трогаем.
* Порядок принципиален: обнулить marker -> проверить -> записать тело ->
* проверить тело -> записать marker последним -> проверить всю запись.
* При обрыве питания загрузчик выбирает прежнюю либо полностью новую запись.
* Каждый memcmp учитывает sizeof элемента, поскольку элемент здесь — октет
* протокола, но на C28x занимает адресуемое 16-битное слово. */
static bool bs_save(f28335_boot_store_t *s)
{
pcan_boot_byte_t p[64],check[64],invalid[4]={0,0,0,0}; unsigned int i;
unsigned int page=s->valid?(s->page^1U):0;
uint32_t addr=(uint32_t)page*256;
memset(p,0,sizeof(p)); bs_put32(p,0x32424146UL); bs_put32(p+4,1);
bs_put32(p+8,s->sequence+1); p[12]=s->active; p[13]=s->pending; p[14]=s->attempted;
for(i=0;i<2;++i) {
pcan_boot_byte_t *q=p+16+i*20;
bs_put32(q,s->image[i].image_size); bs_put32(q+4,s->image[i].image_crc32);
bs_put32(q+8,s->image[i].firmware_version);
q[12]=s->image[i].product_type&255U; q[13]=(s->image[i].product_type>>8)&255U;
q[14]=s->image[i].hardware_revision_min; q[15]=s->image[i].hardware_revision_max;
}
bs_put32(p+56,f28335_boot_crc(0,p,56)); bs_put32(p+60,0x51AFC03DUL);
/* The old page is never erased. A torn write cannot publish a partial record. */
if(!s->io.write(s->io.user,addr+60,invalid,4) ||
!s->io.read(s->io.user,addr+60,check,4) || memcmp(check,invalid,sizeof(invalid)) ||
!s->io.write(s->io.user,addr,p,60) ||
!s->io.read(s->io.user,addr,check,60) || memcmp(p,check,60*sizeof(*p)) ||
!s->io.write(s->io.user,addr+60,p+60,4) ||
!s->io.read(s->io.user,addr,check,64) || memcmp(p,check,sizeof(p))) return false;
s->page=page; s->sequence++; s->valid=true; return true;
}
/* Изменения сначала готовятся в копии состояния next. Рабочее состояние
* заменяем только после публикации. Если запись/контрольное чтение сорвались,
* перечитываем журнал: marker мог успеть записаться до ошибки readback.
* После неудачи чтения io_fault блокирует дальнейшие изменения хранилища. */
static bool bs_publish(f28335_boot_store_t *s, f28335_boot_store_t *next)
{
if(bs_save(next)) { *s=*next; return true; }
/* A failed final read may still have committed: reload before any more writes. */
{ f28335_boot_io_t io=s->io; if(!f28335_boot_open(s,&io)) {
s->valid=false; s->io_fault=true; return false; } }
return false;
}
/* Логический ERASE: обнуляем manifest неактивного слота в журнале.
* Саму EEPROM стирать целиком не требуется — каждый байт будет перезаписан.
* Active защищён всегда; наличие pending запрещает начало новой записи,
* пока кандидат не подтверждён либо не снят при следующем выборе загрузки. */
bool f28335_boot_prepare(f28335_boot_store_t *s, unsigned int slot)
{
f28335_boot_store_t next=*s;
if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE) return false;
memset(&next.image[slot],0,sizeof(next.image[slot]));
return bs_publish(s,&next);
}
/* Записываем не более одного CAN-блока и немедленно сравниваем прочитанное.
* Проверка n <= capacity-off выполняется после off <= capacity: это исключает
* переполнение суммы off+n и выход из слота в соседний слот или параметры. */
bool f28335_boot_write(f28335_boot_store_t *s, unsigned int slot, uint32_t off,
const pcan_boot_byte_t *p, unsigned int n)
{
pcan_boot_byte_t check[8];
if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE || !n || n>8 ||
off>F28335_AB_SLOT_BYTES || n>F28335_AB_SLOT_BYTES-off) return false;
return s->io.write(s->io.user,bs_base(slot)+off,p,n) &&
s->io.read(s->io.user,bs_base(slot)+off,check,n) && !memcmp(check,p,n*sizeof(*p));
}
/* Слово SCI8 занимает два октета файла, младший октет идёт первым.
* Смещение увеличиваем только после успешного чтения; усечённый файл отклоняем. */
static bool bs_u16(f28335_boot_store_t *s,uint32_t base,uint32_t size,uint32_t *off,uint16_t *v)
{
pcan_boot_byte_t p[2]; if(*off>size || size-*off<2 ||
!s->io.read(s->io.user,base+*off,p,2)) return false;
*v=(uint16_t)(p[0]|(p[1]<<8)); *off+=2; return true;
}
/* Особенность таблицы TI: адрес u32 передаётся старшим словом вперёд,
* при этом внутри каждого слова остаётся little endian. Не заменять на get32. */
static bool bs_addr(f28335_boot_store_t *s,uint32_t base,uint32_t size,uint32_t *off,uint32_t *v)
{
uint16_t hi,lo; if(!bs_u16(s,base,size,off,&hi) || !bs_u16(s,base,size,off,&lo)) return false;
*v=((uint32_t)hi<<16)|lo; return true;
}
/* The complete table is validated before any application RAM is written.
* Loader RAM is E000..FFFF and M0/M1. Flash, peripherals and XINTF are forbidden. */
/* writer=NULL: только разбираем и проверяем таблицу, RAM приложения не меняем.
* writer задан: повторно проверяем таблицу и переносим слова в разрешённую RAM.
* Массивы starts/ends ограничены MAX_SECTIONS; они нужны для обнаружения
* пересекающихся секций, в том числе секций с разным порядком адресов. */
static bool bs_table(f28335_boot_store_t *s,unsigned int slot,uint32_t size,
f28335_boot_ram_writer writer,void *user,uint32_t *entry)
{
uint32_t starts[F28335_AB_MAX_SECTIONS],ends[F28335_AB_MAX_SECTIONS];
uint32_t off=0,addr,end,base=bs_base(slot); unsigned int sections=0,i;
uint16_t count,word; bool entry_loaded=false;
if(!bs_u16(s,base,size,&off,&word) || word!=0x08AA) return false;
/* После ключа AA 08 пропускаем 16 резервных октетов; далее расположен entry.
* Нулевой count завершает таблицу строго на конце файла, без хвостовых данных. */
off=18; if(!bs_addr(s,base,size,&off,entry)) return false;
for(;;) {
if(!bs_u16(s,base,size,&off,&count)) return false;
if(!count) return off==size && entry_loaded;
if(sections==F28335_AB_MAX_SECTIONS || !bs_addr(s,base,size,&off,&addr)) return false;
end=addr+count;
/* Разрешён только полуинтервал RAM [0x8000,0xE000) в словах CPU.
* Верхняя граница защищает RAM работающего загрузчика; нижняя — стек,
* handoff и регистры. Проверка размера payload использует уже октеты файла. */
if(addr<0x8000UL || end>0xE000UL || end<=addr ||
off>size || (uint32_t)count*2>size-off) return false;
for(i=0;i<sections;++i) if(addr<ends[i] && end>starts[i]) return false;
starts[sections]=addr; ends[sections++]=end;
if(*entry>=addr && *entry<end) entry_loaded=true;
if(writer) { for(i=0;i<count;++i) {
if(!bs_u16(s,base,size,&off,&word) || !writer(user,addr+i,word)) return false;
} } else off+=(uint32_t)count*2;
}
}
/* VERIFY проверяет содержимое самой EEPROM, а не только CRC принятого потока.
* Затем проверяется структура SCI8 и попадание entry в загружаемую секцию.
* Выходной entry необязателен: COMMIT нужна проверка, но не адрес перехода. */
bool f28335_boot_verify(f28335_boot_store_t *s,unsigned int slot,
const pcan_boot_manifest_t *m,uint32_t *entry)
{
uint32_t off,crc=0,local_entry; unsigned int n; pcan_boot_byte_t p[32];
if(slot>1 || m->image_size<24 || m->image_size>F28335_AB_SLOT_BYTES ||
(m->image_size&1) || m->product_type!=0x2833 ||
m->hardware_revision_min>1 || m->hardware_revision_max<1) return false;
for(off=0;off<m->image_size;off+=n) {
n=(m->image_size-off<32)?(unsigned int)(m->image_size-off):32;
if(!s->io.read(s->io.user,bs_base(slot)+off,p,n)) return false;
crc=f28335_boot_crc(crc,p,n);
}
if(crc!=m->image_crc32 || !bs_table(s,slot,m->image_size,0,0,&local_entry)) return false;
if(entry) *entry=local_entry;
return true;
}
/* COMMIT публикует проверенный образ как pending, не меняя active.
* Повторная проверка EEPROM непосредственно перед публикацией не позволяет
* назначить кандидатом образ, испорченный после предыдущего VERIFY. */
bool f28335_boot_commit(f28335_boot_store_t *s,unsigned int slot,const pcan_boot_manifest_t *m)
{
f28335_boot_store_t next=*s;
if(s->io_fault || slot>1 || slot==s->active || s->pending!=F28335_AB_NONE ||
!f28335_boot_verify(s,slot,m,0)) return false;
next.image[slot]=*m; next.pending=slot; next.attempted=0;
return bs_publish(s,&next);
}
/* Подтверждать можно только действительно выбранный для пробного запуска слот.
* Требование attempted исключает подтверждение ещё ни разу не запускавшегося
* кандидата. Повтор CONFIRM уже активного слота возвращает успех без записи. */
bool f28335_boot_confirm(f28335_boot_store_t *s,unsigned int running_slot)
{
f28335_boot_store_t next=*s;
if(s->io_fault || running_slot>1) return false;
if(s->pending==F28335_AB_NONE) return s->valid && s->active==running_slot;
if(s->pending!=running_slot || !s->attempted) return false;
next.active=running_slot; next.pending=F28335_AB_NONE; next.attempted=0;
return bs_publish(s,&next);
}
/* Выбор выполняется при старте загрузчика. attempted сохраняется ДО перехода
* в приложение: после зависания, watchdog reset или потери питания следующий
* старт уже знает о незавершённой пробе и возвращает прежний active.
* Успех функции с slot=NONE означает recovery, а не разрешение перехода в RAM. */
bool f28335_boot_select(f28335_boot_store_t *s,unsigned int *slot)
{
f28335_boot_store_t next=*s; *slot=F28335_AB_NONE;
if(s->io_fault) return false;
if(s->pending!=F28335_AB_NONE) {
if(!s->attempted && f28335_boot_verify(s,s->pending,&s->image[s->pending],0)) {
next.attempted=1;
if(!bs_publish(s,&next)) return false;
*slot=s->pending; return true;
}
/* Кандидат уже запускался без подтверждения либо не прошёл проверку.
* Снимаем pending отдельной публикацией; подтверждённый active остаётся прежним. */
next.pending=F28335_AB_NONE; next.attempted=0;
if(!bs_publish(s,&next)) return false;
}
if(s->active<2 && f28335_boot_verify(s,s->active,&s->image[s->active],0)) *slot=s->active;
return true;
}
/* Сначала полная проверка CRC и всех адресов без записи, затем перенос слов.
* Платформа вызывает entry только при true; частичная загрузка при ошибке I/O
* не даёт разрешения на запуск приложения. */
bool f28335_boot_load(f28335_boot_store_t *s,unsigned int slot,
f28335_boot_ram_writer writer,void *user,uint32_t *entry)
{
return slot<2 && writer && entry && f28335_boot_verify(s,slot,&s->image[slot],entry) &&
bs_table(s,slot,s->image[slot].image_size,writer,user,entry);
}

View File

@@ -1,55 +0,0 @@
#ifndef F28335_BOOT_STORE_H
#define F28335_BOOT_STORE_H
#include "../../include/pcan_boot.h"
/* All EEPROM offsets/lengths below are OCTETS, CPU destinations are WORDS. */
/* Физическая EEPROM 256 КиБ: две страницы журнала по 256 октетов,
* два слота по 130560 октетов. С 0x3FE00 начинается резерв, с 0x3FEFC —
* прежняя область параметров. Константы ниже не являются CPU word-адресами. */
#define F28335_AB_SLOT_BYTES 0x1FE00UL
#define F28335_AB_SLOT_A 0x00200UL
#define F28335_AB_SLOT_B 0x20000UL
#define F28335_AB_PARAMS 0x3FEFCUL
#define F28335_AB_NONE 0xFFU
#define F28335_AB_RECORD 64U
#define F28335_AB_MAX_SECTIONS 64U
/* Контракт I/O: address и n измеряются в октетах, независимо от sizeof(char).
* true означает, что выполнена вся операция; короткий обмен должен вернуть false.
* Callbacks вызываются синхронно из main, а user передаётся им без изменений. */
typedef struct {
bool (*read)(void *, uint32_t, pcan_boot_byte_t *, unsigned int);
bool (*write)(void *, uint32_t, const pcan_boot_byte_t *, unsigned int);
void *user;
} f28335_boot_io_t;
typedef struct {
f28335_boot_io_t io;
/* sequence — номер последней публикации; page — выбранная копия журнала.
* active — подтверждённый слот; pending — кандидат; attempted — уже была проба.
* valid показывает наличие распознанной записи, io_fault запрещает изменения
* после ошибки чтения. Отсутствующий слот обозначается NONE, а не нулём. */
uint32_t sequence;
unsigned int page, active, pending, attempted;
bool valid, io_fault;
pcan_boot_manifest_t image[2];
} f28335_boot_store_t;
uint32_t f28335_boot_crc(uint32_t crc, const pcan_boot_byte_t *p, unsigned int n);
bool f28335_boot_open(f28335_boot_store_t *s, const f28335_boot_io_t *io);
/* Blank/unrecognised EEPROM opens successfully with active=NONE. I/O errors fail. */
bool f28335_boot_prepare(f28335_boot_store_t *s, unsigned int slot);
bool f28335_boot_write(f28335_boot_store_t *s, unsigned int slot, uint32_t offset,
const pcan_boot_byte_t *data, unsigned int n);
bool f28335_boot_verify(f28335_boot_store_t *s, unsigned int slot,
const pcan_boot_manifest_t *m, uint32_t *entry);
bool f28335_boot_commit(f28335_boot_store_t *s, unsigned int slot,
const pcan_boot_manifest_t *m);
bool f28335_boot_confirm(f28335_boot_store_t *s, unsigned int running_slot);
/* Records a trial BEFORE returning it; a second unconfirmed boot rolls back. */
bool f28335_boot_select(f28335_boot_store_t *s, unsigned int *slot);
/* Запись одного проверенного слова приложения по CPU word-адресу.
* Должна вернуть false при невозможности записи; после отказа запуск запрещён. */
typedef bool (*f28335_boot_ram_writer)(void *, uint32_t, uint16_t);
bool f28335_boot_load(f28335_boot_store_t *s, unsigned int slot,
f28335_boot_ram_writer write_word, void *user, uint32_t *entry);
#endif

View File

@@ -1,276 +0,0 @@
/* Separate Flash recovery loader and application service adapter. */
#include "DSP2833x_Device.h"
#include "f28335_can_boot.h"
#if EEPROM_M95M02
#include "f28335_boot_platform.h"
#include <string.h>
/* boot хранит текущую CAN-сессию в RAM, store — прочитанное состояние журнала.
* Сброс сессии не удаляет active/pending из EEPROM. */
static pcan_boot_t boot;
static f28335_boot_store_t store;
/* Кольцевая очередь: ISR производит кадры, main потребляет. Из 32 элементов
* используется 31, чтобы head==tail однозначно означало пустую очередь.
* Долгие операции SPI запрещены внутри обработчика CAN. */
static volatile unsigned int head,tail,overflow;
static struct { uint32_t id; unsigned int dlc; pcan_boot_byte_t data[8]; } queue[32];
/* reset_requested только планирует reset; Service ждёт завершения TX.
* ready сообщает об окончании старта приложения, confirmed — о сохранённом
* подтверждении слота. healthy отражает результат открытия журнала при Init. */
static unsigned int reset_requested,reset_session,ready,healthy,confirmed;
static uint32_t sent_at,ready_at;
static unsigned int main_passes;
static int tx_busy,tx_canceling;
/* Один исходящий mailbox исключает перезапись ещё передаваемого ответа.
* MDL содержит первые четыре октета, MDH — следующие четыре, младшим вперёд.
* Запрос CANTRS ещё не означает успешную доставку: CANTA проверяется в Service.
* Аппаратный CAN ACK не является отдельным прикладным подтверждением от ПК. */
static bool transmit(void *u, uint32_t id, const pcan_boot_byte_t *d, pcan_boot_byte_t n)
{
unsigned int i; unsigned long lo=0, hi=0; (void)u;
if (tx_busy) return false;
for(i=0;i<n;++i) {
if(i<4) lo|=(unsigned long)(d[i]&255U)<<(8U*i);
else hi|=(unsigned long)(d[i]&255U)<<(8U*(i-4U));
}
EALLOW;
ECanaRegs.CANME.all &= ~16UL;
ECanaMboxes.MBOX4.MSGID.all=0x80000000UL|id;
ECanaMboxes.MBOX4.MSGCTRL.all=n;
ECanaMboxes.MBOX4.MDL.all=lo; ECanaMboxes.MBOX4.MDH.all=hi;
ECanaRegs.CANME.all |=16UL;
ECanaRegs.CANTA.all=16UL; ECanaRegs.CANAA.all=16UL;
ECanaRegs.CANTRS.all=16UL;
EDIS;
sent_at=F28335Boot_Now(); tx_busy=1; tx_canceling=0; return true;
}
/* Изменение образа разрешено только сборке Flash-загрузчика. В приложении
* эти callbacks возвращают отказ, даже если им удастся передать команду записи.
* ERASE для EEPROM означает инвалидирование manifest выбранного слота. */
static bool erase(void *u,pcan_boot_byte_t slot)
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_prepare(&store,slot); }
static bool write_block(void *u,pcan_boot_byte_t slot,uint32_t offset,const pcan_boot_byte_t *d,pcan_boot_byte_t n)
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_write(&store,slot,offset,d,n); }
/* Проверка включает CRC EEPROM и структуру SCI8. Сейчас отказ этого слоя
* кодируется CRC_ERROR как общий отказ проверки образа; причины внутри
* парсера SCI8 отдельными статусами протокола не различаются. */
static pcan_boot_status_t verify(void *u,pcan_boot_byte_t slot,const pcan_boot_manifest_t *m)
{
(void)u;
if(!F28335_BOOT_LOADER || !healthy) return PCAN_BOOT_STATUS_INVALID_STATE;
return f28335_boot_verify(&store,slot,m,0)?PCAN_BOOT_STATUS_OK:PCAN_BOOT_STATUS_CRC_ERROR;
}
static bool commit(void *u,pcan_boot_byte_t slot,const pcan_boot_manifest_t *m)
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_commit(&store,slot,m); }
/* Подтверждение принадлежит приложению после READY. Загрузчик не может
* подтвердить кандидата сам: иначе исчезнет смысл проверки пробного запуска.
* Разрешение кормления watchdog из ISR открываем только после записи журнала. */
static bool confirm(void *u)
{
unsigned int running=F28335Boot_RunningSlot(); (void)u;
if(F28335_BOOT_LOADER || !healthy || !ready || running>1) return false;
if(!f28335_boot_confirm(&store,running)) return false;
boot.config.active_slot=running; confirmed=1; return true;
}
static bool can_reboot(void *u) { (void)u; return healthy; }
static void reboot(void *u) { (void)u; reset_session=0; reset_requested=1; }
/* Контракт enter_boot: 0 — загрузчик готов и ядро может отправить ACK;
* 1 — переход отложен, ACK придёт после reset; 2 — вход запрещён.
* Приложение требует handoff нашего загрузчика и подтверждённый текущий слот,
* чтобы не начать обновление из старого SPI-ROM старта или незавершённой пробы. */
static unsigned int enter(void *u,pcan_boot_byte_t session)
{
(void)u;
if(!healthy) return 2;
if(F28335_BOOT_LOADER) return 0;
/* No destructive fallback on boards still starting through the SPI ROM. */
if(F28335Boot_RunningSlot()>1 || !confirmed) return 2;
reset_session=session; reset_requested=1; return 1;
}
static void info32(pcan_boot_byte_t *p,uint32_t v)
{ unsigned int i; for(i=0;i<4;++i) p[i]=(pcan_boot_byte_t)((v>>(i*8))&255U); }
/* Паспорт помещается в classic CAN по страницам из восьми октетов.
* NAME возвращает фрагменты строки с завершающим NUL; неизвестная страница
* отклоняется. MEMORY: 0 — ёмкость/страница/режим/running, 1 — базы A/B,
* 2 — active/pending/attempted/healthy/тайм-аут, 3 — область параметров. */
static bool info(void *u,pcan_boot_byte_t command,pcan_boot_byte_t page,pcan_boot_byte_t *out)
{
static const char name[]=F28335_CAN_BOOT_DEVICE_NAME;
unsigned int i,index; (void)u;
if(command==PCAN_BOOT_CMD_CAPABILITIES && page==0) {
/* bits: ping, discovery, identity, name, memory, A/B, confirm, rollback,
* separate loader, session timeout, readback, SCI8 validation. */
info32(out,(F28335_BOOT_LOADER || F28335Boot_RunningSlot()<2)?0xFFFUL:0x1FUL);
info32(out+4,F28335_AB_SLOT_BYTES); return true;
}
if(command==PCAN_BOOT_CMD_DEVICE_NAME) {
if((unsigned int)page*8U>=sizeof(name)) return false;
for(i=0;i<8;++i) { index=(unsigned int)page*8U+i;
out[i]=index<sizeof(name)?(pcan_boot_byte_t)name[index]:0; }
return true;
}
if(command!=PCAN_BOOT_CMD_MEMORY_INFO) return false;
if(page==0) { info32(out,0x40000UL); out[4]=0; out[5]=1;
out[6]=F28335_BOOT_LOADER?1:0; out[7]=F28335Boot_RunningSlot(); return true; }
if(page==1) { info32(out,F28335_AB_SLOT_A); info32(out+4,F28335_AB_SLOT_B); return true; }
if(page==2) { out[0]=store.active; out[1]=store.pending; out[2]=store.attempted;
out[3]=healthy?1:0; info32(out+4,boot.config.session_timeout_ms); return true; }
if(page==3) { info32(out,F28335_AB_PARAMS); info32(out+4,258); return true; }
return false;
}
/* Вызывается до разрешения прикладных IRQ. Нельзя полагаться на случайное
* содержимое RAM или оставшееся состояние confirmed от предыдущей программы. */
void F28335CanBoot_EarlyInit(void) { confirmed=0; ready=0; }
/* Общий Init для приложения и загрузчика: открываем журнал, привязываем
* callbacks, задаём идентификацию, ёмкость слота, окно 16 и тайм-аут 30 секунд.
* В загрузчике FW берётся из active manifest; приложение сообщает версию сборки. */
void F28335CanBoot_Init(void)
{
pcan_boot_config_t cfg; pcan_boot_port_t port;
f28335_boot_io_t io; unsigned int running=F28335Boot_RunningSlot();
memset(&cfg,0,sizeof(cfg)); memset(&port,0,sizeof(port));
io.read=F28335Boot_Read; io.write=F28335Boot_Write; io.user=0;
healthy=f28335_boot_open(&store,&io);
cfg.device_type=F28335_CAN_BOOT_DEVICE_TYPE; cfg.device_id=F28335_CAN_BOOT_DEVICE_ID;
cfg.product_type=F28335_CAN_BOOT_PRODUCT; cfg.hardware_revision=1;
#if F28335_BOOT_LOADER
cfg.firmware_version=store.active<2?store.image[store.active].firmware_version:0;
#else
cfg.firmware_version=F28335_CAN_BOOT_FIRMWARE_VERSION;
#endif
head=tail=overflow=reset_requested=reset_session=ready=main_passes=0; tx_busy=tx_canceling=0;
confirmed=healthy && running<2 && store.active==running && store.pending==F28335_AB_NONE;
cfg.active_slot=store.active;
cfg.slot_count=2; cfg.max_image_size=F28335_AB_SLOT_BYTES; cfg.ack_window=16;
cfg.session_timeout_ms=30000UL;
port.send=transmit; port.erase_slot=erase; port.write_slot=write_block;
port.verify_status=verify; port.set_pending_slot=commit; port.reboot=reboot;
port.confirm_running_slot=confirm; port.enter_boot=enter;
port.can_reboot=can_reboot; port.service_info=info;
if(!pcan_boot_init(&boot,&cfg,&port,0)) return;
EALLOW;
/* Меняем только MBOX3/4/6: маска 88=8+16+64. MBOX0..2 — старый обмен,
* MBOX5 — heartbeat. Приём broadcast выделен в отдельный точный mailbox,
* чтобы слишком широкая маска Boot не перехватывала сообщения приложения.
* 72=8+64 — оба RX, 16 — TX. Регистры флагов очищаются записью единицы. */
ECanaRegs.CANME.all &= ~88UL;
ECanaMboxes.MBOX3.MSGID.all=0xC0000000UL|pcan_boot_make_id(0,0,cfg.device_type,cfg.device_id,0,0);
ECanaLAMRegs.LAM3.all=0x100FFFFFUL;
ECanaMboxes.MBOX3.MSGCTRL.all=8;
/* MBOX5 belongs to the application's heartbeat. MBOX6 is discovery only. */
ECanaMboxes.MBOX6.MSGID.all=0xC0000000UL|pcan_boot_make_id(0,0,7,15,13,0);
ECanaLAMRegs.LAM6.all=0x10000000UL; /* Ignore priority only. */
ECanaMboxes.MBOX6.MSGCTRL.all=0;
ECanaRegs.CANMD.all=(ECanaRegs.CANMD.all|72UL)&~16UL;
ECanaRegs.CANRMP.all=72UL;
ECanaRegs.CANMIM.all |=72UL;
ECanaRegs.CANME.all |=88UL;
EDIS;
}
/* ISR только проверяет заголовок и копирует кадр. Чужие адреса и RTR
* отбрасываются до очереди. Глобальный адрес допускается только для discovery;
* DLC и смысл команды окончательно проверит ядро в основном цикле. */
static void receive_box(volatile struct MBOX *box,uint32_t mask)
{
unsigned int next=(head+1U)&31U, i;
unsigned long lo, hi;
uint32_t id; unsigned int type;
if(!(ECanaRegs.CANRMP.all&mask)) return;
if(box->MSGCTRL.bit.RTR) { ECanaRegs.CANRMP.all=mask; return; }
id=box->MSGID.all&0x1FFFFFFFUL;
type=pcan_boot_id_msg_type(id);
if(!(box->MSGID.all&0x80000000UL) || pcan_boot_id_route(id)!=0 ||
(type!=9 && type!=10 && type!=11 && type!=13) ||
!((pcan_boot_id_device_type(id)==F28335_CAN_BOOT_DEVICE_TYPE &&
pcan_boot_id_device_id(id)==F28335_CAN_BOOT_DEVICE_ID) ||
(type==13 && pcan_boot_id_device_type(id)==7 && pcan_boot_id_device_id(id)==15 &&
pcan_boot_id_msg_body(id)==0))) { ECanaRegs.CANRMP.all=mask; return; }
if(next==tail) overflow=1;
else {
queue[head].id=box->MSGID.all&0x1FFFFFFFUL;
queue[head].dlc=box->MSGCTRL.bit.DLC;
lo=box->MDL.all; hi=box->MDH.all;
for(i=0;i<4;++i) { queue[head].data[i]=(lo>>(8U*i))&255U;
queue[head].data[i+4]=(hi>>(8U*i))&255U; }
head=next;
}
ECanaRegs.CANRMP.all=mask;
}
void F28335CanBoot_RxISR(void)
{
receive_box(&ECanaMboxes.MBOX3,8UL);
receive_box(&ECanaMboxes.MBOX6,64UL);
}
/* Вызывать после инициализации параметров, датчиков и прикладного режима.
* От этого момента отсчитывается интервал наблюдения перед авто-CONFIRM. */
void F28335CanBoot_Ready(void)
{ ready=1; ready_at=F28335Boot_Now(); }
/* Используется таймерными ISR приложения. Во время пробы кормление watchdog
* должно доказывать выполнение main: работа одного ISR не доказывает готовность.
* Старый старт без handoff сохраняет прежнюю политику watchdog. */
unsigned int F28335CanBoot_WatchdogAllowed(void)
{ return F28335Boot_RunningSlot()>1 || confirmed; }
/* Неблокирующий диспетчер относительно ожидания CAN: вызывать в каждом проходе main.
* Сами callbacks проверки/записи EEPROM синхронные. Порядок здесь важен:
* проба приложения -> завершение TX -> reset -> poll -> ошибки RX -> один кадр. */
void F28335CanBoot_Service(void)
{
uint32_t now=F28335Boot_Now();
if(!F28335_BOOT_LOADER && ready && !confirmed && healthy && F28335Boot_RunningSlot()<2) {
/* A trial must keep executing main, not merely a timer ISR. */
/* После 100 проходов main и 5 секунд пробуем сохранить CONFIRM.
* Если до 10 секунд сохранение не удалось, main перестаёт кормить watchdog,
* что позволяет следующему старту откатиться к подтверждённому образу. */
if((uint32_t)(now-ready_at)<10000UL) ServiceDog();
if(main_passes<100) ++main_passes;
if(main_passes==100 && (uint32_t)(now-ready_at)>=5000UL) (void)confirm(0);
}
if(tx_busy) {
/* После CANTRR ждём завершения отмены или запоздавшего ACK, прежде чем
* повторно использовать mailbox. reset_requested к этому моменту уже снят,
* поэтому позднее подтверждение не должно перезапустить устройство. */
if(tx_canceling) {
if(!((ECanaRegs.CANAA.all|ECanaRegs.CANTA.all)&16UL)) return;
ECanaRegs.CANAA.all=16UL; ECanaRegs.CANTA.all=16UL;
tx_busy=tx_canceling=0;
} else if((uint32_t)(now-sent_at)>250UL) {
/* Cancel reset immediately, even if a late CAN ACK races CANTRR. */
reset_requested=0; pcan_boot_abort(&boot); tail=head;
tx_canceling=1; ECanaRegs.CANTRR.all=16UL; return;
} else if(ECanaRegs.CANTA.all&16UL) {
ECanaRegs.CANTA.all=16UL; tx_busy=0;
} else return;
}
if(reset_requested) { F28335Boot_Reset(reset_session); return; }
pcan_boot_poll(&boot,now);
if(tx_busy) return;
/* После потери кадра нельзя молча продолжать запись: сбрасываем RAM-сессию
* и накопленную очередь. Подтверждённый образ и опубликованный pending сохраняются. */
if(overflow || (ECanaRegs.CANRML.all&72UL)) {
overflow=0; ECanaRegs.CANRML.all=72UL;
pcan_boot_abort(&boot); tail=head; return;
}
if(tail!=head) {
pcan_boot_process(&boot,queue[tail].id,queue[tail].data,queue[tail].dlc);
tail=(tail+1U)&31U;
}
}
/* Используем session из NOINIT только для нового ENTER_BOOT после reset.
* Это отправка отложенного ACK готовности, а не восстановление принятых блоков
* или CRC незавершённой передачи: данные сессии через reset не сохраняются. */
void F28335CanBoot_Resume(unsigned int session)
{
if(F28335_BOOT_LOADER && session && session<256)
(void)pcan_boot_process(&boot,pcan_boot_make_id(1,0,
F28335_CAN_BOOT_DEVICE_TYPE,F28335_CAN_BOOT_DEVICE_ID,9,(session<<8)|2),0,0);
}
#else
void F28335CanBoot_EarlyInit(void) {}
void F28335CanBoot_Init(void) {}
void F28335CanBoot_RxISR(void) {}
void F28335CanBoot_Service(void) {}
void F28335CanBoot_Ready(void) {}
unsigned int F28335CanBoot_WatchdogAllowed(void) { return 1; }
void F28335CanBoot_Resume(unsigned int session) { (void)session; }
#endif

View File

@@ -1,28 +0,0 @@
#ifndef F28335_CAN_BOOT_H
#define F28335_CAN_BOOT_H
#include "../../include/pcan_boot.h"
/* Приложение задаёт версию из firmware_version.h до подключения этого заголовка.
* Формат поля FW: major[31:24], minor[23:16], patch[15:0]. Ноль — запасное
* значение при отсутствии версии сборки, а не прошитая здесь версия устройства. */
#ifndef F28335_CAN_BOOT_FIRMWARE_VERSION
#define F28335_CAN_BOOT_FIRMWARE_VERSION 0UL /* Board supplies the build identity. */
#endif
/* Адрес ProtoCAN состоит из Device Type (3 бита) и Device ID (4 бита).
* ProductType — отдельный код совместимости прошивки; это не CAN ID и не RS-адрес. */
#define F28335_CAN_BOOT_DEVICE_TYPE 3U
#define F28335_CAN_BOOT_DEVICE_ID 7U
#define F28335_CAN_BOOT_PRODUCT 0x2833U
#define F28335_CAN_BOOT_DEVICE_NAME "F28335-M95M02"
/* Порядок интеграции приложения:
* StartupWatchdog/EarlyInit сразу после InitSysCtrl; Init после настройки
* таймера и EEPROM; Ready после старта прикладных подсистем; Service в main.
* RxISR вызывается из CAN ISR, WatchdogAllowed проверяется перед кормлением
* watchdog таймерным ISR. Resume применяется только отдельным загрузчиком. */
void F28335CanBoot_Init(void);
void F28335CanBoot_EarlyInit(void);
void F28335CanBoot_RxISR(void);
void F28335CanBoot_Service(void);
void F28335CanBoot_Ready(void);
void F28335CanBoot_Resume(unsigned int session);
unsigned int F28335CanBoot_WatchdogAllowed(void);
#endif

View File

@@ -1,16 +1,21 @@
/* ProtoCAN update state machine; platform owns storage and reset. */
#include "../include/pcan_boot.h"
/*
* Автомат адресной загрузки прошивки по ProtoCAN с сессией, контрольной суммой и A/B-слотами.
* Приём блоков, проверка образа и активация разделены на этапы; операции Flash и метаданные
* слотов предоставляет платформа.
*/
#include "pcan_boot.h"
#include <string.h>
#define PCAN_BOOT_ID_MASK 0x1FFFFFFFUL
static uint16_t get_u16(const pcan_boot_byte_t *p)
static uint16_t get_u16(const uint8_t *p)
{
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static uint32_t get_u32(const pcan_boot_byte_t *p)
static uint32_t get_u32(const uint8_t *p)
{
return (uint32_t)p[0]
| ((uint32_t)p[1] << 8)
@@ -18,26 +23,26 @@ static uint32_t get_u32(const pcan_boot_byte_t *p)
| ((uint32_t)p[3] << 24);
}
static void put_u16(pcan_boot_byte_t *p, uint16_t value)
static void put_u16(uint8_t *p, uint16_t value)
{
p[0] = (pcan_boot_byte_t)(value & 0xFFU);
p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU);
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
}
static void put_u32(pcan_boot_byte_t *p, uint32_t value)
static void put_u32(uint8_t *p, uint32_t value)
{
p[0] = (pcan_boot_byte_t)(value & 0xFFU);
p[1] = (pcan_boot_byte_t)((value >> 8) & 0xFFU);
p[2] = (pcan_boot_byte_t)((value >> 16) & 0xFFU);
p[3] = (pcan_boot_byte_t)(value >> 24);
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
p[2] = (uint8_t)(value >> 16);
p[3] = (uint8_t)(value >> 24);
}
static uint32_t crc32_update(uint32_t crc, const pcan_boot_byte_t *data, size_t length)
static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length)
{
size_t i;
crc = ~crc;
for (i = 0U; i < length; ++i) {
pcan_boot_byte_t bit;
uint8_t bit;
crc ^= data[i];
for (bit = 0U; bit < 8U; ++bit) {
crc = (crc >> 1) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U);
@@ -46,9 +51,9 @@ static uint32_t crc32_update(uint32_t crc, const pcan_boot_byte_t *data, size_t
return ~crc;
}
uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
pcan_boot_byte_t device_type, pcan_boot_byte_t device_id,
pcan_boot_byte_t msg_type, uint16_t msg_body)
uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route,
uint8_t device_type, uint8_t device_id,
uint8_t msg_type, uint16_t msg_body)
{
return ((((uint32_t)priority & 1U) << 28)
| (((uint32_t)route & 1U) << 27)
@@ -58,14 +63,14 @@ uint32_t pcan_boot_make_id(pcan_boot_byte_t priority, pcan_boot_byte_t route,
| msg_body) & PCAN_BOOT_ID_MASK;
}
pcan_boot_byte_t pcan_boot_id_route(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 27) & 1U); }
pcan_boot_byte_t pcan_boot_id_device_type(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 24) & 7U); }
pcan_boot_byte_t pcan_boot_id_device_id(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 20) & 15U); }
pcan_boot_byte_t pcan_boot_id_msg_type(uint32_t can_id) { return (pcan_boot_byte_t)((can_id >> 16) & 15U); }
uint8_t pcan_boot_id_route(uint32_t can_id) { return (uint8_t)((can_id >> 27) & 1U); }
uint8_t pcan_boot_id_device_type(uint32_t can_id) { return (uint8_t)((can_id >> 24) & 7U); }
uint8_t pcan_boot_id_device_id(uint32_t can_id) { return (uint8_t)((can_id >> 20) & 15U); }
uint8_t pcan_boot_id_msg_type(uint32_t can_id) { return (uint8_t)((can_id >> 16) & 15U); }
uint16_t pcan_boot_id_msg_body(uint32_t can_id) { return (uint16_t)can_id; }
static bool send_frame(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t body,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body,
const uint8_t *data, uint8_t dlc)
{
uint32_t id = pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_DEVICE,
boot->config.device_type,
@@ -74,12 +79,9 @@ static bool send_frame(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t bo
return boot->port.send(boot->port_user, id, data, dlc);
}
/* STATUS всегда содержит состояние передачи: status, target_slot, next_block, CRC32.
* CRC здесь относится к уже принятому потоку, а не заменяет проверку EEPROM.
* Номер следующего блока помогает клиенту продолжить обмен после потери ACK. */
static bool send_status(pcan_boot_t *boot, pcan_boot_byte_t command, pcan_boot_byte_t status)
static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status)
{
pcan_boot_byte_t data[8];
uint8_t data[8];
uint16_t body = (uint16_t)(((uint16_t)boot->session_id << 8) | command);
data[0] = status;
data[1] = boot->target_slot;
@@ -89,11 +91,9 @@ static bool send_status(pcan_boot_t *boot, pcan_boot_byte_t command, pcan_boot_b
return send_frame(boot, PCAN_BOOT_MSG_STATUS, body, data, 8U);
}
/* IDENTIFY имеет отдельный тип сообщения DISCOVERY и Body=1. Он доступен
* без открытия сессии, поэтому обнаружение устройства не должно запускать обновление. */
static bool send_identity(pcan_boot_t *boot)
{
pcan_boot_byte_t data[8];
uint8_t data[8];
put_u16(&data[0], boot->config.product_type);
data[2] = boot->config.hardware_revision;
data[3] = PCAN_BOOT_VERSION;
@@ -101,9 +101,6 @@ static bool send_identity(pcan_boot_t *boot)
return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, 1U, data, 8U);
}
/* Отменяется только состояние протокола в RAM: manifest, счётчики и session.
* Функция не стирает EEPROM и не снимает ранее опубликованный pending.
* Ответственность за постоянное состояние слотов остаётся у платформы. */
void pcan_boot_abort(pcan_boot_t *boot)
{
if (boot == NULL) {
@@ -121,24 +118,19 @@ void pcan_boot_abort(pcan_boot_t *boot)
boot->have_compat = false;
}
/* Конфигурация копируется в экземпляр; указатели callbacks остаются привязаны
* к port_user. Значения slot_count=0 и max_image_size=0 сохраняют исторические
* настройки по умолчанию. Для пустого A/B-хранилища допустим active_slot=NONE. */
bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
const pcan_boot_port_t *port, void *port_user)
{
pcan_boot_byte_t slot_count;
uint8_t slot_count;
if ((boot == NULL) || (config == NULL) || (port == NULL)
|| (config->device_type > 7U) || (config->device_id > 15U)
|| ((config->active_slot > 1U) && (config->active_slot != PCAN_BOOT_SLOT_NONE))
|| (config->active_slot > 1U)
|| (port->send == NULL) || (port->erase_slot == NULL)
|| (port->write_slot == NULL)) {
return false;
}
slot_count = config->slot_count == 0U ? 2U : config->slot_count;
if ((slot_count > 2U) || ((config->active_slot >= slot_count) &&
(config->active_slot != PCAN_BOOT_SLOT_NONE || slot_count != 2U))
|| config->max_image_size > PCAN_BOOT_SLOT_SIZE
if ((slot_count > 2U) || (config->active_slot >= slot_count)
|| ((slot_count == 2U) && (port->set_pending_slot == NULL))) {
return false;
}
@@ -157,10 +149,7 @@ bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config,
return true;
}
/* BEGIN_IMAGE и BEGIN_COMPAT приходят отдельными кадрами. До получения обоих
* не разрешаем ERASE. Проверяем размер, Product, HW и необязательную политику
* authorize. В A/B выбираем противоположный active слот; для первого образа — A. */
static bool metadata_complete(pcan_boot_t *boot, pcan_boot_byte_t command)
static bool metadata_complete(pcan_boot_t *boot, uint8_t command)
{
if (!boot->have_image || !boot->have_compat) {
boot->state = PCAN_BOOT_STATE_METADATA;
@@ -171,11 +160,8 @@ static bool metadata_complete(pcan_boot_t *boot, pcan_boot_byte_t command)
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
}
if (boot->manifest.product_type != boot->config.product_type) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_DEVICE);
}
if ((boot->config.hardware_revision < boot->manifest.hardware_revision_min)
if ((boot->manifest.product_type != boot->config.product_type)
|| (boot->config.hardware_revision < boot->manifest.hardware_revision_min)
|| (boot->config.hardware_revision > boot->manifest.hardware_revision_max)) {
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_HARDWARE);
@@ -185,51 +171,19 @@ static bool metadata_complete(pcan_boot_t *boot, pcan_boot_byte_t command)
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR);
}
boot->target_slot = boot->config.active_slot == PCAN_BOOT_SLOT_NONE ? 0U :
boot->config.slot_count == 1U
boot->target_slot = boot->config.slot_count == 1U
? boot->config.active_slot
: (pcan_boot_byte_t)(boot->config.active_slot ^ 1U);
: (uint8_t)(boot->config.active_slot ^ 1U);
boot->state = PCAN_BOOT_STATE_READY_TO_ERASE;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
}
/* Body состоит из session в старшем октете и command в младшем.
* Сначала проверяем DLC, чтобы обработчики не читали отсутствующие данные.
* Идентификация и паспорт не открывают сессию; CONFIRM обрабатывается отдельно,
* поскольку приложение не должно входить в загрузчик ради подтверждения себя.
* Остальные команды изменяют RAM-автомат только в допустимой сессии/состоянии. */
static bool process_control(pcan_boot_t *boot, uint16_t body,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
const uint8_t *data, uint8_t dlc)
{
pcan_boot_byte_t session = (pcan_boot_byte_t)(body >> 8);
pcan_boot_byte_t command = (pcan_boot_byte_t)(body & 0xFFU);
uint8_t session = (uint8_t)(body >> 8);
uint8_t command = (uint8_t)body;
if (dlc != ((command == PCAN_BOOT_CMD_BEGIN_IMAGE || command == PCAN_BOOT_CMD_BEGIN_COMPAT) ? 8U :
(command == PCAN_BOOT_CMD_DEVICE_NAME || command == PCAN_BOOT_CMD_MEMORY_INFO) ? 1U : 0U)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
}
if (command == PCAN_BOOT_CMD_PING) {
pcan_boot_byte_t reply[8] = {0};
reply[0] = PCAN_BOOT_VERSION; reply[1] = boot->state;
reply[2] = boot->config.active_slot; reply[3] = boot->target_slot;
reply[4] = boot->session_id; reply[5] = boot->config.slot_count;
reply[6] = boot->last_status;
return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, command, reply, 8);
}
if (command >= PCAN_BOOT_CMD_CAPABILITIES && command <= PCAN_BOOT_CMD_MEMORY_INFO) {
pcan_boot_byte_t reply[8] = {0};
pcan_boot_byte_t page = dlc ? data[0] : 0;
if (!boot->port.service_info || !boot->port.service_info(boot->port_user,command,page,reply))
return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_COMMAND);
return send_frame(boot,PCAN_BOOT_MSG_DISCOVERY,(uint16_t)((page<<8)|command),reply,8);
}
if (command == PCAN_BOOT_CMD_CONFIRM) {
pcan_boot_byte_t saved=boot->session_id; bool sent;
boot->session_id=session;
sent=send_status(boot,command,session && boot->port.confirm_running_slot &&
boot->port.confirm_running_slot(boot->port_user) ? PCAN_BOOT_STATUS_OK : PCAN_BOOT_STATUS_INVALID_STATE);
boot->session_id=saved; return sent;
}
if (command == PCAN_BOOT_CMD_IDENTIFY) {
return send_identity(boot);
}
@@ -237,18 +191,8 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
if (session == 0U) {
return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR);
}
if (boot->state != PCAN_BOOT_STATE_IDLE && boot->session_id != session)
return send_status(boot,command,PCAN_BOOT_STATUS_BUSY);
if (boot->port.enter_boot) {
unsigned int result=boot->port.enter_boot(boot->port_user,session);
if(result==1) return true;
if(result!=0) return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_STATE);
}
if (boot->session_id==session && boot->state!=PCAN_BOOT_STATE_IDLE)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
pcan_boot_abort(boot);
boot->session_id = session;
boot->last_activity_ms=boot->now_ms;
boot->state = PCAN_BOOT_STATE_METADATA;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
}
@@ -256,11 +200,8 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR);
}
boot->last_activity_ms=boot->now_ms;
switch (command) {
case PCAN_BOOT_CMD_BEGIN_IMAGE:
if(boot->state==PCAN_BOOT_STATE_READY_TO_ERASE && boot->manifest.image_size==get_u32(data) &&
boot->manifest.image_crc32==get_u32(data+4)) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -270,9 +211,6 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return metadata_complete(boot, command);
case PCAN_BOOT_CMD_BEGIN_COMPAT:
if(boot->state==PCAN_BOOT_STATE_READY_TO_ERASE && boot->manifest.product_type==get_u16(data) &&
boot->manifest.hardware_revision_min==data[2] && boot->manifest.hardware_revision_max==data[3] &&
boot->manifest.firmware_version==get_u32(data+4)) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -284,8 +222,6 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return metadata_complete(boot, command);
case PCAN_BOOT_CMD_ERASE:
if(boot->state==PCAN_BOOT_STATE_RECEIVING && boot->bytes_received==0)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if (boot->state != PCAN_BOOT_STATE_READY_TO_ERASE) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
@@ -300,8 +236,6 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_VERIFY:
if(boot->state==PCAN_BOOT_STATE_VERIFIED || boot->state==PCAN_BOOT_STATE_COMMITTED)
return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if ((boot->state != PCAN_BOOT_STATE_RECEIVING)
|| (boot->bytes_received != boot->manifest.image_size)) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
@@ -310,13 +244,7 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
boot->state = PCAN_BOOT_STATE_FAILED;
return send_status(boot, command, PCAN_BOOT_STATUS_CRC_ERROR);
}
if (boot->port.verify_status) {
pcan_boot_status_t status=boot->port.verify_status(boot->port_user,boot->target_slot,&boot->manifest);
if(status!=PCAN_BOOT_STATUS_OK) {
boot->state=PCAN_BOOT_STATE_FAILED; return send_status(boot,command,status);
}
}
if (!boot->port.verify_status && (boot->port.verify_image != NULL)
if ((boot->port.verify_image != NULL)
&& !boot->port.verify_image(boot->port_user, boot->target_slot,
&boot->manifest)) {
boot->state = PCAN_BOOT_STATE_FAILED;
@@ -326,16 +254,21 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_COMMIT:
if(boot->state==PCAN_BOOT_STATE_COMMITTED) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
if (boot->state != PCAN_BOOT_STATE_VERIFIED) {
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE);
}
if ((boot->port.set_pending_slot != NULL)
if ((boot->config.slot_count == 2U)
&& !boot->port.set_pending_slot(boot->port_user, boot->target_slot,
&boot->manifest)) {
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
}
boot->state=PCAN_BOOT_STATE_COMMITTED;
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_CONFIRM:
if ((boot->port.confirm_running_slot == NULL)
|| !boot->port.confirm_running_slot(boot->port_user)) {
return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR);
}
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_QUERY_PROGRESS:
@@ -347,9 +280,7 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
return send_status(boot, command, PCAN_BOOT_STATUS_OK);
case PCAN_BOOT_CMD_REBOOT:
if(!boot->port.reboot || (boot->port.can_reboot && !boot->port.can_reboot(boot->port_user)))
return send_status(boot,command,PCAN_BOOT_STATUS_INVALID_STATE);
if(!send_status(boot,command,PCAN_BOOT_STATUS_OK)) return false;
(void)send_status(boot, command, PCAN_BOOT_STATUS_OK);
if (boot->port.reboot != NULL) {
boot->port.reboot(boot->port_user);
}
@@ -360,16 +291,11 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
}
}
/* DATA не содержит session в Body: все 16 бит заняты номером блока. Поэтому
* кадры данных принимаются только в состоянии RECEIVING и для выбранного A/B.
* При неверном номере ничего не записываем и возвращаем ожидаемый next_block.
* Последний кадр дополняется клиентом до 8 октетов, но запись и CRC учитывают
* только оставшиеся октеты image_size. ACK выдаётся на границе окна или в конце. */
static bool process_data(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t block,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block,
const uint8_t *data, uint8_t dlc)
{
pcan_boot_byte_t expected_type;
pcan_boot_byte_t write_length;
uint8_t expected_type;
uint8_t write_length;
uint32_t remaining;
if (boot->state != PCAN_BOOT_STATE_RECEIVING) {
@@ -383,9 +309,8 @@ static bool process_data(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t
if ((block != boot->next_block) || (dlc != PCAN_BOOT_BLOCK_SIZE)) {
return send_status(boot, 0U, PCAN_BOOT_STATUS_SEQUENCE_ERROR);
}
boot->last_activity_ms=boot->now_ms;
remaining = boot->manifest.image_size - boot->bytes_received;
write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (pcan_boot_byte_t)remaining
write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (uint8_t)remaining
: PCAN_BOOT_BLOCK_SIZE;
if ((write_length == 0U)
|| !boot->port.write_slot(boot->port_user, boot->target_slot,
@@ -404,14 +329,10 @@ static bool process_data(pcan_boot_t *boot, pcan_boot_byte_t msg_type, uint16_t
return true;
}
/* Общая точка входа для одного кадра после аппаратной фильтрации.
* Проверки адреса повторяются программно, чтобы широкая маска CAN не разрешила
* обновление чужого устройства. Broadcast допускается только для DISCOVERY
* с Body=0/DLC=0; ответ откладывается до poll для разведения устройств по времени. */
bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
const pcan_boot_byte_t *data, pcan_boot_byte_t dlc)
const uint8_t *data, uint8_t dlc)
{
pcan_boot_byte_t msg_type;
uint8_t msg_type;
if ((boot == NULL) || (dlc > 8U) || ((dlc != 0U) && (data == NULL))) {
return false;
}
@@ -419,17 +340,6 @@ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
if ((msg_type < PCAN_BOOT_MSG_CONTROL) || (msg_type > PCAN_BOOT_MSG_DISCOVERY)) {
return false;
}
if(msg_type==PCAN_BOOT_MSG_DISCOVERY && pcan_boot_id_route(can_id)==0 &&
pcan_boot_id_msg_body(can_id)==0 && dlc==0 &&
((pcan_boot_id_device_type(can_id)==7 && pcan_boot_id_device_id(can_id)==15) ||
(pcan_boot_id_device_type(can_id)==boot->config.device_type &&
pcan_boot_id_device_id(can_id)==boot->config.device_id))) {
if(!boot->discovery_pending) {
boot->discovery_at=boot->now_ms+2U*(1U+16U*boot->config.device_type+boot->config.device_id);
boot->discovery_pending=true;
}
return true;
}
if ((pcan_boot_id_route(can_id) != PCAN_BOOT_ROUTE_FROM_PM)
|| (pcan_boot_id_device_type(can_id) != boot->config.device_type)
|| (pcan_boot_id_device_id(can_id) != boot->config.device_id)) {
@@ -443,18 +353,3 @@ bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id,
}
return false;
}
/* Обслуживание времени не требует входящего кадра. Регулярно вызывать из main
* с монотонным счётчиком миллисекунд: иначе не истечёт сессия и не выйдет discovery.
* При занятом TX discovery остаётся pending до успешного send_identity.
* Вычитание unsigned учитывает переход счётчика через 0xFFFFFFFF. */
void pcan_boot_poll(pcan_boot_t *boot,uint32_t now_ms)
{
if(!boot) return;
boot->now_ms=now_ms;
if(boot->config.session_timeout_ms && boot->state!=PCAN_BOOT_STATE_IDLE &&
(uint32_t)(now_ms-boot->last_activity_ms)>=boot->config.session_timeout_ms)
pcan_boot_abort(boot);
if(boot->discovery_pending && (uint32_t)(now_ms-boot->discovery_at)<0x80000000UL && send_identity(boot))
boot->discovery_pending=false;
}

View File

@@ -1,13 +0,0 @@
#ifndef TEST_DSP_H
#define TEST_DSP_H
#include <stdint.h>
#define EALLOW
#define EDIS
struct test_reg { uint32_t all; struct { unsigned DLC, RTR; } bit; };
struct MBOX { struct test_reg MSGID, MSGCTRL, MDL, MDH; };
static struct { struct MBOX MBOX3, MBOX4, MBOX6; } ECanaMboxes;
static struct { struct test_reg CANME,CANMD,CANRMP,CANRML,CANMIM,CANTA,CANAA,CANTRS,CANTRR; } ECanaRegs;
static struct { struct test_reg LAM3,LAM6; } ECanaLAMRegs;
static struct { unsigned long InterruptCount; } CpuTimer1;
static struct { unsigned WDCR; } SysCtrlRegs;
#endif

View File

@@ -1,7 +0,0 @@
#include <stdint.h>
#ifndef EEPROM_M95M02
#define EEPROM_M95M02 1
#endif
#define BALSAM167_EEPROM_FIRMWARE_BYTES 261884UL
void Seeprom_write(unsigned long word, unsigned int *data, unsigned long bytes);
void Seeprom_read(unsigned long word, unsigned int *data, unsigned long bytes);

View File

@@ -1,27 +0,0 @@
"""Host regression suite. CC may point to clang/gcc; this does not replace a TI build."""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
cc = os.environ.get('CC') or shutil.which('clang') or shutil.which('gcc')
if not cc:
raise SystemExit('Set CC to a C99 host compiler')
store = 'ports/tms320f28335/f28335_boot_store.c'
cases = [
('core', [], ['src/pcan_boot.c', 'tests/test_pcan_boot.c']),
('store_native', [], [store, 'tests/test_f28335_boot_store.c']),
('store_wide', ['-DPCAN_BOOT_WIDE_BYTES'], [store, 'tests/test_f28335_boot_store.c']),
('loader', ['-DPCAN_BOOT_WIDE_BYTES'], [store, 'src/pcan_boot.c', 'tests/test_f28335_can_boot.c']),
('application', ['-DPCAN_BOOT_WIDE_BYTES', '-DF28335_BOOT_LOADER=0'],
[store, 'src/pcan_boot.c', 'tests/test_f28335_can_boot.c']),
]
with tempfile.TemporaryDirectory(prefix='f28335-ab-') as tmp:
for name, defines, sources in cases:
exe = Path(tmp) / (name + '.exe')
subprocess.run([cc, '-std=c99', '-Wall', '-Wextra', '-Werror', '-Iinclude',
'-Itests/f28335_stubs', *defines, *sources, '-o', str(exe)], cwd=root, check=True)
sample = os.environ.get('F28335_SAMPLE_IMAGE') if name == 'store_wide' else None
subprocess.run([str(exe), *([sample] if sample else [])], check=True, timeout=30)

View File

@@ -1,118 +0,0 @@
#define _CRT_SECURE_NO_WARNINGS
#include "../ports/tms320f28335/f28335_boot_store.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
static unsigned char eeprom[0x40000],backup[0x40000];
static long budget=-1;
static int read_fail;
static unsigned written_words;
static bool read_mem(void *u,uint32_t addr,pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(addr+n<=sizeof(eeprom)); if(read_fail) return false;
for(i=0;i<n;++i) p[i]=eeprom[addr+i]; return true; }
static bool write_mem(void *u,uint32_t addr,const pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(addr+n<=0x3FE00);
for(i=0;i<n;++i) { if(budget==0) return false; if(budget>0) --budget; eeprom[addr+i]=(unsigned char)p[i]; }
return true; }
static f28335_boot_io_t io={read_mem,write_mem,0};
static pcan_boot_byte_t image[40];
static pcan_boot_manifest_t manifest;
static void make_image(void)
{
memset(image,0,sizeof(image)); image[0]=0xAA; image[1]=8;
image[20]=0; image[21]=0x80; /* entry=8000, high-word first */
image[22]=5; image[26]=0; image[27]=0x80;
image[28]=0x34; image[29]=0x12;
memset(&manifest,0,sizeof(manifest)); manifest.product_type=0x2833;
manifest.hardware_revision_min=manifest.hardware_revision_max=1;
manifest.image_size=40; manifest.image_crc32=f28335_boot_crc(0,image,40);
manifest.firmware_version=0x01010002;
}
static void image_to(f28335_boot_store_t *s,unsigned slot)
{ unsigned i; assert(f28335_boot_prepare(s,slot));
for(i=0;i<40;i+=8) assert(f28335_boot_write(s,slot,i,image+i,8)); }
static bool ram(void *u,uint32_t address,uint16_t word)
{ (void)u; assert(address==0x8000+written_words); if(!written_words) assert(word==0x1234);
++written_words; return true; }
static void reopen(f28335_boot_store_t *s) { assert(f28335_boot_open(s,&io)); }
int main(int argc,char **argv)
{
f28335_boot_store_t s; unsigned slot; uint32_t entry; long cut;
memset(eeprom,0xA5,sizeof(eeprom)); make_image(); reopen(&s);
assert(s.active==255 && !s.valid); assert(f28335_boot_select(&s,&slot) && slot==255);
image_to(&s,0); assert(f28335_boot_verify(&s,0,&manifest,&entry) && entry==0x8000);
assert(f28335_boot_commit(&s,0,&manifest));
assert(!f28335_boot_confirm(&s,0)); /* Cannot confirm an image not yet started. */
reopen(&s); assert(f28335_boot_select(&s,&slot) && slot==0 && s.attempted);
assert(f28335_boot_load(&s,slot,ram,0,&entry) && written_words==5);
assert(!f28335_boot_confirm(&s,1)); assert(f28335_boot_confirm(&s,0));
assert(f28335_boot_confirm(&s,0)); /* idempotent */
assert(!f28335_boot_prepare(&s,0));
assert(!f28335_boot_write(&s,0,0,image,8));
assert(!f28335_boot_write(&s,1,F28335_AB_SLOT_BYTES,image,8));
image_to(&s,1); memcpy(backup,eeprom,sizeof(eeprom));
/* Every torn write position in journal publication keeps confirmed A. */
for(cut=0;cut<=68;++cut) {
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut;
(void)f28335_boot_commit(&s,1,&manifest); budget=-1; reopen(&s);
assert(s.active==0); assert(f28335_boot_verify(&s,0,&s.image[0],0));
assert(s.pending==255 || s.pending==1);
}
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); assert(f28335_boot_commit(&s,1,&manifest));
memcpy(backup,eeprom,sizeof(eeprom));
/* Torn trial bookkeeping must never select an unrecorded trial. */
for(cut=0;cut<=68;++cut) {
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut;
if(f28335_boot_select(&s,&slot)) { assert(slot==1); assert(s.attempted); }
budget=-1; reopen(&s); assert(s.active==0);
}
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s);
assert(f28335_boot_select(&s,&slot) && slot==1);
reopen(&s); assert(f28335_boot_select(&s,&slot) && slot==0 && s.pending==255);
image_to(&s,1); assert(f28335_boot_commit(&s,1,&manifest));
assert(f28335_boot_select(&s,&slot) && slot==1);
memcpy(backup,eeprom,sizeof(eeprom));
for(cut=0;cut<=68;++cut) {
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); budget=cut;
(void)f28335_boot_confirm(&s,1); budget=-1; reopen(&s);
assert(s.active==0 || s.active==1);
assert(f28335_boot_select(&s,&slot) && (slot==0 || slot==1));
}
memcpy(eeprom,backup,sizeof(eeprom)); reopen(&s); assert(f28335_boot_confirm(&s,1));
/* CRC correct but malicious SCI8 destination and entry are rejected. */
image_to(&s,0); image[26]=0; image[27]=0xE0;
manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0));
image[27]=0x80; image[21]=0x90; manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0));
make_image(); image[22]=1; image[30]=1; image[35]=0x80;
manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0)); /* overlapping sections */
make_image(); image_to(&s,0); manifest.image_size=38;
manifest.image_crc32=f28335_boot_crc(0,image,38);
assert(!f28335_boot_verify(&s,0,&manifest,0)); /* missing terminator */
make_image(); image[22]=4; manifest.image_crc32=f28335_boot_crc(0,image,40);
image_to(&s,0); assert(!f28335_boot_verify(&s,0,&manifest,0)); /* trailing octets */
make_image(); image_to(&s,0); eeprom[F28335_AB_SLOT_A+29]^=1;
assert(!f28335_boot_verify(&s,0,&manifest,0));
/* Failed EEPROM reads fail closed, including subsequent writes. */
read_fail=1; assert(!f28335_boot_open(&s,&io));
assert(!f28335_boot_prepare(&s,0)); assert(!f28335_boot_write(&s,0,0,image,8)); read_fail=0;
for(entry=0x3FE00;entry<0x40000;++entry) assert(eeprom[entry]==0xA5);
if(argc>1) {
FILE *file=fopen(argv[1],"rb"); uint32_t offset,crc=0;
pcan_boot_byte_t bytes[32]; unsigned n;
assert(file); make_image();
manifest.image_size=(uint32_t)fread(eeprom+F28335_AB_SLOT_A,1,F28335_AB_SLOT_BYTES,file);
assert(!ferror(file) && fgetc(file)==EOF); fclose(file); reopen(&s);
for(offset=0;offset<manifest.image_size;offset+=n) {
n=manifest.image_size-offset<32?(unsigned)(manifest.image_size-offset):32;
assert(read_mem(0,F28335_AB_SLOT_A+offset,bytes,n)); crc=f28335_boot_crc(crc,bytes,n);
}
manifest.image_crc32=crc;
assert(f28335_boot_verify(&s,0,&manifest,&entry));
puts("Production SCI8 binary: CRC, destinations and entry accepted");
}
puts("F28335 A/B: journal power cuts, trial, rollback, confirm, bounds, SCI8: OK");
return 0;
}

View File

@@ -1,94 +0,0 @@
/* Production CAN adapter, fake hardware, wide C28x wire octets. */
#define EEPROM_M95M02 1
#ifndef F28335_BOOT_LOADER
#define F28335_BOOT_LOADER 1
#endif
#define F28335_CAN_BOOT_FIRMWARE_VERSION 0x12345678UL
#include <assert.h>
#include <stdio.h>
#include <string.h>
void ServiceDog(void);
#include "../ports/tms320f28335/f28335_can_boot.c"
static unsigned char memory[0x40000];
static unsigned int running=255,resets,reset_arg,dog_feeds;
static int fail_write;
void ServiceDog(void) { ++dog_feeds; }
bool F28335Boot_Read(void *u,uint32_t a,pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(a+n<=sizeof(memory)); for(i=0;i<n;++i)p[i]=memory[a+i]; return true; }
bool F28335Boot_Write(void *u,uint32_t a,const pcan_boot_byte_t *p,unsigned int n)
{ unsigned i; (void)u; assert(a+n<=0x3FE00); if(fail_write)return false;
for(i=0;i<n;++i)memory[a+i]=(unsigned char)p[i]; return true; }
uint32_t F28335Boot_Now(void) { return CpuTimer1.InterruptCount; }
unsigned int F28335Boot_RunningSlot(void) { return running; }
void F28335Boot_Reset(unsigned int session) { ++resets;reset_arg=session;reset_requested=0; }
static void command(unsigned cmd,const pcan_boot_byte_t *d,unsigned n)
{ tx_busy=0; assert(pcan_boot_process(&boot,pcan_boot_make_id(1,0,3,7,9,0x100|cmd),d,n)); }
static void put32(pcan_boot_byte_t *p,uint32_t v)
{ unsigned i;for(i=0;i<4;++i)p[i]=(v>>(i*8))&255; }
int main(void)
{
pcan_boot_byte_t image[40]={0xAA,8},meta[8],compat[8]={0x33,0x28,1,1,2,0,1,1};
unsigned i,slot; pcan_boot_manifest_t m;
memset(memory,0xA5,sizeof(memory)); image[21]=0x80; image[22]=5; image[27]=0x80;
F28335CanBoot_Init(); assert(boot.config.max_image_size==130560 && boot.config.slot_count==2);
if(!F28335_BOOT_LOADER) {
/* SPI-ROM application without Flash-loader handoff cannot erase anything. */
command(2,0,0); assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE && !reset_requested);
memset(&m,0,sizeof(m));m.image_size=40;m.image_crc32=f28335_boot_crc(0,image,40);
m.product_type=0x2833;m.hardware_revision_min=m.hardware_revision_max=1;
assert(f28335_boot_prepare(&store,0));
for(i=0;i<40;i+=8)assert(f28335_boot_write(&store,0,i,image+i,8));
assert(f28335_boot_commit(&store,0,&m)); assert(f28335_boot_select(&store,&slot));
running=0; F28335CanBoot_Init();
assert(!F28335CanBoot_WatchdogAllowed());
command(8,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE);
F28335CanBoot_Ready();
for(i=0;i<100;++i) F28335CanBoot_Service();
assert(!confirmed && store.pending==0);
CpuTimer1.InterruptCount=5000; F28335CanBoot_Service();
assert(confirmed && store.active==0 && dog_feeds);
command(2,0,0); assert(reset_requested && reset_session==1 && boot.state==PCAN_BOOT_STATE_IDLE);
F28335CanBoot_Service();assert(resets==1 && reset_arg==1);
command(1,0,0);assert(ECanaMboxes.MBOX4.MDH.all==0x12345678UL);
} else {
command(2,0,0); put32(meta,40);put32(meta+4,f28335_boot_crc(0,image,40));
command(3,meta,8);command(4,compat,8);assert(boot.target_slot==0);
command(5,0,0);
for(i=0;i<5;++i) { tx_busy=0; assert(pcan_boot_process(&boot,
pcan_boot_make_id(1,0,3,7,10,i),image+i*8,8)); }
command(6,0,0);assert(boot.state==PCAN_BOOT_STATE_VERIFIED);
command(7,0,0);assert(store.pending==0 && boot.state==PCAN_BOOT_STATE_COMMITTED);
command(7,0,0);assert(boot.last_status==0);
command(8,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_INVALID_STATE);
command(9,0,0);assert(reset_requested);tx_busy=0;
F28335CanBoot_Service();assert(resets==1 && reset_arg==0);
command(10,0,0);command(2,0,0);command(3,meta,8);command(4,compat,8);
command(5,0,0);assert(boot.last_status==PCAN_BOOT_STATUS_FLASH_ERROR); /* Pending is protected. */
}
command(12,0,0); assert((ECanaMboxes.MBOX4.MSGID.all&65535)==12);
command(13,0,0); assert(ECanaMboxes.MBOX4.MDH.all==130560);
meta[0]=0;command(14,meta,1); assert(ECanaMboxes.MBOX4.MDL.all==0x33383246UL); /* F283 */
meta[0]=3;command(15,meta,1);assert(ECanaMboxes.MBOX4.MDL.all==0x3FEFCUL);
assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,3,6,9,0x102),0,0));
tx_busy=0;
assert(pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,13,0),0,0));
assert(boot.discovery_pending); pcan_boot_poll(&boot,boot.discovery_at);
assert(!boot.discovery_pending && (ECanaMboxes.MBOX4.MSGID.all&65535)==1);
assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,9,0x102),0,0));
head=tail=0; ECanaRegs.CANRMP.all=8;
ECanaMboxes.MBOX3.MSGID.all=0x80000000UL|pcan_boot_make_id(1,0,3,7,9,12);
ECanaMboxes.MBOX3.MSGCTRL.bit.DLC=0;
F28335CanBoot_RxISR(); assert(head==1 && tail==0);
tx_busy=0; F28335CanBoot_Service(); assert(tail==1);
ECanaMboxes.MBOX3.MSGCTRL.bit.RTR=1; F28335CanBoot_RxISR(); assert(head==1);
ECanaMboxes.MBOX3.MSGCTRL.bit.RTR=0;
overflow=1;tx_busy=0;F28335CanBoot_Service();assert(boot.state==PCAN_BOOT_STATE_IDLE);
/* An ACK arriving after cancellation must never execute a queued reset. */
tx_busy=1;tx_canceling=0;reset_requested=1;sent_at=0;
CpuTimer1.InterruptCount=300;ECanaRegs.CANTA.all=0;ECanaRegs.CANAA.all=0;
F28335CanBoot_Service();assert(!reset_requested && tx_canceling);
ECanaRegs.CANTA.all=16; F28335CanBoot_Service();assert(!tx_busy && resets==1);
for(i=0x3FE00;i<0x40000;++i)assert(memory[i]==0xA5);
puts(F28335_BOOT_LOADER?"F28335 loader CAN services: OK":"F28335 application handoff/confirmation: OK");
return 0;
}

View File

@@ -264,7 +264,7 @@ static void test_rejects_wrong_session_and_compatibility(void)
data[3] = 3U;
put_u32(&data[4], 1U);
assert(command(&boot, 7U, PCAN_BOOT_CMD_BEGIN_COMPAT, data, 8U));
assert(fake.last_data[0] == PCAN_BOOT_STATUS_WRONG_DEVICE);
assert(fake.last_data[0] == PCAN_BOOT_STATUS_WRONG_HARDWARE);
assert(boot.state == PCAN_BOOT_STATE_FAILED);
}
@@ -306,41 +306,8 @@ static void test_single_slot_update(void)
assert(memcmp(fake.flash[0], image, sizeof(image)) == 0);
}
static void test_services_timeout_and_retries(void)
{
pcan_boot_t boot; fake_t fake;
uint8_t image[8]={0},compat[8]={0}; unsigned count;
setup(&boot,&fake); boot.config.session_timeout_ms=30000;
pcan_boot_poll(&boot,0xFFFFFF00UL);
assert(command(&boot,7,PCAN_BOOT_CMD_ENTER_BOOT,NULL,0));
assert(command(&boot,8,PCAN_BOOT_CMD_ENTER_BOOT,NULL,0));
assert(boot.session_id==7 && fake.last_data[0]==PCAN_BOOT_STATUS_BUSY);
assert(command(&boot,7,PCAN_BOOT_CMD_ERASE,image,1));
assert(fake.last_data[0]==PCAN_BOOT_STATUS_INVALID_SIZE);
pcan_boot_poll(&boot,29743); assert(boot.state!=PCAN_BOOT_STATE_IDLE);
pcan_boot_poll(&boot,29744); assert(boot.state==PCAN_BOOT_STATE_IDLE);
assert(command(&boot,0,PCAN_BOOT_CMD_PING,NULL,0));
assert(pcan_boot_id_msg_body(fake.last_id)==12);
count=fake.sends;
assert(pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,13,0),NULL,0));
assert(fake.sends==count); pcan_boot_poll(&boot,boot.discovery_at);
assert(fake.sends==count+1 && pcan_boot_id_msg_body(fake.last_id)==1);
assert(!pcan_boot_process(&boot,pcan_boot_make_id(1,0,7,15,9,0x702),NULL,0));
begin_update(&boot,image,8);
put_u16(compat,boot.manifest.product_type);
compat[2]=boot.manifest.hardware_revision_min; compat[3]=boot.manifest.hardware_revision_max;
put_u32(compat+4,boot.manifest.firmware_version);
assert(command(&boot,7,4,compat,8)); assert(fake.last_data[0]==0);
assert(command(&boot,7,5,NULL,0)); count=fake.erases;
assert(command(&boot,7,5,NULL,0)); assert(fake.erases==count && fake.last_data[0]==0);
assert(pcan_boot_process(&boot,request_id(PCAN_BOOT_MSG_DATA_B,0),image,8));
assert(command(&boot,7,6,NULL,0)); assert(command(&boot,7,6,NULL,0)); assert(fake.last_data[0]==0);
assert(command(&boot,7,7,NULL,0)); assert(command(&boot,7,7,NULL,0)); assert(fake.last_data[0]==0);
}
int main(void)
{
test_services_timeout_and_retries();
test_id_layout();
test_crc32_reference();
test_complete_update();

View File

@@ -6,16 +6,6 @@
`include/balsam167_eeprom_layout.h`; PEEK, CRC и порядок байтов реализованы
в `src/periph28335.c`. Проверка полного диапазона — `test_periph28335_eeprom`.
Декодер `balsam_can` знает служебные слова heartbeat периферии Бальзам-167:
`0x0100` — счётчик секунд по модулю 65536, `0x0101` — аппаратный Mode,
`0x0102` — код типа платы Desk. Назначения соответствуют `CAN_HEARTBEAT_ADDR`
и ветке `CAN_send` в `Balsam_167_periph/Source/Internal/ecan.c`.
Python-адаптер `set_devices.balsam_can` выделяет ответ с адресом `0x0100`
и маской `111` как `BALZAM Heartbeat`, сохраняя исходные регистры и маршрут.
Пример: `0x00BA0017`, `00 08 00 04 E1 00 00 5A` — heartbeat задатчика,
90 секунд, Mode=8, Desk=4. Счётчик отображается без знака, в том числе
при значениях `0x8000–0xFFFF`; обычные технологические int16 сохраняют знак.
Универсальные сервисы эмуляторов: [адресное пространство, API и порт новой платы](docs/EMULATOR_SERVICES.md).
`set_regmap` объединяет УМП, 2812, IGBT и DAC за общим API для RTU, CAN и других оболочек.

View File

@@ -31,8 +31,7 @@ enum set_plot_operation {
PCAN_ABI_API uint32_t set_plot_abi_version(void);
/** Evaluates one operation. Returns output count, or 0 for invalid arguments.
* TRANSFORM rejects malformed viewports; invalid gesture values return the
* unchanged viewport. Zoom is restricted to 1..10000000 on X and 1..128 on Y,
* with focus anchoring.
* unchanged viewport. Zoom is restricted to 1..128, with focus anchoring.
* Screen fractions increase downwards; set inverted=1 for the Y value axis.
*/
PCAN_ABI_API size_t set_plot_eval(uint32_t operation, const double *input,

View File

@@ -13,7 +13,6 @@ extern "C" {
#endif
enum { SET_SIGNAL_POLYNOMIAL, SET_SIGNAL_LINEAR, SET_SIGNAL_PCHIP, SET_SIGNAL_SPLINE };
/* Caller-owned workspace: at least 14*count+128 doubles. No heap or globals.
* Bounds: 2..100000 input points, 2..1000000 output points.
* Inputs may be unsorted; duplicate times are averaged. No extrapolation.
* meta[0..2] = original count, unique count, RMSE at original measurements.
* endpoint=1 includes the last time; 0 samples a cyclic period [first,last).

View File

@@ -152,17 +152,14 @@ object BalsamCanProtocol {
fun deviceName(device: Int): String = if (NativeSetProtocol.available) {
NativeSetProtocol.nativeBalsamDeviceName(device)
} else listOf(
"Трансформатор 1", "Трансформатор 2", "Силовой 1", "Силовой 2", // Имена узлов 1..4 повторяют таблицу адресов 167 и общее C-ядро.
"УМП 1", "УМП 2", "Двигатель", "Задатчик", "ВЭП", "ПМУ", "Узел 11", // Узлы 5..10 соответствуют таблице, включая ПМУ на паре 0xBA0009/0xBA0019; узел 11 остаётся служебным.
"Трансформатор 1", "Трансформатор 2", "Силовой блок 1", "Силовой блок 2",
"УМП 1", "УМП 2", "Двигатель", "ВЭП", "Задатчик", "Узел 10", "Узел 11",
"Узел 12", "Терминал",
).getOrElse(device - 1) { "Неизвестный узел" }
fun registerName(device: Int, address: Int): String =
if (NativeSetProtocol.available) NativeSetProtocol.nativeBalsamRegisterName(device, address)
else when {
address == 0x0100 -> "Heartbeat: счётчик, с" // CAN_HEARTBEAT_ADDR из ecan.c содержит беззнаковый секундный счётчик по модулю 65536.
address == 0x0101 -> "Heartbeat: Mode" // Второе слово служебного пакета передаёт аппаратный номер Mode платы.
address == 0x0102 -> "Heartbeat: тип платы (Desk)" // Третье слово передаёт Desk, чтобы Android без JNI использовал названия общего C-ядра.
device in 1..2 && address in 0x18..0x2B -> "Показания T° ${address - 0x17}"
device in 3..4 && address in 0x18..0x27 -> "Показания T° ${address - 0x17}"
device == 7 && address in 0x18..0x1F -> "Показания T° ${address - 0x17}"

View File

@@ -0,0 +1,206 @@
package ru.setcorp.setprotocol.deif
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.json.JSONArray
import org.json.JSONObject
import ru.setcorp.setflash.core.CanBridgeProtocol
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
data class DeifBinding(val index: Int, val subindex: Int = 2, val scale: Double = 1.0)
data class DeifDisplayConfig(
val node: Int = 1, val canId: Int = 0x201,
val bindings: Map<Parameter, DeifBinding> = Parameter.entries.associateWith { DeifBinding(it.index, 2, it.scale) },
)
data class DeifReading(val value: Double, val at: Long)
data class DeifState(
val config: DeifProtocol.Config = DeifProtocol.Config(),
val raw: Map<Parameter, Int> = Parameter.entries.associateWith { 0 },
val controlScales: DeifControls.Scales = DeifControls.Scales(),
val unlocked: Boolean = false, val running: Boolean = false,
val fullScreen: Boolean = false,
val displays: List<DeifDisplayConfig> = List(2) { DeifDisplayConfig() },
val readings: List<Map<Parameter, DeifReading>> = List(2) { emptyMap() },
val sent: Long = 0, val received: Long = 0, val errors: Long = 0,
val echoes: Long = 0, val echoTimes: Map<Int, Long> = emptyMap(),
val intervalMs: Long = 0, val maxGapMs: Long = 0, val maxCycleMs: Long = 0,
val status: String = "Передача выключена", val lastReceived: String = "Нет принятых данных",
)
/** Owns both timers and display freshness independently of the Android screen lifecycle. */
class DeifController(
private val scope: CoroutineScope,
private val send: suspend (CanBridgeProtocol.Frame) -> Unit,
private val save: (String) -> Unit = {},
private val now: () -> Long = { System.nanoTime() / 1_000_000 },
) {
private val mutable = MutableStateFlow(DeifState())
val state = mutable.asStateFlow()
private var job: Job? = null
private val wire = Mutex()
private var previousCycle: Long? = null
fun setFullScreen(enabled: Boolean) { mutable.value = state.value.copy(fullScreen = enabled) }
fun stop(message: String = "Передача остановлена") {
job?.cancel(); job = null
mutable.value = mutable.value.copy(running = false, status = message)
}
fun unlock(value: Boolean) {
stop()
mutable.value = mutable.value.copy(unlocked = value)
}
fun configure(config: DeifProtocol.Config) {
check(state.value.unlocked) { "Разблокируйте настройки" }
config.validate(); stop()
mutable.value = state.value.copy(config = config)
persist()
}
fun select(parameter: Parameter, enabled: Boolean) {
stop()
val c = state.value.config
mutable.value = state.value.copy(config = c.copy(enabled = if (enabled) c.enabled + parameter else c.enabled - parameter))
persist()
}
fun setRaw(parameter: Parameter, raw: Int) {
require(raw in -32768..32767)
mutable.value = state.value.copy(raw = state.value.raw + (parameter to raw))
persist()
}
fun requestInstrument(field: DeifControls.Field, value: Double) {
mutable.value = state.value.copy(raw = state.value.raw + DeifControls.raw(field, value, state.value.controlScales))
persist()
}
fun configureControls(scales: DeifControls.Scales) {
check(state.value.unlocked); scales.validate(); stop()
mutable.value = state.value.copy(controlScales = scales)
persist()
}
fun configureDisplay(side: Int, config: DeifDisplayConfig) {
check(state.value.unlocked)
require(config.node in 1..127 && config.canId in 0x201..0x57F)
require(config.bindings.values.all { it.index in 0..65535 && it.subindex in 0..255 && it.scale.isFinite() })
stop()
mutable.value = state.value.copy(
displays = state.value.displays.mapIndexed { i, d -> if (i == side) config else d },
readings = state.value.readings.mapIndexed { i, d -> if (i == side) emptyMap() else d },
)
persist()
}
fun start(repeat: Boolean) {
if (state.value.unlocked) { stop("Заблокируйте настройки перед передачей"); return }
stop()
if (!repeat && state.value.config.enabled.isEmpty()) return
if (state.value.config.enabled.isEmpty() && !state.value.config.heartbeat) return
previousCycle = null
mutable.value = state.value.copy(running = true, sent = 0, errors = 0, echoes = 0, echoTimes = emptyMap(), maxGapMs = 0, maxCycleMs = 0)
job = scope.launch {
try {
coroutineScope {
if (repeat && state.value.config.heartbeat) launch {
while (isActive) {
transmit(DeifProtocol.heartbeat(state.value.config)); delay(500)
}
}
do {
val started = now()
val snapshot = state.value
val frames = DeifProtocol.cycle(snapshot.config, snapshot.raw)
frames.forEach { transmit(it) }
val elapsed = now() - started
val gap = previousCycle?.let { started - it } ?: 0
previousCycle = started
mutable.value = state.value.copy(intervalMs = gap,
maxGapMs = maxOf(state.value.maxGapMs, gap), maxCycleMs = maxOf(state.value.maxCycleMs, elapsed))
if (!repeat) break
delay(maxOf(1, snapshot.config.periodMs - elapsed))
} while (isActive)
}
mutable.value = state.value.copy(running = false)
} catch (cancelled: CancellationException) { throw cancelled }
catch (error: Exception) { stop("Ошибка передачи: ${error.message}") }
}
}
private suspend fun transmit(frame: CanBridgeProtocol.Frame) = wire.withLock {
currentCoroutineContext().ensureActive()
check(!state.value.unlocked)
DeifProtocol.validateTransmit(frame)
send(frame)
accept(0, frame)
mutable.value = state.value.copy(sent = state.value.sent + 1, status = "Передано в транспорт; приём прибором не подтверждён")
}
fun receive(frame: CanBridgeProtocol.Frame) {
if (frame.isError) {
mutable.value = state.value.copy(errors = state.value.errors + 1, lastReceived = "Ошибка CAN: %03X".format(frame.canId))
return
}
if (frame.toCan) {
val d = DeifProtocol.decode(frame)
if (d.index != null && d.subindex != null) mutable.value = state.value.copy(
echoes = state.value.echoes + 1, echoTimes = state.value.echoTimes + ((d.index * 256 + d.subindex) to now()))
return // Transport TX notifications must never masquerade as SLAVE RX.
}
val decoded = DeifProtocol.decode(frame)
mutable.value = state.value.copy(received = state.value.received + 1, lastReceived = decoded.summary)
accept(1, frame)
}
private fun accept(side: Int, frame: CanBridgeProtocol.Frame) {
val d = DeifProtocol.decode(frame)
if (d.raw == null) return
val config = state.value.displays[side]
if (d.node != config.node || (d.mode == DeifProtocol.Mode.SAM && frame.canId != config.canId.toLong())) return
val readings = state.value.readings[side].toMutableMap()
config.bindings.forEach { (key, binding) ->
if (binding.index == d.index && binding.subindex == d.subindex) {
if (d.valid) readings[key] = DeifReading(d.raw * binding.scale, now()) else readings.remove(key)
}
}
mutable.value = state.value.copy(readings = state.value.readings.mapIndexed { i, r -> if (i == side) readings else r })
}
fun value(side: Int, parameter: Parameter): Double? = state.value.readings[side][parameter]
?.takeIf { now() - it.at <= 5000 }?.value
fun echoAge(index: Int, subindex: Int): Long? = state.value.echoTimes[index * 256 + subindex]?.let { now() - it }
fun restore(text: String) {
runCatching {
val root = JSONObject(text)
val c = DeifProtocol.Config(DeifProtocol.Mode.valueOf(root.getString("mode")), root.getInt("node"),
root.getInt("canId"), root.getLong("period"), root.getBoolean("heartbeat"), root.getInt("masterNode"),
Parameter.entries.filter { root.getJSONArray("enabled").toString().contains("\"${it.name}\"") }.toSet())
c.validate()
val raw = Parameter.entries.associateWith { root.getJSONObject("raw").getInt(it.name).also { v -> require(v in -32768..32767) } }
val displays = (0..1).map { side ->
val o = root.getJSONArray("displays").getJSONObject(side)
DeifDisplayConfig(o.getInt("node"), o.getInt("canId"), Parameter.entries.associateWith {
val b = o.getJSONObject(it.name)
DeifBinding(b.getInt("index"), b.getInt("sub"), b.getDouble("scale"))
})
}
require(displays.all { it.node in 1..127 && it.canId in 0x201..0x57F && it.bindings.values.all { b -> b.index in 0..65535 && b.subindex in 0..255 && b.scale.isFinite() } })
val controls = root.optJSONObject("controls")
val scales = controls?.let { DeifControls.Scales(it.getDouble("power100"), it.getDouble("rpm100"),
it.getDouble("angleRawDegree"), it.getDouble("powerPercentRaw")) } ?: DeifControls.Scales()
scales.validate()
mutable.value = DeifState(config = c, raw = raw, displays = displays, controlScales = scales)
}.onFailure { mutable.value = DeifState(status = "Сохранённые настройки DEIF повреждены; загружены значения по умолчанию") }
}
private fun persist() {
val s = state.value; val c = s.config
val root = JSONObject().put("mode", c.mode.name).put("node", c.node).put("canId", c.canId)
.put("controls", JSONObject().put("power100", s.controlScales.power100).put("rpm100", s.controlScales.rpm100)
.put("angleRawDegree", s.controlScales.angleRawDegree).put("powerPercentRaw", s.controlScales.powerPercentRaw))
.put("period", c.periodMs).put("heartbeat", c.heartbeat).put("masterNode", c.masterNode)
.put("enabled", JSONArray(c.enabled.map { it.name }))
.put("raw", JSONObject().also { o -> s.raw.forEach { (p, v) -> o.put(p.name, v) } })
.put("displays", JSONArray().also { a -> s.displays.forEach { d ->
a.put(JSONObject().put("node", d.node).put("canId", d.canId).also { o -> d.bindings.forEach { (p, b) ->
o.put(p.name, JSONObject().put("index", b.index).put("sub", b.subindex).put("scale", b.scale))
} })
} })
save(root.toString())
}
}

View File

@@ -0,0 +1,45 @@
package ru.setcorp.setprotocol.deif
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
import kotlin.math.*
/** SETGUI Propulsor uses the same 600 × 510 coordinate system and calibration. */
object DeifControls {
enum class Field { POWER, RPM, ANGLE }
data class Scales(val power100: Double = 1000.0, val rpm100: Double = 1000.0,
val angleRawDegree: Double = 10.0, val powerPercentRaw: Double = 10.0) {
fun validate() {
require(listOf(power100, rpm100, angleRawDegree, powerPercentRaw).all { it.isFinite() && it > 0 && it <= 100000 }) {
"Масштабы должны быть больше нуля и не больше 100000"
}
}
}
fun hit(x: Float, y: Float): Field? = when {
x in 46f..116f && y in 110f..410f -> Field.POWER
x in 174f..244f && y in 110f..410f -> Field.RPM
hypot(x - 411f, y - 264f) <= 155f -> Field.ANGLE
else -> null
}
fun requested(field: Field, x: Float, y: Float): Double? = when (field) {
Field.ANGLE -> if (hypot(x - 411f, y - 264f) < 12f) null else atan2((x - 411).toDouble(), (264 - y).toDouble()) * 180 / PI
else -> { val low = if (field == Field.RPM) -20.0 else 0.0
(120 - (y - 126) / 270 * (120 - low)).coerceIn(low, 120.0) }
}
fun raw(field: Field, value: Double, scales: Scales): Map<Parameter, Int> {
scales.validate(); require(value.isFinite())
fun limited(v: Double) = round(v).coerceIn(-32767.0, 32765.0).toInt()
if (field == Field.ANGLE) return mapOf(Parameter.ANGLE to limited(value * scales.angleRawDegree))
val power = field == Field.POWER
val factor = if (power) 1.0 else 10.0
val full = if (power) scales.power100 else scales.rpm100
val raw = limited(value / 100 * full * factor)
val percent = raw / factor / full * 100
return mapOf((if (power) Parameter.POWER else Parameter.RPM) to raw,
(if (power) Parameter.POWER_PERCENT else Parameter.RPM_PERCENT) to limited(percent * if (power) scales.powerPercentRaw else 10.0))
}
fun preview(raw: Map<Parameter, Int>, scales: Scales): Map<Field, Double> = mapOf(
Field.POWER to (raw[Parameter.POWER] ?: 0) / scales.power100 * 100,
Field.RPM to (raw[Parameter.RPM] ?: 0) / 10.0 / scales.rpm100 * 100,
Field.ANGLE to (raw[Parameter.ANGLE] ?: 0) / scales.angleRawDegree,
)
}

View File

@@ -0,0 +1,53 @@
package ru.setcorp.setprotocol.deif
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
/** Host hides system chrome; controller survives rotation and maintains the active cycle. */
@Composable
fun DeifFullScreen(controller: DeifController, connected: Boolean) {
val state by controller.state.collectAsState()
var tick by remember { mutableLongStateOf(0) }
LaunchedEffect(controller) { while (true) { delay(250); tick++ } }
BackHandler { controller.setFullScreen(false) }
Column(Modifier.fillMaxSize().background(Color(0xFF090C0E)).displayCutoutPadding()) {
BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) {
val preview = DeifControls.preview(state.raw, state.controlScales)
@Composable fun instrument(side: Int, modifier: Modifier) {
val values = remember(state.readings, tick) { Parameter.entries.associateWith { controller.value(side, it) } }
Box(modifier, contentAlignment = Alignment.Center) {
DeifInstrument("${if (side == 0) "MASTER" else "SLAVE"} · Node ${state.displays[side].node}",
values, preview, controller::requestInstrument, Modifier.fillMaxSize())
}
}
if (maxWidth > maxHeight) Row(Modifier.fillMaxSize()) {
instrument(0, Modifier.weight(1f).fillMaxHeight())
instrument(1, Modifier.weight(1f).fillMaxHeight())
} else Column(Modifier.fillMaxSize()) {
instrument(0, Modifier.weight(1f).fillMaxWidth())
instrument(1, Modifier.weight(1f).fillMaxWidth())
}
}
Surface(color = Color(0xFF111820), contentColor = Color.White) {
Row(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button({ controller.setFullScreen(false) }) { Text("Выйти из полного экрана") }
Button({ controller.start(false) }, enabled = connected && !state.unlocked && !state.running) { Text("Цикл") }
Button({ controller.start(true) }, enabled = connected && !state.unlocked && !state.running) { Text("Старт") }
Button({ controller.stop() }, enabled = state.running) { Text("Стоп") }
Text(if (!connected) "Нет подключения" else if (state.unlocked) "Настройки разблокированы" else if (state.running) "MASTER работает" else "Передача выключена")
}
}
}
}

View File

@@ -0,0 +1,134 @@
package ru.setcorp.setprotocol.deif
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Typeface
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.input.pointer.pointerInput
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
import kotlin.math.*
/** Vector port of SETGUI Propulsor.paintEvent, including its original silhouette. */
@Composable
fun DeifInstrument(title: String, values: Map<Parameter, Double?>, preview: Map<DeifControls.Field, Double>,
onRequest: (DeifControls.Field, Double) -> Unit, modifier: Modifier = Modifier) {
val request by rememberUpdatedState(onRequest)
Canvas(modifier.fillMaxWidth().aspectRatio(600f / 510f).pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
val scale = min(size.width / 600f, size.height / 510f)
val ox = (size.width - 600 * scale) / 2
val oy = (size.height - 510 * scale) / 2
fun logical(p: androidx.compose.ui.geometry.Offset) = (p.x - ox) / scale to (p.y - oy) / scale
val (x, y) = logical(down.position)
val field = DeifControls.hit(x, y)
if (field != null) {
down.consume()
DeifControls.requested(field, x, y)?.let { request(field, it) }
do {
val change = awaitPointerEvent().changes.firstOrNull { it.id == down.id } ?: break
val (px, py) = logical(change.position)
DeifControls.requested(field, px, py)?.let { request(field, it) }
change.consume()
} while (change.pressed)
}
}
}) {
val c = drawContext.canvas.nativeCanvas
val p = Paint(Paint.ANTI_ALIAS_FLAG)
fun color(hex: String) = android.graphics.Color.parseColor(hex)
fun text(x: Float, y: Float, w: Float, h: Float, value: String, size: Float = 18f, ink: String = "#e9edf4", bold: Boolean = false) {
p.style = Paint.Style.FILL; p.color = color(ink); p.textSize = size * 1.25f
p.typeface = Typeface.create("sans-serif", if (bold) Typeface.BOLD else Typeface.NORMAL)
p.textAlign = Paint.Align.CENTER
c.drawText(value, x + w / 2, y + h / 2 - (p.ascent() + p.descent()) / 2, p)
}
fun line(x1: Float, y1: Float, x2: Float, y2: Float, ink: String, width: Float = 1f) {
p.color = color(ink); p.strokeWidth = width; p.style = Paint.Style.STROKE; p.strokeCap = Paint.Cap.BUTT
c.drawLine(x1, y1, x2, y2, p)
}
fun number(key: Parameter, suffix: String = "") = values[key]?.let { "%s%s".format(java.math.BigDecimal.valueOf(it).stripTrailingZeros().toPlainString(), suffix) } ?: "—"
c.save()
c.clipRect(0f, 0f, size.width, size.height)
c.drawColor(color("#090c0e"))
val scale = min(size.width / 600, size.height / 510)
c.translate((size.width - 600 * scale) / 2, (size.height - 510 * scale) / 2); c.scale(scale, scale)
text(15f, 12f, 570f, 40f, title, 22f)
text(205f, 54f, 95f, 25f, "Задание:", 11f, "#8798ad")
text(300f, 54f, 95f, 25f, preview[DeifControls.Field.ANGLE]?.let { "%.1f°".format(it) } ?: "—°", 11f)
for ((x, field) in listOf(64f to DeifControls.Field.POWER, 192f to DeifControls.Field.RPM)) {
val power = field == DeifControls.Field.POWER
val low = if (power) 0 else -20
fun yy(v: Double) = (126 + (120 - v) / (120 - low) * 270).toFloat()
for ((a, b, ink) in listOf(Triple(low, 0, "#ae616b"), Triple(0, 100, "#657caf"), Triple(100, 120, "#b6c799"))) {
if (b > a) { p.style = Paint.Style.FILL; p.color = color(ink); c.drawRect(x, yy(b.toDouble()), x + 34, yy(a.toDouble()), p) }
}
for (v in low..120 step 20) {
val y = yy(v.toDouble()); line(x, y, x + 34, y, "#202b3a")
if (v in listOf(low, 0, 40, 80, 120)) text(x - 40, y - 12, 36f, 24f, v.toString(), 12f)
}
val percentKey = if (power) Parameter.POWER_PERCENT else Parameter.RPM_PERCENT
val percent = values[percentKey]
if (percent != null) { val y = yy(percent.coerceIn(low.toDouble(), 120.0)); line(x - 3, y, x + 38, y, "#ffffff", 4f) }
else { p.style = Paint.Style.FILL; p.color = android.graphics.Color.argb(130, 16, 21, 29); c.drawRect(x, 126f, x + 34, 396f, p) }
preview[field]?.let { val y = yy(it.coerceIn(low.toDouble(), 120.0)); line(x - 8, y, x + 42, y, "#eeef69", 3f) }
text(x - 35, 89f, 110f, 30f, number(percentKey, "%"), 17f)
text(x - 45, 410f, 128f, 28f, if (power) "POWER" else "RPM", 17f)
text(x - 52, 444f, 145f, 30f, number(if (power) Parameter.POWER else Parameter.RPM), 22f)
text(x - 40, 478f, 125f, 22f, if (power) "RAW" else "rpm", 11f, "#8798ad")
}
val cx = 411f; val cy = 264f; val radius = 125f
val ring = RectF(cx - 120, cy - 120, cx + 120, cy + 120)
p.style = Paint.Style.STROKE; p.strokeWidth = 10f; p.strokeCap = Paint.Cap.BUTT
p.color = color("#78f033"); c.drawArc(ring, -90f, 180f, false, p)
p.color = color("#ff4935"); c.drawArc(ring, -90f, -180f, false, p)
for (degree in 0 until 360 step 5) {
val a = (degree - 90) * PI / 180
val major = degree % 30 == 0
fun xx(r: Int) = cx + r * cos(a).toFloat()
fun yy(r: Int) = cy + r * sin(a).toFloat()
line(xx(if (major) 115 else 118), yy(if (major) 115 else 118), xx(if (major) 137 else 126), yy(if (major) 137 else 126), "#e9f3ff", if (major) 2.3f else 1f)
if (major) text(xx(156) - 26, yy(156) - 16, 52f, 32f, min(degree, 360 - degree).toString() + if (degree % 90 == 0) "°" else "", 17f, bold = true)
}
val measured = values[Parameter.ANGLE]
val angle = preview[DeifControls.Field.ANGLE] ?: measured
val outline = if (angle != null) "#eeef69" else "#657181"
c.save(); c.translate(cx, cy); c.rotate((angle ?: 0.0).toFloat() + 90)
p.color = color(outline); p.style = Paint.Style.STROKE; p.strokeWidth = 5.3f; p.strokeJoin = Paint.Join.ROUND; p.strokeCap = Paint.Cap.ROUND
c.save(); c.scale(radius / 315, radius / 315); c.translate(-460f, -610f)
c.drawPath(Path().apply {
moveTo(399f, 550f); cubicTo(478f, 516f, 577f, 525f, 630f, 562f)
cubicTo(679f, 594f, 672f, 632f, 626f, 661f); cubicTo(570f, 701f, 478f, 702f, 399f, 674f); close()
}, p)
c.drawPath(Path().apply {
moveTo(399f, 550f); lineTo(360f, 552f); moveTo(318f, 563f)
cubicTo(278f, 575f, 278f, 619f, 312f, 642f); lineTo(315f, 629f); cubicTo(341f, 659f, 370f, 676f, 399f, 674f)
}, p)
c.drawPath(Path().apply {
moveTo(384f, 631f); cubicTo(348f, 615f, 307f, 555f, 317f, 506f)
cubicTo(329f, 466f, 350f, 418f, 366f, 438f); cubicTo(384f, 466f, 392f, 524f, 384f, 631f); close()
}, p)
c.drawLine(318f, 499f, 384f, 631f, p)
c.drawPath(Path().apply { moveTo(315f, 629f); cubicTo(304f, 681f, 320f, 780f, 350f, 785f); cubicTo(388f, 792f, 386f, 712f, 373f, 670f) }, p)
c.restore()
if (angle != null) {
p.style = Paint.Style.FILL
c.drawPath(Path().apply { moveTo(-115f, 0f); lineTo(-77f, -15f); lineTo(-77f, 15f); close() }, p)
c.drawPath(Path().apply { moveTo(-126f, 0f); lineTo(-138f, -5f); lineTo(-138f, 5f); close() }, p)
}
c.restore()
text(300f, 435f, 225f, 36f, number(Parameter.ANGLE, "°"), 25f)
text(278f, 478f, 275f, 22f, "AZIMUTH", 11f, "#8798ad")
c.restore()
}
}

View File

@@ -0,0 +1,167 @@
package ru.setcorp.setprotocol.deif
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
/** Reusable Compose port. The host supplies only transport and persistence. */
@Composable
fun DeifPanel(controller: DeifController, connected: Boolean, channel: String) {
val state by controller.state.collectAsState()
var clock by remember { mutableLongStateOf(0) }
LaunchedEffect(controller) { while (true) { delay(250); clock++ } }
var invalidInputs by remember { mutableStateOf(emptySet<Parameter>()) }
var showRaw by remember { mutableStateOf(false) }
var controlsDialog by remember { mutableStateOf(false) }
var configuring by remember { mutableStateOf(false) }
var displaySide by remember { mutableStateOf<Int?>(null) }
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("DEIF XDi · MASTER / SLAVE", style = MaterialTheme.typography.titleLarge)
Text("Канал: $channel · ${if (connected) "подключён" else "нет подключения"}")
Row {
Checkbox(state.unlocked, { controller.unlock(it) })
Text("Разблокировать настройки", Modifier.padding(top = 12.dp))
}
Text("${state.config.mode} · Node ${state.config.node} · STD 0x%03X · %d мс".format(state.config.canId, state.config.periodMs))
OutlinedButton(onClick = { configuring = true }, enabled = state.unlocked) { Text("Настроить передачу") }
Row(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button({ controller.start(false) }, enabled = connected && !state.unlocked && !state.running && invalidInputs.isEmpty()) { Text("Цикл") }
Button({ controller.start(true) }, enabled = connected && !state.unlocked && !state.running && invalidInputs.isEmpty()) { Text("Старт") }
OutlinedButton({ controller.stop() }, enabled = state.running) { Text("Стоп") }
OutlinedButton({ controller.setFullScreen(true) }) { Text("На весь экран") }
}
Text("Коснитесь или перетащите POWER, RPM либо винт. Жёлтый — задание, белый — показания.", style = MaterialTheme.typography.bodySmall)
val preview = remember(state.raw, state.controlScales) { DeifControls.preview(state.raw, state.controlScales) }
@Composable fun instrument(side: Int, modifier: Modifier) {
Column(modifier) {
val values = remember(state.readings, clock) { Parameter.entries.associateWith { controller.value(side, it) } }
DeifInstrument("${if (side == 0) "MASTER" else "SLAVE"} · Node ${state.displays[side].node}",
values, preview, { field, value ->
controller.requestInstrument(field, value)
invalidInputs = emptySet()
})
OutlinedButton({ displaySide = side }, enabled = state.unlocked) { Text("Настроить ${if (side == 0) "MASTER" else "SLAVE"}") }
}
}
BoxWithConstraints {
if (maxWidth >= 760.dp) Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
instrument(0, Modifier.weight(1f)); instrument(1, Modifier.weight(1f))
} else Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
instrument(0, Modifier.fillMaxWidth()); instrument(1, Modifier.fillMaxWidth())
}
}
OutlinedButton({ controlsDialog = true }, enabled = state.unlocked) { Text("Масштабы управления приборами") }
TextButton({ showRaw = !showRaw }) { Text(if (showRaw) "Скрыть RAW и выбор параметров" else "RAW и выбор параметров") }
if (showRaw) {
Parameter.entries.forEach { parameter ->
var input by remember(state.raw[parameter]) { mutableStateOf((state.raw[parameter] ?: 0).toString()) }
val raw = input.toIntOrNull()
Row(Modifier.fillMaxWidth()) {
Checkbox(parameter in state.config.enabled, { controller.select(parameter, it) })
OutlinedTextField(input, { text ->
input = text
val value = text.toIntOrNull()
invalidInputs = if (value != null && value in -32768..32767) invalidInputs - parameter else invalidInputs + parameter
if (value != null && value in -32768..32767) controller.setRaw(parameter, value)
else controller.stop("Исправьте RAW перед передачей")
}, label = { Text("${parameter.title} (${parameter.unit}) · RAW") },
isError = raw == null || raw !in -32768..32767, singleLine = true, modifier = Modifier.weight(1f))
}
Text("0x%04X:02%s · %s".format(parameter.index, if (parameter == Parameter.ANGLE) " + :07" else "",
raw?.let { "%.1f %s".format(it * parameter.scale, parameter.unit) } ?: "—"))
}
Text("AC2: физическая единица мощности не подтверждена. Угол передаётся как фактический и заданный.", style = MaterialTheme.typography.bodySmall)
}
Text(state.status)
Text("Передано: ${state.sent} · RX: ${state.received} · ошибок CAN: ${state.errors}")
Text("TX-эхо gs_usb: ${state.echoes}. Эхо не является ответом DEIF.")
if (channel.contains("gs_usb")) {
val echoStatus = remember(state.echoTimes, state.config.enabled, clock) {
state.config.enabled.flatMap { p -> (if (p == Parameter.ANGLE) listOf(2, 7) else listOf(2)).map { sub ->
"%04X:%02X — %s".format(p.index, sub, controller.echoAge(p.index, sub)?.let { if (it > 3000) "нет эха >3 с" else "$it мс" } ?: "нет эха")
} }.joinToString("\n")
}
Text(echoStatus, style = MaterialTheme.typography.bodySmall)
}
Text("Интервал: ${state.intervalMs} мс · макс. пауза: ${state.maxGapMs} мс · макс. цикл: ${state.maxCycleMs} мс")
Text("Heartbeat ${if (state.config.heartbeat) "включён · 500 мс" else "выключен"}. TX означает передачу в API адаптера, а не подтверждение DEIF.", style = MaterialTheme.typography.bodySmall)
Text(state.lastReceived)
Text("SLAVE не использует TX-эхо; отсутствие ответа допустимо. Данные скрываются через 5 с. NMT, LSS и SDO записи не отправляются.", style = MaterialTheme.typography.bodySmall)
}
if (controlsDialog) {
val scales = state.controlScales
val labels = listOf("POWER при 100%, RAW", "RPM при 100%, rpm", "Угол: RAW на 1°", "POWER%: RAW на 1%")
val values = remember { listOf(scales.power100, scales.rpm100, scales.angleRawDegree, scales.powerPercentRaw).map { mutableStateOf(it.toString()) } }
var problem by remember { mutableStateOf<String?>(null) }
AlertDialog(onDismissRequest = { controlsDialog = false }, title = { Text("Масштабы управления") }, text = {
Column(Modifier.verticalScroll(rememberScrollState())) {
Text("Оба индикатора меняют общие задания. При работающем master значения отправятся следующим циклом.")
labels.forEachIndexed { i, label -> OutlinedTextField(values[i].value, { values[i].value = it }, label = { Text(label) }) }
problem?.let { Text(it, color = MaterialTheme.colorScheme.error) }
}
}, confirmButton = { TextButton({
runCatching { controller.configureControls(DeifControls.Scales(values[0].value.toDouble(), values[1].value.toDouble(),
values[2].value.toDouble(), values[3].value.toDouble())) }
.onSuccess { controlsDialog = false }.onFailure { problem = it.message }
}) { Text("Применить") } }, dismissButton = { TextButton({ controlsDialog = false }) { Text("Отмена") } })
}
if (configuring) {
val c = state.config
var mode by remember { mutableStateOf(c.mode) }
var node by remember { mutableStateOf(c.node.toString()) }
var id by remember { mutableStateOf(c.canId.toString(16)) }
var period by remember { mutableStateOf(c.periodMs.toString()) }
var heartbeat by remember { mutableStateOf(c.heartbeat) }
var master by remember { mutableStateOf(c.masterNode.toString()) }
var problem by remember { mutableStateOf<String?>(null) }
AlertDialog(onDismissRequest = { configuring = false }, title = { Text("Передача DEIF") }, text = {
Column(Modifier.verticalScroll(rememberScrollState())) {
Row { DeifProtocol.Mode.entries.forEach { m -> FilterChip(mode == m, { mode = m }, label = { Text(m.name) }) } }
Text("SAM: источник; DAM: получатель. Адрес самого прибора не изменяется.")
OutlinedTextField(node, { node = it; it.toIntOrNull()?.let { n -> id = (0x200 + n).toString(16) } }, label = { Text("Node-ID (1…127)") })
OutlinedTextField(id, { id = it }, label = { Text("STD ID, HEX (201 / 20B / 30B…)") })
OutlinedTextField(period, { period = it }, label = { Text("Период, мс") })
Row { Checkbox(heartbeat, { heartbeat = it }); Text("Отправлять heartbeat") }
OutlinedTextField(master, { master = it }, label = { Text("Node-ID master для heartbeat DAM") })
problem?.let { Text(it, color = MaterialTheme.colorScheme.error) }
}
}, confirmButton = { TextButton({
runCatching { controller.configure(c.copy(mode = mode, node = node.toInt(), canId = id.removePrefix("0x").toInt(16),
periodMs = period.toLong(), heartbeat = heartbeat, masterNode = master.toInt())) }
.onSuccess { configuring = false }.onFailure { problem = it.message }
}) { Text("Применить") } }, dismissButton = { TextButton({ configuring = false }) { Text("Отмена") } })
}
displaySide?.let { side ->
val config = state.displays[side]
var node by remember { mutableStateOf(config.node.toString()) }
var id by remember { mutableStateOf(config.canId.toString(16)) }
val fields = remember { Parameter.entries.associateWith { p ->
val b = config.bindings.getValue(p)
mutableStateOf("%04X:%02X:%s".format(b.index, b.subindex, b.scale))
} }
var problem by remember { mutableStateOf<String?>(null) }
AlertDialog(onDismissRequest = { displaySide = null }, title = { Text("Привязки ${if (side == 0) "MASTER" else "SLAVE"}") }, text = {
Column(Modifier.verticalScroll(rememberScrollState())) {
OutlinedTextField(node, { node = it }, label = { Text("Node-ID: SAM источник / DAM получатель") })
OutlinedTextField(id, { id = it }, label = { Text("SAM CAN ID, HEX") })
Text("Формат: индекс HEX:подиндекс HEX:масштаб. Например 3001:02:0.1. DAM принимается по Node-ID на RPDO1…4.")
Parameter.entries.forEach { p -> OutlinedTextField(fields.getValue(p).value, { fields.getValue(p).value = it }, label = { Text("${p.title} ${p.unit}") }) }
problem?.let { Text(it, color = MaterialTheme.colorScheme.error) }
}
}, confirmButton = { TextButton({
runCatching {
controller.configureDisplay(side, DeifDisplayConfig(node.toInt(), id.removePrefix("0x").toInt(16), fields.mapValues { (_, text) ->
val parts = text.value.split(':'); require(parts.size == 3) { "Нужны индекс:подиндекс:масштаб" }
DeifBinding(parts[0].toInt(16), parts[1].toInt(16), parts[2].toDouble())
}))
}.onSuccess { displaySide = null }.onFailure { problem = it.message }
}) { Text("Применить") } }, dismissButton = { TextButton({ displaySide = null }) { Text("Отмена") } })
}
}

View File

@@ -0,0 +1,94 @@
package ru.setcorp.setprotocol.deif
import ru.setcorp.setflash.core.CanBridgeProtocol
/** XDi-net MPDO contract shared by Android hosts; matches set_devices/deif_xdi.py. */
object DeifProtocol {
enum class Mode { SAM, DAM }
enum class Parameter(val index: Int, val title: String, val unit: String, val scale: Double) {
ANGLE(0x3001, "Угол", "°", 0.1),
RPM(0x3081, "Обороты", "rpm", 0.1),
RPM_PERCENT(0x3091, "Обороты винта", "%", 0.1),
POWER(0x3421, "Мощность AC2", "RAW", 1.0),
POWER_PERCENT(0x3431, "Мощность AC2", "%", 0.1),
}
data class Config(
val mode: Mode = Mode.SAM, val node: Int = 1, val canId: Int = 0x201,
val periodMs: Long = 100, val heartbeat: Boolean = false, val masterNode: Int = 1,
val enabled: Set<Parameter> = Parameter.entries.toSet(),
) {
fun validate() {
require(node in 1..127 && masterNode in 1..127) { "Node-ID: 1…127" }
require(periodMs in 20..60000) { "Период: 20…60000 мс" }
require(if (mode == Mode.SAM) canId == 0x200 + node || canId == 0x27F
else canId - node in listOf(0x200, 0x300, 0x400, 0x500)) { "STD ID не соответствует режиму и Node-ID" }
}
}
data class Decoded(
val summary: String, val node: Int? = null, val index: Int? = null,
val subindex: Int? = null, val raw: Int? = null, val value: Double? = null,
val unit: String = "", val mode: Mode? = null, val valid: Boolean = false,
)
fun cycle(config: Config, raw: Map<Parameter, Int>): List<CanBridgeProtocol.Frame> {
config.validate()
val frames = Parameter.entries.filter { it in config.enabled }.map { p ->
dataFrame(config, p.index, 2, raw[p] ?: 0)
}.toMutableList()
if (Parameter.ANGLE in config.enabled) frames += dataFrame(config, 0x3001, 7, raw[Parameter.ANGLE] ?: 0)
return frames
}
private fun dataFrame(c: Config, index: Int, sub: Int, raw: Int): CanBridgeProtocol.Frame {
require(raw in -32768..32767) { "RAW: −32768…32767" }
return CanBridgeProtocol.buildFrame(c.canId.toLong(), byteArrayOf(
(c.node or if (c.mode == Mode.DAM) 0x80 else 0).toByte(),
index.toByte(), (index shr 8).toByte(), sub.toByte(), raw.toByte(), (raw shr 8).toByte(), 0, 0,
), extended = false)
}
fun heartbeat(config: Config): CanBridgeProtocol.Frame {
config.validate()
val node = if (config.mode == Mode.SAM) config.node else config.masterNode
return CanBridgeProtocol.buildFrame((0x700 + node).toLong(), byteArrayOf(5), extended = false)
}
fun decode(frame: CanBridgeProtocol.Frame): Decoded {
fun reject(s: String) = Decoded("DEIF: $s")
if (frame.isExtended || frame.isRemote || frame.isError) return reject("ожидается STD data frame")
val id = frame.canId.toInt()
val data = frame.data
if (id in 0x701..0x77F) {
if (data.size != 1) return reject("heartbeat: ожидается DLC=1")
val state = when (data[0].toInt() and 255) { 0 -> "Boot-up"; 5 -> "Operational"; else -> "состояние 0x%02X".format(data[0]) }
return Decoded("Node ${id - 0x700}: $state", node = id - 0x700, valid = true)
}
if (id !in 0x201..0x57F || data.size != 8) return reject("неизвестный ID или DLC (MPDO: 8 байт)")
val node = data[0].toInt() and 127
val mode = if (data[0].toInt() and 128 != 0) Mode.DAM else Mode.SAM
if (node == 0 || !(if (mode == Mode.DAM) id - node in listOf(0x200, 0x300, 0x400, 0x500)
else id in 0x201..0x27F && (id == 0x27F || id == 0x200 + node))) return reject("адрес не соответствует COB-ID")
val index = (data[1].toInt() and 255) or ((data[2].toInt() and 255) shl 8)
val sub = data[3].toInt() and 255
val parameter = Parameter.entries.find { it.index and 0xFFF0 == index and 0xFFF0 }
if (parameter == null || index and 15 == 0 || sub !in listOf(2, 7)) return reject("неизвестный объект 0x%04X:%02X".format(index, sub))
val raw = ((data[4].toInt() and 255) or ((data[5].toInt() and 255) shl 8)).toShort().toInt()
val status = when (raw) { -32768 -> "ниже диапазона"; 32766 -> "выше диапазона"; 32767 -> "ошибка данных"; else -> null }
val scaled = sub == 2 || parameter == Parameter.ANGLE
val value = if (status == null && scaled) raw * parameter.scale else null
val unit = if (scaled) parameter.unit else "масштаб не установлен"
val padding = if (data[6] != 0.toByte() || data[7] != 0.toByte()) " · ненулевое заполнение" else ""
val summary = "$mode Node $node · 0x%04X:%02X · %s · RAW=%d".format(index, sub, parameter.title, raw) +
" · " + (status ?: value?.let { "%.1f %s".format(it, unit) } ?: unit) + padding
return Decoded(summary, node, index, sub, raw, value, unit, mode, status == null && padding.isEmpty())
}
/** Only display data and Operational heartbeat may reach this transmit path. */
fun validateTransmit(frame: CanBridgeProtocol.Frame) {
val decoded = decode(frame)
val heartbeat = frame.canId in 0x701..0x77F && frame.data.contentEquals(byteArrayOf(5))
val data = frame.data
val known = decoded.index in Parameter.entries.map { it.index } &&
(decoded.subindex == 2 || decoded.index == 0x3001 && decoded.subindex == 7)
require(!frame.isExtended && !frame.isRemote && !frame.isError &&
(heartbeat || known && decoded.raw != null && data.size == 8 && data[6] == 0.toByte() && data[7] == 0.toByte())) {
"DEIF: разрешены только данные индикации и heartbeat"
}
}
}

View File

@@ -0,0 +1,93 @@
package ru.setcorp.setprotocol.deif
import org.junit.Assert.*
import org.junit.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.*
import ru.setcorp.setprotocol.deif.DeifControls.Field
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
@OptIn(ExperimentalCoroutinesApi::class)
class DeifControlsTest {
@Test fun fullScreenTogglePreservesLiveMasterAndIsNotRestoredOnNextLaunch() = runTest {
val sent = mutableListOf<ru.setcorp.setflash.core.CanBridgeProtocol.Frame>()
var saved = ""
val controller = DeifController(backgroundScope, { sent += it }, { saved = it })
controller.requestInstrument(Field.ANGLE, -90.0)
controller.start(true); runCurrent()
controller.setFullScreen(true)
val before = sent.size
advanceTimeBy(101); runCurrent()
assertTrue(sent.size > before)
assertTrue(controller.state.value.fullScreen)
assertEquals(-900, controller.state.value.raw[Parameter.ANGLE])
controller.setFullScreen(false)
assertTrue(controller.state.value.running)
assertFalse(controller.state.value.fullScreen)
val restored = DeifController(backgroundScope, {})
restored.restore(saved)
assertFalse(restored.state.value.fullScreen)
assertFalse(restored.state.value.running)
controller.stop()
}
@Test fun hitRegionsMatchSetguiAndLeaveBackgroundScrollable() {
assertEquals(Field.POWER, DeifControls.hit(64f, 126f))
assertEquals(Field.RPM, DeifControls.hit(192f, 396f))
assertEquals(Field.ANGLE, DeifControls.hit(411f, 120f))
assertNull(DeifControls.hit(10f, 10f))
assertNull(DeifControls.hit(250f, 450f))
}
@Test fun angleHasNorthZeroAndSignedLeftRight() {
for ((point, angle) in listOf((411f to 120f) to 0.0, (550f to 264f) to 90.0,
(411f to 410f) to 180.0, (270f to 264f) to -90.0)) {
assertEquals(angle, DeifControls.requested(Field.ANGLE, point.first, point.second)!!, 0.001)
}
assertNull(DeifControls.requested(Field.ANGLE, 411f, 264f))
}
@Test fun verticalDragsIncludeOverloadAndReverseRpm() {
assertEquals(120.0, DeifControls.requested(Field.POWER, 64f, 126f)!!, 0.001)
assertEquals(0.0, DeifControls.requested(Field.POWER, 64f, 600f)!!, 0.001)
assertEquals(-20.0, DeifControls.requested(Field.RPM, 192f, 396f)!!, 0.001)
assertEquals(120.0, DeifControls.requested(Field.RPM, 192f, -50f)!!, 0.001)
}
@Test fun gesturesUpdateAbsoluteAndPercentTogetherLikeSetgui() {
val scales = DeifControls.Scales()
assertEquals(mapOf(Parameter.POWER to 600, Parameter.POWER_PERCENT to 600), DeifControls.raw(Field.POWER, 60.0, scales))
assertEquals(mapOf(Parameter.RPM to 5000, Parameter.RPM_PERCENT to 500), DeifControls.raw(Field.RPM, 50.0, scales))
assertEquals(mapOf(Parameter.RPM to -2000, Parameter.RPM_PERCENT to -200), DeifControls.raw(Field.RPM, -20.0, scales))
assertEquals(mapOf(Parameter.ANGLE to -900), DeifControls.raw(Field.ANGLE, -90.0, scales))
}
@Test fun clampingNeverProducesStatusSentinelsAndKeepsPercentConsistent() {
val scales = DeifControls.Scales(rpm100 = 10000.0)
val raw = DeifControls.raw(Field.RPM, 120.0, scales)
assertEquals(32765, raw[Parameter.RPM])
assertEquals(328, raw[Parameter.RPM_PERCENT])
assertEquals(32.765, DeifControls.preview(raw, scales)[Field.RPM]!!, 0.0001)
assertEquals(-32767, DeifControls.raw(Field.ANGLE, -180.0, scales.copy(angleRawDegree = 1000.0))[Parameter.ANGLE])
}
@Test fun commandPreviewWorksBeforeTransmissionAndScalesPersist() = runTest {
var saved = ""
val controller = DeifController(backgroundScope, { error("Must not send while editing") }, { saved = it })
controller.unlock(true)
controller.configureControls(DeifControls.Scales(power100 = 2000.0))
controller.requestInstrument(Field.POWER, 60.0)
assertEquals(1200, controller.state.value.raw[Parameter.POWER])
assertEquals(60.0, DeifControls.preview(controller.state.value.raw, controller.state.value.controlScales)[Field.POWER]!!, 0.001)
assertNull(controller.value(0, Parameter.POWER))
val restored = DeifController(backgroundScope, {})
restored.restore(saved)
assertEquals(controller.state.value.controlScales, restored.state.value.controlScales)
}
@Test fun draggingDuringMasterChangesNextCycle() = runTest {
val sent = mutableListOf<ru.setcorp.setflash.core.CanBridgeProtocol.Frame>()
val controller = DeifController(backgroundScope, { sent += it })
controller.start(true); runCurrent()
controller.requestInstrument(Field.RPM, 50.0)
advanceTimeBy(101); runCurrent()
val rpm = sent.map(DeifProtocol::decode).filter { it.index == 0x3081 }
assertEquals(0, rpm.first().raw)
assertEquals(5000, rpm.last().raw)
assertTrue(controller.state.value.running)
controller.stop()
}
}

View File

@@ -0,0 +1,129 @@
package ru.setcorp.setprotocol.deif
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.*
import org.junit.Assert.*
import org.junit.Test
import ru.setcorp.setflash.core.CanBridgeProtocol
import ru.setcorp.setprotocol.deif.DeifProtocol.Config
import ru.setcorp.setprotocol.deif.DeifProtocol.Mode
import ru.setcorp.setprotocol.deif.DeifProtocol.Parameter
@OptIn(ExperimentalCoroutinesApi::class)
class DeifTest {
@Test fun matchesPythonSetguiReferenceCycles() {
val text = checkNotNull(javaClass.getResourceAsStream("/deif-setgui.json")).bufferedReader().use { it.readText() }
val cases = org.json.JSONArray(text)
repeat(cases.length()) { i ->
val c = cases.getJSONObject(i)
val raw = Parameter.entries.mapIndexed { n, p -> p to c.getJSONArray("raw").getInt(n) }.toMap()
val frames = DeifProtocol.cycle(Config(Mode.valueOf(c.getString("mode")), c.getInt("node"), c.getInt("canId")), raw)
frames.forEachIndexed { n, f ->
assertEquals("case $i frame $n", c.getJSONArray("frames").getString(n), f.data.joinToString("") { "%02x".format(it) })
DeifProtocol.validateTransmit(f)
}
}
}
private fun frame(id: Long, data: String, tx: Boolean = false) = CanBridgeProtocol.buildFrame(
id, CanBridgeProtocol.parseHex(data), extended = false, toCan = tx)
@Test fun setguiDamVectorAndBothAngles() {
val frames = DeifProtocol.cycle(Config(Mode.DAM, 11, 0x20B), mapOf(Parameter.POWER to 1234, Parameter.ANGLE to -900))
assertEquals(6, frames.size)
assertEquals(frame(0x20B, "8B 21 34 02 D2 04 00 00", true), frames[3])
assertEquals(frame(0x20B, "8B 01 30 02 7C FC 00 00", true), frames[0])
assertEquals(frame(0x20B, "8B 01 30 07 7C FC 00 00", true), frames[5])
assertTrue(frames.all { !it.isExtended && !it.isRemote })
}
@Test fun routingAndInputValidation() {
for (node in listOf(1, 11, 127)) for (base in listOf(0x200, 0x300, 0x400, 0x500)) {
val frames = DeifProtocol.cycle(Config(Mode.DAM, node, base + node), emptyMap())
assertTrue(frames.all { DeifProtocol.decode(it).valid })
frames.forEach(DeifProtocol::validateTransmit)
}
assertTrue(DeifProtocol.decode(frame(0x27F, "0B 81 30 02 D2 04 00 00")).valid)
assertFalse(DeifProtocol.decode(frame(0x201, "0B 81 30 02 D2 04 00 00")).valid)
assertThrows(IllegalArgumentException::class.java) { DeifProtocol.cycle(Config(node = 0), emptyMap()) }
assertThrows(IllegalArgumentException::class.java) { DeifProtocol.cycle(Config(), mapOf(Parameter.RPM to 32768)) }
assertThrows(IllegalArgumentException::class.java) { DeifProtocol.cycle(Config(Mode.SAM, 11, 0x30B), emptyMap()) }
}
@Test fun decoderScalesStatusesAndRejectsUnknownFrames() {
assertEquals(-90.0, DeifProtocol.decode(frame(0x201, "01 01 30 07 7C FC 00 00")).value!!, 0.001)
assertEquals(123.4, DeifProtocol.decode(frame(0x201, "01 81 30 02 D2 04 00 00")).value!!, 0.001)
assertEquals("RAW", DeifProtocol.decode(frame(0x201, "01 21 34 02 D2 04 00 00")).unit)
assertNull(DeifProtocol.decode(frame(0x201, "01 81 30 07 D2 04 00 00")).value)
listOf("00 80", "FE 7F", "FF 7F").forEach { raw ->
val decoded = DeifProtocol.decode(frame(0x201, "01 81 30 02 $raw 00 00"))
assertFalse(decoded.valid); assertNull(decoded.value)
}
assertFalse(DeifProtocol.decode(frame(0x201, "01 81 30 02 00 00 00 01")).valid)
assertFalse(DeifProtocol.decode(frame(0x201, "01 81 30 02 00 00 00 00").copy(flags = CanBridgeProtocol.FLAG_IDE)).valid)
assertFalse(DeifProtocol.decode(frame(0x201, "01 81 30")).valid)
}
@Test fun transmitGuardRejectsConfigurationAndBadAddresses() {
listOf(frame(0, "01 0B"), frame(0x60B, "23 00 20 00 01 00 00 00"), frame(0x7E5, "04"),
frame(0x70B, "00"), frame(0x201, "0B 81 30 02 00 00 00 00")).forEach {
assertThrows(IllegalArgumentException::class.java) { DeifProtocol.validateTransmit(it) }
}
DeifProtocol.validateTransmit(DeifProtocol.heartbeat(Config()))
assertEquals(0x701L, DeifProtocol.heartbeat(Config(Mode.DAM, 11, 0x20B)).canId)
}
@Test fun heartbeatHasIndependentPeriodAndStopsOnUnlock() = runTest {
val sent = mutableListOf<CanBridgeProtocol.Frame>()
val controller = DeifController(backgroundScope, { sent += it }, now = { testScheduler.currentTime })
controller.unlock(true); controller.configure(Config(periodMs = 1000, heartbeat = true)); controller.unlock(false)
controller.start(true); runCurrent()
assertEquals(6, sent.count { it.data.size == 8 }); assertEquals(1, sent.count { it.data.size == 1 })
advanceTimeBy(501); runCurrent()
assertEquals(6, sent.count { it.data.size == 8 }); assertEquals(2, sent.count { it.data.size == 1 })
controller.unlock(true)
val count = sent.size; advanceTimeBy(2000); runCurrent(); assertEquals(count, sent.size)
assertFalse(controller.state.value.running)
}
@Test fun singleCycleHasNoHeartbeatAndSelectionStopsRepeat() = runTest {
val sent = mutableListOf<CanBridgeProtocol.Frame>()
val controller = DeifController(backgroundScope, { sent += it })
controller.unlock(true); controller.configure(Config(heartbeat = true, enabled = setOf(Parameter.RPM))); controller.unlock(false)
controller.start(false); runCurrent()
assertEquals(1, sent.size); assertEquals(8, sent.single().data.size)
assertFalse(controller.state.value.running)
controller.start(true); runCurrent(); controller.select(Parameter.RPM, false)
val count = sent.size; advanceTimeBy(1500); runCurrent(); assertEquals(count, sent.size)
}
@Test fun failedTransportStopsBothTimers() = runTest {
var calls = 0
val controller = DeifController(backgroundScope, { calls++; error("disconnected") })
controller.start(true); runCurrent(); advanceTimeBy(1000); runCurrent()
assertEquals(1, calls); assertFalse(controller.state.value.running)
assertEquals(0L, controller.state.value.sent)
}
@Test fun slaveNeverUsesLocalTxOrEchoAndExpires() = runTest {
var time = 0L
val controller = DeifController(backgroundScope, {}, now = { time })
controller.setRaw(Parameter.RPM, 1234); controller.start(false); runCurrent()
assertEquals(123.4, controller.value(0, Parameter.RPM)!!, 0.001)
assertNull(controller.value(1, Parameter.RPM))
controller.receive(frame(0x201, "01 81 30 02 D2 04 00 00", true))
assertNull(controller.value(1, Parameter.RPM)); assertEquals(1L, controller.state.value.echoes)
controller.receive(frame(0x201, "01 81 30 02 D2 04 00 00"))
assertEquals(123.4, controller.value(1, Parameter.RPM)!!, 0.001)
time = 5001; assertNull(controller.value(1, Parameter.RPM))
controller.receive(frame(0x201, "01 81 30 02 FF 7F 00 00"))
assertNull(controller.value(1, Parameter.RPM))
}
@Test fun settingsRestoreIsLockedAndStoppedAndKeepsBindings() = runTest {
var saved = ""
val controller = DeifController(backgroundScope, {}, { saved = it })
controller.unlock(true); controller.configure(Config(Mode.DAM, 11, 0x30B, heartbeat = true))
controller.configureDisplay(1, DeifDisplayConfig(11, 0x30B))
controller.setRaw(Parameter.POWER, 1234)
val restored = DeifController(backgroundScope, {})
restored.restore(saved)
assertEquals(controller.state.value.config, restored.state.value.config)
assertEquals(11, restored.state.value.displays[1].node)
assertEquals(1234, restored.state.value.raw[Parameter.POWER])
assertFalse(restored.state.value.running); assertFalse(restored.state.value.unlocked)
restored.receive(frame(0x30B, "8B 21 34 02 D2 04 00 00"))
assertEquals(1234.0, restored.value(1, Parameter.POWER)!!, 0.001)
}
}

View File

@@ -61,7 +61,7 @@ int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
relative = can_id - BALSAM_CAN_BASE_ID;
output->direction = relative >= BALSAM_CAN_DATA_OFFSET
? BALSAM_CAN_FROM_NODE : BALSAM_CAN_TO_NODE;
output->device = (uint8_t)((relative & 0x0FU) + 1U); /* В ecan.c перед сборкой CAN ID выполняется DevNum--; маска 0x0F выделяет индекс 0..12, а +1 восстанавливает номер Mode. */
output->device = (uint8_t)((relative & 0x0FU) + 1U);
if (can_id == 0x00745009UL || can_id == 0x00745019UL) {
output->device = 10U;
output->direction = can_id == 0x00745019UL
@@ -80,9 +80,9 @@ int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
const char *balsam_can_device_name(uint8_t device)
{
static const char *const names[BALSAM_CAN_NODE_COUNT] = {
"Трансформатор 1", "Трансформатор 2", "Силовой 1", /* Первые три узла соответствуют строкам 1..3 предоставленной таблицы адресов 167. */
"Силовой 2", "УМП 1", "УМП 2", "Двигатель", "Задатчик", /* Строки 4..8 таблицы; VPU1_CAN_DEVICE=8 соответствует задатчику, а не ВЭП. */
"ВЭП", "ПМУ", "Узел 11", "Узел 12", "Терминал" /* Строка 9 — ВЭП, пара 0xBA0009/0xBA0019 — ПМУ; служебные узлы 11..13 сохраняют прежние назначения. */
"Трансформатор 1", "Трансформатор 2", "Силовой блок 1",
"Силовой блок 2", "УМП 1", "УМП 2", "Двигатель", "ВЭП",
"Задатчик", "Пульт (МПУ)", "Узел 11", "Узел 12", "Терминал"
};
return (device >= 1U && device <= BALSAM_CAN_NODE_COUNT)
? names[device - 1U] : "Неизвестный узел";
@@ -91,13 +91,6 @@ const char *balsam_can_device_name(uint8_t device)
size_t balsam_can_register_name(uint8_t device, uint16_t address,
char *output, size_t output_size)
{
if (address == 0x0100U) /* CAN_HEARTBEAT_ADDR в ecan.c периферии 167 задаёт служебный адрес вне технологической карты R0..R127. */
return copy_name("Heartbeat: счётчик, с", output, output_size); /* Первое слово hiword содержит секунды после запуска по модулю 65536. */
if (address == 0x0101U) /* Следующее слово heartbeat соответствует старшим 16 битам loword в CAN_send. */
return copy_name("Heartbeat: Mode", output, output_size); /* Прошивка передаёт аппаратный Mode без уменьшения на единицу, применяемого только к CAN ID. */
if (address == 0x0102U) /* Третье слово heartbeat соответствует младшим 16 битам loword. */
return copy_name("Heartbeat: тип платы (Desk)", output, output_size); /* Значение Desk задаёт тип платы и отличается от номера Mode и номера CAN-узла. */
if ((device == 1U || device == 2U) && address < 20U)
return indexed_name("Диагностика T°", address + 1U, output, output_size);
if ((device == 1U || device == 2U) && address >= 0x18U && address <= 0x2BU)
@@ -146,7 +139,7 @@ size_t balsam_can_register_name(uint8_t device, uint16_t address,
if (device == 7U && address >= 0x18U && address <= 0x1FU)
return indexed_name("Показания T°", address - 0x17U, output, output_size);
if (device == 9U) { /* Карта напряжений принадлежит ВЭП с adr_SHKF=9; задатчик с номером 8 использует другую карту. */
if (device == 8U) {
static const char *const vep_names[] = {
"Диагностика 380В Ф1", "Диагностика 380В Ф2",
"Диагностика 31В Ф1", "Диагностика 31В Ф2",
@@ -171,15 +164,15 @@ size_t balsam_can_register_name(uint8_t device, uint16_t address,
return copy_name("Дискретные датчики (14 бит)", output, output_size);
}
if (device == 8U && address == 0U) /* У VPU1_CAN_DEVICE=8 слово 0 передаёт обороты ГЭД, а не диагностику ВЭП. */
if (device == 9U && address == 0U)
return copy_name("Обороты ГЭД, об/мин", output, output_size);
if (device == 8U && address == 1U) /* Слово 1 той же платы задатчика содержит обороты гребного вала. */
if (device == 9U && address == 1U)
return copy_name("Обороты ГВ, об/мин", output, output_size);
if (device == 8U && address == 2U) /* ADR_CAN_LAMPS_ON_VPU=2 задаёт регистр первой группы ламп задатчика. */
if (device == 9U && address == 2U)
return copy_name("Лампы (8 бит)", output, output_size);
if (device == 8U && address == 3U) /* ADR_CAN_LAMPS_2_ON_VPU=3 задаёт регистр второй группы индикаторов задатчика. */
if (device == 9U && address == 3U)
return copy_name("Диоды (16 бит)", output, output_size);
if (device == 8U && address == 0x10U) /* ADR_CAN_KEY_ON_VPU=16, то есть 0x10, задаёт слово кнопок VPU; адрес не перенумеровывается. */
if (device == 9U && address == 0x10U)
return copy_name("Кнопки (12 бит)", output, output_size);
if (address == 0x16U)

View File

@@ -7,8 +7,6 @@
#include "set_plot.h"
#include <math.h>
#define MIN_VIEW_SPAN 1e-12 /* Минимальная безразмерная доля полного диапазона по X и Y задаёт увеличение до 10^12 раз; запас относительно точности double сохраняет различимые границы нормированной области. */
static double clamp(double v, double lo, double hi) { return fmin(hi, fmax(lo, v)); }
static int finite_values(const double *v, size_t n) {
size_t i;
@@ -25,13 +23,13 @@ size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t
cap < (op == SET_PLOT_TRANSFORM || op == SET_PLOT_LIMITS ? 4U : 1U)) return 0;
if (op == SET_PLOT_TRANSFORM) {
double w, h, fx, fy;
if (!finite_values(v, 4) || v[2] < MIN_VIEW_SPAN || v[2] > 1 || /* Проверяем конечность смещений и размеров, разрешая ширину до триллионной доли исходного диапазона X. */
v[3] < MIN_VIEW_SPAN || v[3] > 1 || v[0] < 0 || v[1] < 0 || /* Такой же предел для Y сохраняет возможность дальнейшего перемещения и увеличения уже приближённой области. */
if (!finite_values(v, 4) || v[2] < 1.0/128 || v[2] > 1 ||
v[3] < 1.0/128 || v[3] > 1 || v[0] < 0 || v[1] < 0 ||
v[0] + v[2] > 1.000000000001 || v[1] + v[3] > 1.000000000001) return 0;
out[0] = v[0]; out[1] = v[1]; out[2] = v[2]; out[3] = v[3];
if (!finite_values(v + 4, 6) || v[4] <= 0 || v[5] <= 0) return 4;
w = clamp(v[2] / v[4], MIN_VIEW_SPAN, 1); /* Делим текущую долю X на положительный коэффициент приближения; ограничиваем результат между новым пределом и полным диапазоном. */
h = clamp(v[3] / v[5], MIN_VIEW_SPAN, 1); /* Аналогично масштабируем Y, снимая прежнее ограничение 128 раз и сохраняя ненулевую высоту для координатных преобразований. */
w = clamp(v[2] / v[4], 1.0/128, 1);
h = clamp(v[3] / v[5], 1.0/128, 1);
fx = clamp(v[8], 0, 1); fy = clamp(v[9], 0, 1);
out[0] = clamp(v[0] + (v[2] - w) * fx - v[6] * w, 0, 1 - w);
out[1] = clamp(v[1] + (v[3] - h) * fy - v[7] * h, 0, 1 - h);

View File

@@ -40,7 +40,7 @@ int set_signal_reconstruct(const double *input_x,const double *input_y,size_t co
double coef[6]={0},r[6][6]={{0}},origin,span,scale=1,rmse=0;
size_t i,j,k,n=0; unsigned pass;
if(!input_x||!input_y||!out_x||!out_y||!meta||!work || count<2||count>100000
||output_count<2||output_count>1000000||work_count<14*count+128
||output_count<2||output_count>10000||work_count<14*count+128
||method<0||method>3||(endpoint!=0&&endpoint!=1)) return 1;
pairs=(point *)work; x=work+2*count; y=x+count; d=y+count; h=d+count;
s=h+count; q=s+count; column=q+6*count;

View File

@@ -0,0 +1,722 @@
[
{
"mode": "SAM",
"node": 1,
"canId": 513,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"010130027cfc0000",
"01813002d2040000",
"01913002e8030000",
"01213402d2040000",
"01313402f4010000",
"010130077cfc0000"
]
},
{
"mode": "SAM",
"node": 1,
"canId": 513,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"0101300200800000",
"01813002ffff0000",
"0191300200000000",
"01213402fe7f0000",
"01313402ff7f0000",
"0101300700800000"
]
},
{
"mode": "SAM",
"node": 1,
"canId": 639,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"010130027cfc0000",
"01813002d2040000",
"01913002e8030000",
"01213402d2040000",
"01313402f4010000",
"010130077cfc0000"
]
},
{
"mode": "SAM",
"node": 1,
"canId": 639,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"0101300200800000",
"01813002ffff0000",
"0191300200000000",
"01213402fe7f0000",
"01313402ff7f0000",
"0101300700800000"
]
},
{
"mode": "SAM",
"node": 11,
"canId": 523,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"0b0130027cfc0000",
"0b813002d2040000",
"0b913002e8030000",
"0b213402d2040000",
"0b313402f4010000",
"0b0130077cfc0000"
]
},
{
"mode": "SAM",
"node": 11,
"canId": 523,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"0b01300200800000",
"0b813002ffff0000",
"0b91300200000000",
"0b213402fe7f0000",
"0b313402ff7f0000",
"0b01300700800000"
]
},
{
"mode": "SAM",
"node": 11,
"canId": 639,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"0b0130027cfc0000",
"0b813002d2040000",
"0b913002e8030000",
"0b213402d2040000",
"0b313402f4010000",
"0b0130077cfc0000"
]
},
{
"mode": "SAM",
"node": 11,
"canId": 639,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"0b01300200800000",
"0b813002ffff0000",
"0b91300200000000",
"0b213402fe7f0000",
"0b313402ff7f0000",
"0b01300700800000"
]
},
{
"mode": "SAM",
"node": 127,
"canId": 639,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"7f0130027cfc0000",
"7f813002d2040000",
"7f913002e8030000",
"7f213402d2040000",
"7f313402f4010000",
"7f0130077cfc0000"
]
},
{
"mode": "SAM",
"node": 127,
"canId": 639,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"7f01300200800000",
"7f813002ffff0000",
"7f91300200000000",
"7f213402fe7f0000",
"7f313402ff7f0000",
"7f01300700800000"
]
},
{
"mode": "SAM",
"node": 127,
"canId": 639,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"7f0130027cfc0000",
"7f813002d2040000",
"7f913002e8030000",
"7f213402d2040000",
"7f313402f4010000",
"7f0130077cfc0000"
]
},
{
"mode": "SAM",
"node": 127,
"canId": 639,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"7f01300200800000",
"7f813002ffff0000",
"7f91300200000000",
"7f213402fe7f0000",
"7f313402ff7f0000",
"7f01300700800000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 513,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"810130027cfc0000",
"81813002d2040000",
"81913002e8030000",
"81213402d2040000",
"81313402f4010000",
"810130077cfc0000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 513,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8101300200800000",
"81813002ffff0000",
"8191300200000000",
"81213402fe7f0000",
"81313402ff7f0000",
"8101300700800000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 769,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"810130027cfc0000",
"81813002d2040000",
"81913002e8030000",
"81213402d2040000",
"81313402f4010000",
"810130077cfc0000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 769,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8101300200800000",
"81813002ffff0000",
"8191300200000000",
"81213402fe7f0000",
"81313402ff7f0000",
"8101300700800000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 1025,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"810130027cfc0000",
"81813002d2040000",
"81913002e8030000",
"81213402d2040000",
"81313402f4010000",
"810130077cfc0000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 1025,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8101300200800000",
"81813002ffff0000",
"8191300200000000",
"81213402fe7f0000",
"81313402ff7f0000",
"8101300700800000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 1281,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"810130027cfc0000",
"81813002d2040000",
"81913002e8030000",
"81213402d2040000",
"81313402f4010000",
"810130077cfc0000"
]
},
{
"mode": "DAM",
"node": 1,
"canId": 1281,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8101300200800000",
"81813002ffff0000",
"8191300200000000",
"81213402fe7f0000",
"81313402ff7f0000",
"8101300700800000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 523,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"8b0130027cfc0000",
"8b813002d2040000",
"8b913002e8030000",
"8b213402d2040000",
"8b313402f4010000",
"8b0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 523,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8b01300200800000",
"8b813002ffff0000",
"8b91300200000000",
"8b213402fe7f0000",
"8b313402ff7f0000",
"8b01300700800000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 779,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"8b0130027cfc0000",
"8b813002d2040000",
"8b913002e8030000",
"8b213402d2040000",
"8b313402f4010000",
"8b0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 779,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8b01300200800000",
"8b813002ffff0000",
"8b91300200000000",
"8b213402fe7f0000",
"8b313402ff7f0000",
"8b01300700800000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 1035,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"8b0130027cfc0000",
"8b813002d2040000",
"8b913002e8030000",
"8b213402d2040000",
"8b313402f4010000",
"8b0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 1035,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8b01300200800000",
"8b813002ffff0000",
"8b91300200000000",
"8b213402fe7f0000",
"8b313402ff7f0000",
"8b01300700800000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 1291,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"8b0130027cfc0000",
"8b813002d2040000",
"8b913002e8030000",
"8b213402d2040000",
"8b313402f4010000",
"8b0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 11,
"canId": 1291,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"8b01300200800000",
"8b813002ffff0000",
"8b91300200000000",
"8b213402fe7f0000",
"8b313402ff7f0000",
"8b01300700800000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 639,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"ff0130027cfc0000",
"ff813002d2040000",
"ff913002e8030000",
"ff213402d2040000",
"ff313402f4010000",
"ff0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 639,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"ff01300200800000",
"ff813002ffff0000",
"ff91300200000000",
"ff213402fe7f0000",
"ff313402ff7f0000",
"ff01300700800000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 895,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"ff0130027cfc0000",
"ff813002d2040000",
"ff913002e8030000",
"ff213402d2040000",
"ff313402f4010000",
"ff0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 895,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"ff01300200800000",
"ff813002ffff0000",
"ff91300200000000",
"ff213402fe7f0000",
"ff313402ff7f0000",
"ff01300700800000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 1151,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"ff0130027cfc0000",
"ff813002d2040000",
"ff913002e8030000",
"ff213402d2040000",
"ff313402f4010000",
"ff0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 1151,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"ff01300200800000",
"ff813002ffff0000",
"ff91300200000000",
"ff213402fe7f0000",
"ff313402ff7f0000",
"ff01300700800000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 1407,
"raw": [
-900,
1234,
1000,
1234,
500
],
"frames": [
"ff0130027cfc0000",
"ff813002d2040000",
"ff913002e8030000",
"ff213402d2040000",
"ff313402f4010000",
"ff0130077cfc0000"
]
},
{
"mode": "DAM",
"node": 127,
"canId": 1407,
"raw": [
-32768,
-1,
0,
32766,
32767
],
"frames": [
"ff01300200800000",
"ff813002ffff0000",
"ff91300200000000",
"ff213402fe7f0000",
"ff313402ff7f0000",
"ff01300700800000"
]
}
]

View File

@@ -4,7 +4,7 @@
{"name":"zoom_x_anchor","op":0,"input":[0,0,1,1,2,1,0,0,0.25,0.5],"output":[0.125,0,0.5,1]},
{"name":"zoom_y_anchor","op":0,"input":[0,0,1,1,1,4,0,0,0.5,0.75],"output":[0,0.5625,1,0.25]},
{"name":"pan_clamp","op":0,"input":[0.25,0.25,0.5,0.5,1,1,20,-20,0.5,0.5],"output":[0,0.5,0.5,0.5]},
{"name":"zoom_limit","op":0,"input":[0,0,1,1,1e15,1e15,0,0,0.5,0.5],"output":[0.4999999999995,0.4999999999995,1e-12,1e-12]},
{"name":"zoom_limit","op":0,"input":[0,0,1,1,1000,1000,0,0,0.5,0.5],"output":[0.49609375,0.49609375,0.0078125,0.0078125]},
{"name":"ignore_invalid_zoom","op":0,"input":[0.25,0.25,0.5,0.5,0,1,0,0,0.5,0.5],"output":[0.25,0.25,0.5,0.5]},
{"name":"horizontal_spread","op":1,"input":[100,2,8],"output":[1]},
{"name":"vertical_contract","op":1,"input":[2,-100,8],"output":[2]},

View File

@@ -7,7 +7,6 @@ Device Name — отображаемое имя адреса, а не допол
| DeviceType | DeviceID | Device Name | Назначение |
|---|---|---|---|
| `0x0` | существующие | Верхний уровень | Существующие адреса исходной таблицы |
| `0x3` | `0x7` | **F28335-M95M02** | Загрузка прошивки по ProtoCAN в EEPROM M95M02 платы TMS320F28335; ProductType `0x2833` |
| `0x6` | `0xE` | **Altera Logic** | Онлайн-анализатор 16 цифровых каналов |
| `0x7` | `0xD` | configurator | Существующий конфигуратор |
| `0x7` | `0xF` | KONOR / SETTINGS | Существующий профиль SETCAN и привязка датчиков |

Some files were not shown because too many files have changed in this diff Show More