80 Commits

Author SHA1 Message Date
7dfa8bd94d Добавить общие компоненты DEIF для Android и интерактивные приборы
Реализовать SAM/DAM, циклическую передачу, heartbeat, привязки и диагностику. Перенести рисунок и управление MASTER/SLAVE из SETGUI, добавить полноэкранный режим и проверки совместимости кадров.
2026-10-04 23:32:49 +03:00
a63c1dd7d8 merge: объедини сервисы UMP и USB-загрузчики с актуальным API 2026-10-02 12:45:31 +03:00
083ab55cb6 merge: сохрани уже перенесённую историю графиков и NAND 2026-10-02 12:41:51 +03:00
d2578299d8 merge: сохрани историю графиков и bxCAN, уже включённых в main 2026-10-02 12:41:36 +03:00
fdfae8fa6c merge: включи доработки логгера и портов SETGUI 2026-10-02 12:40:48 +03:00
66fad32a81 merge: объедини локальные доработки templates в main 2026-10-02 12:40:34 +03:00
f7a6f767e1 feat(gas-logger): добавь непрерывный логгер и JSON-карту 2026-10-01 22:54:29 +03:00
675ce063f0 feat(qt-ports): унифицировать выбор последовательного порта 2026-10-01 22:40:20 +03:00
3d9174a124 fix(ump): поддержать индексное чтение записей ПМ67 2026-10-01 22:40:19 +03:00
46ecbe3210 docs: опиши работу с сабмодулями и добавь сборку Doxygen 2026-10-01 18:19:38 +03:00
6df5674996 feat(hdl): добавь двухтактовый буфер логического анализатора 2026-10-01 18:19:38 +03:00
196d083098 feat(tms2812): добавь общий протокол терминала и привязки Python и Android 2026-10-01 18:19:37 +03:00
c0ee7c46ea Support PM67 voltage fields in UMP logger v3 2026-10-01 16:00:25 +03:00
3457657979 Объединить протокол терминала TMS2812 для Python и Android 2026-10-01 15:41:43 +03:00
79411dbc26 Add shared emulator services, waveform generation and STM32 USB boot ports 2026-09-28 11:54:17 +03:00
5260186c16 Добавить протокол Altera Logic и общие клиенты прошивки 2026-09-19 07:12:09 +03:00
10bfa587c7 perf(ds18b20-ds2480): передавай байты в Data Mode 2026-09-15 22:45:07 +03:00
ac3025ee32 feat(ds18b20-ds2480): добавь порт F407 и асинхронную подтяжку 2026-09-15 22:32:30 +03:00
37508f058b merge: сохрани историю NAND, уже включённую в c/parallel-nand 2026-09-15 22:15:19 +03:00
3334f4cddd Merge remote-tracking branch 'origin/feature/konor-runtime-settings' 2026-09-15 22:15:07 +03:00
aedd5fc082 merge: объедини DS2480 и актуальный templates в main 2026-09-15 22:15:06 +03:00
9ba3742b29 feat(ds18b20-ds2480): добавь драйвер датчиков через UART-мост 2026-09-15 16:57:33 +03:00
4f23f48896 Объединить разработки CAN и STM32 в master 2026-09-15 15:57:48 +03:00
8e9fd144ca Сохранить текущие изменения NAND и LED перед объединением веток 2026-09-15 15:57:06 +03:00
49c65ea78d fix(set-protocol): сохрани совместимость порта с ARMCC 2026-09-07 08:42:02 +03:00
34f728cfaa fix(set-protocol): убери libc из порта DevBoard F407 2026-09-07 08:41:26 +03:00
72205216f5 feat(set-protocol): добавь порт F407 для DevBoard_V1 2026-09-07 08:32:02 +03:00
5d48ddf0b0 Добавить HTML-справочник CAN v1 и v2 2026-09-06 03:09:53 +03:00
245d486a63 Добавить HTML-справочник CAN v1 и v2 2026-09-06 03:03:59 +03:00
e262b0acdd Добавить порт SETProtocol v2 для TMS320F2812 2026-09-06 02:14:17 +03:00
ed92ec346e Расширить общий протокол CAN_Bal_2812 2026-09-06 01:06:30 +03:00
3d374e7777 Добавить общую модель конструктора ProtoCAN для Android 2026-09-06 00:11:25 +03:00
d03e039cb4 Merge remote-tracking branch 'origin/trend-display-scale' into HEAD 2026-09-05 11:43:08 +03:00
d002b14e4e Share PM67 upload protocol through C core 2026-09-05 11:37:03 +03:00
11007b0704 Добавить множитель и IQ в Python-тренды 2026-09-05 03:45:05 +03:00
821202b655 Добавить выпуск прошивок из Keil и CCS 2026-09-05 03:16:16 +03:00
8596918bf7 Добавить публикацию каталога прошивок 2026-09-05 03:05:17 +03:00
bada3bbe8d Сохранить совместимость ABI графиков 2026-09-05 02:44:08 +03:00
87f2f2f56b Дополнить общий контракт границ осей 2026-09-05 02:43:07 +03:00
5523f049ec Расширить общие API графиков и GAS обмена 2026-09-05 02:37:11 +03:00
b08ea02a5c Align Android parser statistics with C core 2026-09-04 21:10:02 +03:00
1b3a6318eb Move PM35 protocol into shared C core 2026-09-04 21:06:40 +03:00
57622ccd66 Добавить Android API протокола ПМ35 2026-09-04 20:39:04 +03:00
ba529f579c Merge pull request 'Расширить графики и общие протоколы CAN и Periph28335' (#4) from feature/plot-can-periph-updates into master
Reviewed-on: #4
2026-09-04 19:18:54 +03:00
4f788d39d6 Убрать лишнюю строку в Legacy CAN API 2026-09-04 19:17:27 +03:00
1799b00167 Добавить канал TMS в каталог прошивок 2026-09-04 19:15:48 +03:00
940d9ada76 Добавить общий протокол Periph28335 2026-09-04 19:15:48 +03:00
6eaa47d3a8 Добавить общий Python API старого CAN terminal 2026-09-04 19:15:47 +03:00
1f49ba49bb Добавить общий API старого CAN terminal 2026-09-04 19:15:47 +03:00
b7f00acb24 feat(plot): lock axes and calculate marker levels in dB 2026-09-04 19:15:47 +03:00
70b2ee3cad feat(plot): configure one or two marker pairs per axis 2026-09-04 19:15:47 +03:00
b3ec401318 Добавить канал TMS в каталог прошивок 2026-09-04 19:10:06 +03:00
666173b2f0 Добавить общий протокол Periph28335 2026-09-04 18:59:54 +03:00
17259f5e28 Добавить общий Python API старого CAN terminal 2026-09-04 18:52:36 +03:00
79ac0d52a7 Добавить общий API старого CAN terminal 2026-09-04 18:19:38 +03:00
52d687b5ae feat(plot): lock axes and calculate marker levels in dB 2026-09-04 13:50:20 +03:00
7897fe9f0d feat(plot): configure one or two marker pairs per axis 2026-09-04 12:56:16 +03:00
45582cda4a Merge pull request 'Добавить общие графики и драйвер parallel NAND' (#3) from feature/shared-plot-and-parallel-nand into master
Reviewed-on: #3
2026-09-04 12:39:12 +03:00
2bb9fea90a Убрать лишнюю строку в модели маркеров 2026-09-04 12:36:56 +03:00
6392d36741 Добавить общий Python-парсер каталога прошивок 2026-09-04 12:31:42 +03:00
0c635ce1be Добавить безопасный клиент каталога прошивок Android 2026-09-04 12:30:37 +03:00
5caca07ad4 Поддержать настройки трендов в Python 3.8 2026-09-04 12:23:07 +03:00
31a111ee46 Добавить переносимый драйвер parallel NAND 2026-09-04 12:22:21 +03:00
20f63bc389 Добавить общие графики, декодер KONOR и порт STM32 bxCAN 2026-09-04 12:22:17 +03:00
0a30c39e07 feat(plot): add eight markers and high-rate timestamps 2026-09-04 10:25:27 +03:00
e8490b3122 Merge K:/git_project/newProject/templates into codex/shared-plot-interaction 2026-09-03 16:02:26 +03:00
a14140f794 feat(gui): share plot interaction, trend and spectrum core across JNI and Python 2026-09-03 16:02:25 +03:00
261f2ca6f1 refactor(stm32-bxcan): перенеси SETCAN в порт templates
Сохрани API PROTOCAN и историю SETCAN вторым родителем коммита. Подключи pcan_id из общего ядра, обнови пути и инструкции. Отдельный репозиторий и вложенный сабмодуль больше не требуются.
2026-09-02 15:49:19 +03:00
c8eec55978 Подключить ProtoCAN из общего SETProtocol 2026-09-01 11:50:08 +03:00
ab60e58318 docs: consolidate documentation under doc 2026-08-30 06:12:49 +03:00
94fc8c5ef2 Добавить HTML-документацию 2026-08-30 03:44:47 +03:00
58da8aa2fe Добавить типы сообщений ProtoCAN Boot 2026-08-29 22:18:26 +03:00
bd8d22312e docs: document CAN bootloader protocol 2026-08-29 21:45:30 +03:00
01de7651e8 добавил ссылку на документ яндекс 2026-08-28 13:45:39 +03:00
34c46ec679 Merge branch 'master' of https://git.rd12.ru/Templates/SETCAN 2026-08-27 08:17:13 +03:00
23e37d1c4d test 2026-08-27 08:17:01 +03:00
aea2a79d2c Добавить таблицу протокола CAN и ОАП 2026-08-26 20:55:54 +03:00
bb7c94f1b7 Добавить пакеты SETTINGS для привязки датчиков 2026-08-26 20:13:06 +03:00
ce6c4327a4 Добавить документацию SETCAN 2026-08-25 18:55:43 +03:00
3cf85a278e init commit 2026-08-20 17:24:34 +03:00
108 changed files with 7299 additions and 24 deletions

27
Doxyfile Normal file
View File

@@ -0,0 +1,27 @@
PROJECT_NAME = "templates"
PROJECT_BRIEF = "Переносимые библиотеки и правила интеграции"
OUTPUT_DIRECTORY = .codex-build/doxygen
CREATE_SUBDIRS = NO
OUTPUT_LANGUAGE = Russian
INPUT = README.md NEW_PROJECT.md CONTRIBUTING.md doc/submodules.dox c python
FILE_PATTERNS = *.h *.c *.dox *.md
RECURSIVE = YES
USE_MDFILE_AS_MAINPAGE = README.md
MARKDOWN_SUPPORT = YES
AUTOLINK_SUPPORT = YES
EXTRACT_ALL = NO
EXTRACT_STATIC = NO
SOURCE_BROWSER = YES
INLINE_SOURCES = NO
STRIP_FROM_PATH = .
GENERATE_HTML = YES
HTML_OUTPUT = html
GENERATE_TREEVIEW = YES
DISABLE_INDEX = NO
GENERATE_LATEX = NO
QUIET = YES
WARNINGS = YES
WARN_IF_UNDOCUMENTED = NO
WARN_IF_DOC_ERROR = YES
WARN_AS_ERROR = FAIL_ON_WARNINGS
HAVE_DOT = NO

View File

@@ -120,3 +120,188 @@ USB gs_usb и конкретной разводки адаптера; перен
Так новый проект не является копией старого: он использует общие ядра одной
версии, а аппаратные отличия видны в одном небольшом каталоге `port/`.
## Слияние и обновление `templates` как Git submodule
### Что именно хранит основной проект
Основной проект не хранит содержимое `lib/templates` в своей истории. Вместо
этого он хранит **gitlink** — ссылку на один конкретный commit репозитория
`templates`. Поэтому при работе есть два независимых уровня истории:
1. изменения библиотек сливаются и публикуются в репозитории `templates`;
2. основной проект отдельным коммитом переводит gitlink на проверенную ревизию
`templates`.
Обычный `git merge` в основном проекте не переносит исходники между ветками
`templates` и не создаёт merge-коммит внутри сабмодуля. Он может только выбрать
одну из уже существующих ссылок на commit. Если ссылки разошлись, сначала нужно
получить общий commit в самом `templates`, а затем зафиксировать его в основном
проекте.
Текущую зафиксированную ревизию удобно смотреть из корня основного проекта:
```bash
git submodule status lib/templates
git diff --submodule=log
```
Символ `-` перед SHA в `git submodule status` означает, что сабмодуль ещё не
инициализирован; `+` — что рабочая копия сабмодуля находится не на том commit,
который записан в основном проекте; `U` — конфликт gitlink.
### Клонирование проекта и переключение его веток
Новый клон лучше сразу создавать вместе с сабмодулями:
```bash
git clone --recurse-submodules <url-основного-проекта>
cd <основной-проект>
```
Если проект уже клонирован:
```bash
git submodule update --init --recursive
```
После `git switch`, `git checkout`, `git pull` или завершения слияния основной
проект может начать ссылаться на другую ревизию `templates`. Рабочее дерево
сабмодуля следует явно привести к записанному состоянию:
```bash
git submodule update --init --recursive
```
Это нормальный и воспроизводимый режим: внутри `lib/templates` обычно будет
detached HEAD, потому что проект фиксирует commit, а не ветку. Не следует делать
там `git pull`, пока не выбрана рабочая ветка и не понятно, какую ревизию должен
получить основной проект.
### Обычное обновление проекта на новую версию `templates`
Сначала изменение должно быть проверено, закоммичено, отправлено и слито в
целевую ветку самого репозитория `templates`. После этого из корня основного
проекта выполняется:
```bash
git -C lib/templates fetch origin
git -C lib/templates switch --detach origin/master
git add lib/templates
git diff --cached --submodule=log
git commit -m "build(templates): обновить общие библиотеки"
```
Перед коммитом нужно собрать основной проект и прогнать его тесты: успешные
тесты в `templates` подтверждают работу библиотеки, но не проверяют конкретный
порт, настройки платы и интеграцию приложения.
Коммит основного проекта содержит только переход со старого SHA сабмодуля на
новый. Случайные незакоммиченные файлы внутри `lib/templates` в него не входят.
Проверить оба уровня перед фиксацией можно так:
```bash
git status
git -C lib/templates status
git diff --submodule=log
```
Не нужно обновлять сабмодуль на последний `master` автоматически при каждой
сборке. Зафиксированный SHA нужен именно для того, чтобы одна и та же версия
проекта всегда собиралась с одной и той же версией библиотек.
### Если библиотеку правят из рабочего дерева основного проекта
Нельзя коммитить изменение, оставаясь на detached HEAD: такой commit легко
потерять при следующем `git submodule update`. Сначала внутри сабмодуля создаётся
обычная ветка:
```bash
git -C lib/templates fetch origin
git -C lib/templates switch -c feat/<имя> origin/master
```
Затем изменения коммитятся и отправляются именно в репозиторий `templates`:
```bash
git -C lib/templates add <файлы>
git -C lib/templates commit -m "feat(<библиотека>): <описание>"
git -C lib/templates push -u origin feat/<имя>
```
После проверки ветка сливается в `master` репозитория `templates`. Только после
публикации итогового commit основной проект обновляет свой gitlink по процедуре
из предыдущего раздела. Не следует отправлять в общий основной проект ссылку на
commit сабмодуля, которого ещё нет на сервере: остальные разработчики и CI не
смогут его получить.
### Что происходит при слиянии веток основного проекта
Возможны три ситуации:
1. Gitlink изменён только в одной ветке. Git обычно принимает эту ревизию
автоматически. После merge нужно выполнить `git submodule update` и тесты.
2. Обе ветки указывают на разные commits, но один commit `templates` является
предком другого. Следует выбрать более новый проверенный commit, выполнить
`git add lib/templates` и продолжить merge.
3. Обе ветки указывают на расходящиеся commits `templates`. Это настоящий
конфликт истории сабмодуля: выбирать один SHA наугад нельзя, потому что так
потеряются изменения второй ветки.
При конфликте сначала смотрят, какие ссылки пришли с обеих сторон:
```bash
git ls-files -u lib/templates
git -C lib/templates fetch origin
git -C lib/templates log --oneline --graph --decorate --all
```
Затем в отдельной ветке **репозитория `templates`** объединяют оба commit,
устраняют конфликты исходников, запускают тесты и публикуют результат:
```bash
git -C lib/templates switch -c merge/<имя> <sha-одной-стороны>
git -C lib/templates merge <sha-другой-стороны>
# исправить конфликты и запустить тесты templates
git -C lib/templates add <исправленные-файлы>
git -C lib/templates commit
git -C lib/templates push -u origin merge/<имя>
```
Если общий merge-коммит уже существует в `origin/master`, создавать ещё один не
нужно: достаточно выбрать существующий SHA. После получения итогового commit
конфликт завершается на уровне основного проекта:
```bash
git -C lib/templates switch --detach <итоговый-sha>
git add lib/templates
git diff --cached --submodule=log
git merge --continue
git submodule update --init --recursive
```
`git add lib/templates` здесь отмечает разрешённым именно gitlink. Не нужно
удалять каталог, копировать библиотеку поверх него или разрешать конфликт как
обычный текстовый файл.
### Что должно попасть в merge request основного проекта
Перед отправкой результата следует проверить:
1. итоговый commit `templates` доступен в удалённом репозитории;
2. `git -C lib/templates status` не показывает локальных изменений;
3. `git submodule status` не начинается с `+`, `-` или `U`;
4. `git diff --submodule=log <целевая-ветка>...HEAD` показывает ожидаемый набор
commits библиотек;
5. тесты `templates`, сборка основного проекта и проверки на целевой плате
прошли;
6. в основном проекте закоммичены необходимые изменения адаптеров, конфигурации
и сам новый gitlink.
После получения ветки другой разработчик восстанавливает ровно выбранное
состояние одной командой:
```bash
git pull
git submodule update --init --recursive
```

View File

@@ -48,6 +48,10 @@ templates/
### Python
Экспериментальный [`c/gas-logger`](c/gas-logger/README.md): непрерывная запись
в два банка RAM, снимки через GAS/Modbus/ProtoCAN, JSON-карта имён и адресов,
проектный порт ПМ35 и MCU-порты C28x / STM32. Клиент в `set_devices.gas_logger`.
| Модуль | Что делает | Зависимости |
|---|---|---|
| [`python/setprotocol/firmware_database.py`](python/setprotocol/firmware_database.py) | база прошивок: HTTPS-каталог, скачивание с SHA-256 и кэшем, публикация в Gitea без SETGUI; [подключение и CLI](tools/firmware-publish/DATABASE.md) | stdlib, Python 3.10+ |
@@ -72,6 +76,11 @@ templates/
В ней отдельно описаны граница ядра, ABI, память, три wire format и состояние
портов Windows, Android, Linux и MCU.
Обзор всего репозитория, включая слияние при подключении через submodule:
[`doc/index.html`](doc/index.html). Справочник Doxygen собирается командой
`powershell -ExecutionPolicy Bypass -File doc/build-doxygen.ps1` в
`.codex-build/doxygen/html/index.html`.
## Как подключить к проекту
**Сабмодуль** — когда нужна одна конкретная версия и обновление по команде:
@@ -81,6 +90,10 @@ git submodule add https://git.rd12.ru/Andrey/templates.git lib/templates
git submodule update --init --recursive
```
Как обновлять зафиксированную ревизию, переносить изменения из проекта и
разрешать конфликт gitlink при слиянии веток, подробно описано в разделе
[`Слияние и обновление templates как Git submodule`](NEW_PROJECT.md#слияние-и-обновление-templates-как-git-submodule).
Дальше в сборку добавляются только нужные каталоги:
```

View File

@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.16)
project(gas_logger C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_library(gas_logger src/gas_logger.c)
target_include_directories(gas_logger PUBLIC include)
add_library(gas_logger_pm35 ports/pm35/gas_logger_pm35.c)
target_include_directories(gas_logger_pm35 PUBLIC ports/pm35)
target_link_libraries(gas_logger_pm35 PUBLIC gas_logger)
add_library(gas_logger_pcan src/gas_logger_pcan.c)
target_include_directories(gas_logger_pcan PUBLIC ../set-protocol/include)
target_link_libraries(gas_logger_pcan PUBLIC gas_logger)
add_executable(test_gas_logger tests/test_gas_logger.c)
target_link_libraries(test_gas_logger PRIVATE gas_logger)
enable_testing()
add_test(NAME gas_logger_core COMMAND test_gas_logger)

177
c/gas-logger/README.md Normal file
View File

@@ -0,0 +1,177 @@
# GAS Logger — экспериментальная версия 1
Непрерывный регистратор с двумя кольцевыми банками, неизменяемым снимком и
общей картой GAS / ProtoCAN GAS / Modbus FC03/FC06. Весь код и проектные/MCU
адаптеры находятся в `templates`; приложения подключают ревизию сабмодулем.
Ядро C99 не содержит HAL, malloc, глобальных переменных, драйвера связи или
прерываний. Хранение — RAM, после отключения питания данные исчезают.
Приложение → проектный источник сигналов → `gas_logger` → MCU-порт блокировки.
GAS/Modbus/CAN диспетчеры → тот же экземпляр `gas_logger`.
## Состав
| Файл | Назначение / зависимости |
|---|---|
| `include/gas_logger.h`, `src/gas_logger.c` | переносимое ядро, только стандартный C99 |
| `ports/pm35/` | источники ПМ35 и кэш напряжений ПМ67, сгенерированная карта |
| `ports/c28x/` | сохранение/восстановление IRQ для TI F2812/F28335 |
| `ports/stm32/` | PRIMASK для STM32 F1/F4/G4, CMSIS проекта |
| `src/gas_logger_pcan.c` | регион сервиса `pcan_gas`, только MCU с поддержкой `uint8_t` |
| `../../python/set_devices/gas_logger.py` | валидация карты, генератор C, клиент снимков; stdlib |
| `../../python/set_devices/gas_logger_maps/pm35.json` | имена, адреса GAS/CAN/Modbus, источники и регистры сервиса |
| `tests/` | ядро и сквозной тест клиента с настоящим C-регистратором |
## JSON и адреса
JSON — исходное описание. `ports/pm35/pm35_map.h` — автоматически созданная
копия для прошивки. `tools/generate_map.py --check` проверяет их соответствие.
Изменения выполняются в JSON, затем запускается генератор. CRC32 канонического
JSON публикуется платой: клиент откажется читать/записывать при несовпадении
карты. Это идентификатор совместимости, не механизм аутентификации.
Все адреса — **нулевые индексы 16-битных регистров**, без префикса 4xxxx.
В поле `can.address` указан MsgBody для **ProtoCAN GAS**, а не полный CAN ID.
DeviceType/DeviceID/Route выбирает транспорт проекта. Идентификаторы legacy
CAN ПМ35 и ПМ67 не переименовываются в GAS: проект сначала обновляет их кэш,
после чего регистратор читает его через `source`.
Экспериментальный профиль ПМ35:
| GAS | Modbus FC03 | ProtoCAN GAS | Имя | Источник |
|---|---|---|---|---|
| 0x5000 | 0x5000 | 0x5000 | Команды УМП | modbus[127] |
| 0x5001 | 0x5001 | 0x5001 | Ток, 0.1 мА | modbus[28] |
| 0x5002 | 0x5002 | 0x5002 | Код ЦАП | modbus[123] |
| 0x5003 | 0x5003 | 0x5003 | Начальная уставка | modbus[80] |
| 0x5004 | 0x5004 | 0x5004 | Конечная уставка | modbus[81] |
| 0x5005 | 0x5005 | 0x5005 | ЗПТ1 | pm67_voltage[0], ранее принятый CAN адрес 305 |
| 0x5006 | 0x5006 | 0x5006 | ЗПТ2 | pm67_voltage[1], ранее принятый CAN адрес 8 |
Адреса 0x5000/0x6000 — выделение **для тестового профиля**, не утверждение о
поддержке этих окон существующей прошивкой. Перед включением в другую карту
проект обязан проверить отсутствие пересечений со своими регионами.
## Карта сервиса
База одинакова для всех трёх транспортов: `0x6000`; имена и смещения также
перечислены в JSON. Многословные числа: сначала младшее 16-битное слово.
Endian байтов задаёт транспорт: Modbus BE, ProtoCAN GAS LE.
| Смещение | Доступ | Значение |
|---|---|---|
| 0, 1 | R | сигнатура 0x474C, версия 1 |
| 2 | R | бит 0 запись, бит 1 снимок закреплён |
| 3..5 | R | каналов, слов в записи, ёмкость банка |
| 6, 7 | R | записей в текущем банке / снимке |
| 8..9 | R | поколение снимка u32 |
| 10..11 | R | CRC32 JSON-карты |
| 12..13 | R | следующий номер записи u32 |
| 14..15 | R | пропущенные снимки: запрос во время закреплённого снимка |
| 16..17 | R | ошибки источников при записи |
| 18 | W | 1 START, 2 SNAPSHOT, 3 RELEASE |
| 19..20 | W | поколение, разрешённое к освобождению |
| 0x100.. | R | записи снимка по прямому адресу, без команд курсора |
Запись: `time_ms:u32, sequence:u32, event:u16, channels[N]:u16`.
В профиле ПМ35 — 12 слов, 760 записей на банк, всего 18240 слов RAM для двух
банков. Блочное чтение разрешено по любой границе, максимум 125 слов.
Клиент собирает записи из блоков, в том числе неполных по размеру записи.
Классический CAN использует до 4 слов в кадре. Для UART подходит 120/125 слов.
Для 760 записей этого профиля требуется 76 чтений по 120 слов без записи
курсора; состав полей отличается от старого 42-словного УМП, поэтому это не
прямое измерение ускорения прежнего лога.
## Непрерывная запись и снимки
`START` идемпотентен: включает запись, не очищает историю. STOP-команды нет.
`SNAPSHOT` кратко блокирует IRQ, переключает активный банк и закрепляет старый.
Во время загрузки новые записи продолжают поступать в другой кольцевой банк.
Снимок не перезаписывается, пока клиент не передаст совпадающее поколение и
`RELEASE`. Одновременные управляющие клиенты не поддерживаются.
Повторный SNAPSHOT при занятом снимке возвращает BUSY и увеличивает счётчик.
Пустой банк возвращает EMPTY. Долгий обрыв связи не останавливает запись, но
закреплённый снимок остаётся занят: нужно явно продолжить чтение (`resume=True`)
либо освободить его. Автоматического таймаута освобождения нет.
После переключения новый банк начинает историю с нуля; непрерывность означает
продолжение сбора, а не дублирование предыстории между снимками.
Триггер/постинтервал определяет проект. Например, ПМ35 вызывает `gl_snapshot()`
по завершении своей секунды после отключения, а не при начале события.
Этот модуль не заменяет автоматически старую логику УМП.
## Контракт порта
```c
uint32_t enter(void *user); /* сохранить и запретить IRQ */
void leave(void *user, uint32_t previous); /* восстановить ровно previous */
int source(void *user, uint16_t gas, uint16_t *value);
```
`source` выполняется под блокировкой: только быстрые чтения RAM. Ни запросов
CAN/Modbus, ни ожиданий периферии. Ошибка любого канала отбрасывает всю запись.
Кэш CAN обновляется под той же блокировкой. Для RTOS/многоядерных платформ
нужен порт с подходящей межпоточной синхронизацией; PRIMASK — для одного ядра.
На стеке блочного чтения до 125 слов; при захвате до 64 слов каналов.
Массивы банков не должны перекрываться друг с другом и контекстом.
## Подключение ПМ35 / F28335
```c
#include "gas_logger_pm35.h"
#include "gas_logger_c28x.h"
static gas_logger logger;
static uint16_t bank0[9120], bank1[9120]; /* разместить в RAM по linker map */
static gl_pm35_context source;
void logger_init(volatile uint16_t *modbus, volatile uint16_t *pm67) {
source.modbus = modbus; source.pm67_voltage = pm67;
source.enter = gl_c28x_enter; source.leave = gl_c28x_leave;
source.irq_user = 0;
if (gl_pm35_init(&logger, &source, bank0, bank1, 9120) == GL_OK)
gl_write(&logger, 0x6012, GL_START);
}
/* Из существующего таймера: gl_capture(&logger, time_ms, event); */
```
Включить `src/gas_logger.c`, `ports/pm35/gas_logger_pm35.c` и
`ports/c28x/gas_logger_c28x.c`. Include-пути — `include`, `ports/pm35`,
`ports/c28x`. F2812/F28335 используют одни intrinsics TI.
Для STM32 подключить `ports/stm32/gas_logger_stm32.c`, выбрать
`GL_STM32_DEVICE_HEADER` (`stm32f1xx.h`/`stm32f4xx.h`/`stm32g4xx.h`) и передать
`gl_stm32_enter/leave`. Проектный PM35-адаптер применим к STM-эмулятору только
если его массивы соответствуют указанному контракту.
FC03 диспетчер вызывает `gl_modbus_read()`, FC06 — `gl_modbus_write()`.
GL_ADDRESS → exception 2, GL_ARGUMENT → 3, GL_BUSY → 6, остальные ошибки → 4.
Ответ на запись отправляется лишь после GL_OK. CRC/кадры остаются в транспорте.
Для `pcan_gas` добавляется регион `gl_pcan_region_init()`; сигнальные адреса
маршрутизируются отдельно в `gl_can_read()` (или уже существующие GAS-регионы).
Стандартные GAS-записи CAN не имеют ACK: CAN-адаптер клиента должен подтвердить
результат чтением статуса; просто отправить CAN-кадр недостаточно.
Передача UART/CAN выполняется **после** снятия IRQ-блокировки.
## Клиент и проверка
```python
from set_devices.gas_logger import GasLoggerClient
client = GasLoggerClient(read_registers, write_register, block_words=120)
client.start() # после проверки сигнатуры и JSON CRC
records = client.download() # фиксирует, читает, проверяет, освобождает
```
Вызовы транспорта синхронные, подтверждённые; для GUI запускать в его рабочем
потоке. Исключение/отмена оставляет снимок закреплённым для явного продолжения.
Записи с таймаутом автоматически не повторяются.
```powershell
python c/gas-logger/tools/generate_map.py --check
python c/gas-logger/tests/run_tests.py --cc C:/msys64/mingw64/bin/gcc.exe
```
Либо `cmake -S c/gas-logger -B <build>`, сборка и `ctest --test-dir <build>`.
Ветка `codex/test-gas-logger`: потребитель SETGUI импортирует модуль из
`third_party/templates` и включает JSON в пакет. Производственные прошивки
ПМ35/166 пока не переключены; адреса не включены в их текущие диспетчеры.
Нужны подключение к таймеру/карте конкретной платы, проверка linker RAM и
испытание на устройстве. Проверки на хосте не измеряют аппаратные тайминги.

View File

@@ -0,0 +1,70 @@
/* Portable continuous recorder. All addresses/counts are 16-bit WORDS,
* including on C28x (CHAR_BIT=16). No heap, UART, HAL or global state. */
#ifndef GAS_LOGGER_H
#define GAS_LOGGER_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define GL_MAGIC 0x474CU
#define GL_VERSION 1U
#define GL_HEADER_WORDS 5U /* time_ms:u32, sequence:u32, event:u16, values... */
#define GL_DATA_OFFSET 0x100U
#define GL_MAX_CHANNELS 64U
#define GL_COMMAND 18U
#define GL_RELEASE_LO 19U
#define GL_RELEASE_HI 20U
#define GL_START 1U
#define GL_SNAPSHOT 2U
#define GL_RELEASE 3U
typedef enum { GL_OK=0, GL_ADDRESS, GL_ARGUMENT, GL_BUSY, GL_EMPTY,
GL_STALE, GL_SOURCE_ERROR } gl_result;
typedef struct {
uint16_t gas, modbus, can;
} gl_channel;
typedef struct {
uint16_t base, channels, capacity;
uint32_t schema_id;
const gl_channel *map;
} gl_config;
/* enter returns the previous IRQ state; leave MUST restore it, not enable
* IRQs unconditionally. source executes under this lock: bounded RAM reads
* only, never I/O/waits. Cache remote CAN values before sampling. */
typedef struct {
void *user;
uint32_t (*enter)(void *user);
void (*leave)(void *user, uint32_t state);
int (*source)(void *user, uint16_t gas, uint16_t *value);
} gl_port;
typedef struct {
gl_config config;
gl_port port;
uint16_t *banks[2];
uint16_t active, count, next, recording, pinned, snap_count, snap_next;
uint32_t generation, release_generation, sequence, missed, source_errors;
} gas_logger;
/* Each distinct bank must have >=capacity*(channels+5) uint16_t elements.
* All callbacks and a nonoverlapping register map are required. */
gl_result gl_init(gas_logger *log, const gl_config *config, const gl_port *port,
uint16_t *bank0, uint16_t *bank1, uint32_t bank_words);
/* Called by the project timer/event hook. A failed source discards the whole
* sample. Sequence/time remain uint32, wrapping modulo 2^32. */
gl_result gl_capture(gas_logger *log, uint32_t time_ms, uint16_t event);
/* Snapshot swaps banks, pins the old one, and NEVER stops acquisition.
* Another snapshot while pinned returns BUSY and increments missed. */
gl_result gl_snapshot(gas_logger *log);
/* Atomic, all-or-nothing block read. Snapshot records are chronological;
* reads past snap_count are rejected, not zero-filled. */
gl_result gl_read(gas_logger *log, uint16_t address, uint16_t *out, uint16_t count);
/* No STOP/reset command. START is idempotent. RELEASE requires the generation
* staged in base+19/20. One controlling client per device. */
gl_result gl_write(gas_logger *log, uint16_t address, uint16_t value);
/* Register aliases for FC03/06 and ProtoCAN GAS. Transports keep their own
* framing/CRC; addresses are zero-based and words preserve their values. */
gl_result gl_modbus_read(gas_logger *log, uint16_t address, uint16_t *out, uint16_t count);
gl_result gl_modbus_write(gas_logger *log, uint16_t address, uint16_t value);
gl_result gl_can_read(gas_logger *log, uint16_t address, uint16_t *out, uint16_t count);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,8 @@
#ifndef GAS_LOGGER_PCAN_H
#define GAS_LOGGER_PCAN_H
#include "gas_logger.h"
#include "pcan_gas.h"
/* Service region; signal aliases may be registered separately in the project's
* GAS dispatcher. CAN callbacks use the logger's alias map. */
void gl_pcan_region_init(pcan_gas_region_t *region,gas_logger *log);
#endif

View File

@@ -0,0 +1,4 @@
#include "gas_logger_c28x.h"
/* __disable_interrupts/__restore_interrupts are TI C28x compiler intrinsics. */
uint32_t gl_c28x_enter(void *user) { (void)user; return __disable_interrupts(); }
void gl_c28x_leave(void *user,uint32_t state) { (void)user; __restore_interrupts((unsigned int)state); }

View File

@@ -0,0 +1,7 @@
#ifndef GAS_LOGGER_C28X_H
#define GAS_LOGGER_C28X_H
#include <stdint.h>
/* TI C2000 compiler; F2812/F28335. Supply these as gl_port lock callbacks. */
uint32_t gl_c28x_enter(void *user);
void gl_c28x_leave(void *user,uint32_t state);
#endif

View File

@@ -0,0 +1,22 @@
#include "gas_logger_pm35.h"
#include "pm35_map.h"
static uint32_t enter(void *u) { gl_pm35_context *p=u; return p->enter(p->irq_user); }
static void leave(void *u,uint32_t state) { gl_pm35_context *p=u; p->leave(p->irq_user,state); }
static int source(void *u,uint16_t gas,uint16_t *v)
{
gl_pm35_context *p=u;
uint16_t i;
for(i=0;i<gl_pm35_config.channels;i++) if(gl_pm35_channels[i].gas==gas) {
*v=gl_pm35_source_space[i] ? p->pm67_voltage[gl_pm35_source_address[i]] :
p->modbus[gl_pm35_source_address[i]];
return 1;
}
return 0;
}
gl_result gl_pm35_init(gas_logger *l,gl_pm35_context *p,uint16_t *a,uint16_t *b,uint32_t n)
{
gl_port port;
if(!p || !p->modbus || !p->pm67_voltage || !p->enter || !p->leave) return GL_ARGUMENT;
port.user=p;port.enter=enter;port.leave=leave;port.source=source;
return gl_init(l,&gl_pm35_config,&port,a,b,n);
}

View File

@@ -0,0 +1,16 @@
#ifndef GAS_LOGGER_PM35_H
#define GAS_LOGGER_PM35_H
#include "gas_logger.h"
/* Project adapter: pm35_modbus uses the firmware's zero-based modbus[128].
* pm67_voltage[2] is the latest CAN cache, updated under the same IRQ lock.
* No direct CAN requests are performed while capturing. */
typedef struct {
volatile uint16_t *modbus;
volatile uint16_t *pm67_voltage;
uint32_t (*enter)(void *);
void (*leave)(void *,uint32_t);
void *irq_user;
} gl_pm35_context;
gl_result gl_pm35_init(gas_logger *log, gl_pm35_context *context,
uint16_t *bank0, uint16_t *bank1, uint32_t bank_words);
#endif

View File

@@ -0,0 +1,19 @@
/* Generated from pm35.json. Do not edit. */
#ifndef GL_PM35_MAP_H
#define GL_PM35_MAP_H
#include "gas_logger.h"
static const gl_channel gl_pm35_channels[] = {
{20480U, 20480U, 20480U}, /* commands */
{20481U, 20481U, 20481U}, /* current */
{20482U, 20482U, 20482U}, /* dac */
{20483U, 20483U, 20483U}, /* go */
{20484U, 20484U, 20484U}, /* stop */
{20485U, 20485U, 20485U}, /* zpt1 */
{20486U, 20486U, 20486U}, /* zpt2 */
};
static const uint16_t gl_pm35_source_space[] = { 0U, 0U, 0U, 0U, 0U, 1U, 1U };
static const uint16_t gl_pm35_source_address[] = { 127U, 28U, 123U, 80U, 81U, 0U, 1U };
static const gl_config gl_pm35_config = {
24576U, 7U, 760U, 0x635F84CBUL, gl_pm35_channels
};
#endif

View File

@@ -0,0 +1,8 @@
#include "gas_logger_stm32.h"
#ifndef GL_STM32_DEVICE_HEADER
#define GL_STM32_DEVICE_HEADER "stm32f4xx.h"
#endif
#include GL_STM32_DEVICE_HEADER
uint32_t gl_stm32_enter(void *user)
{ uint32_t saved; (void)user; saved=__get_PRIMASK(); __disable_irq(); return saved; }
void gl_stm32_leave(void *user,uint32_t state) { (void)user; __set_PRIMASK(state); }

View File

@@ -0,0 +1,8 @@
#ifndef GAS_LOGGER_STM32_H
#define GAS_LOGGER_STM32_H
#include <stdint.h>
/* Define GL_STM32_DEVICE_HEADER to the project's CMSIS device header.
* F1/F4/G4 use the same PRIMASK contract. No HAL/interrupt handler ownership. */
uint32_t gl_stm32_enter(void *user);
void gl_stm32_leave(void *user,uint32_t state);
#endif

View File

@@ -0,0 +1,161 @@
#include "gas_logger.h"
#include <stddef.h>
#include <string.h>
static uint16_t width(const gas_logger *l) { return l->config.channels + GL_HEADER_WORDS; }
static int service(const gl_config *c, uint16_t a)
{
uint32_t end = (uint32_t)c->base + GL_DATA_OFFSET +
(uint32_t)c->capacity * (c->channels + GL_HEADER_WORDS);
return a >= c->base && (uint32_t)a < end;
}
gl_result gl_init(gas_logger *l, const gl_config *c, const gl_port *p,
uint16_t *b0, uint16_t *b1, uint32_t words)
{
uint16_t i,j;
uint32_t needed;
if (!l || !c || !p || !b0 || !b1 || b0 == b1 || !c->map ||
!p->enter || !p->leave || !p->source || !c->capacity ||
!c->channels || c->channels > GL_MAX_CHANNELS) return GL_ARGUMENT;
needed = (uint32_t)c->capacity * (c->channels + GL_HEADER_WORDS);
if (needed > words || (uint32_t)c->base + GL_DATA_OFFSET + needed > 65536UL)
return GL_ARGUMENT;
for (i=0;i<c->channels;i++) {
if (service(c,c->map[i].gas) || service(c,c->map[i].modbus) ||
service(c,c->map[i].can)) return GL_ARGUMENT;
for (j=0;j<i;j++)
if (c->map[i].gas == c->map[j].gas ||
c->map[i].modbus == c->map[j].modbus ||
c->map[i].can == c->map[j].can) return GL_ARGUMENT;
}
memset(l,0,sizeof(*l)); l->config=*c; l->port=*p; l->banks[0]=b0; l->banks[1]=b1;
return GL_OK;
}
gl_result gl_capture(gas_logger *l, uint32_t time, uint16_t event)
{
uint16_t values[GL_MAX_CHANNELS],i,*record;
uint32_t irq = l->port.enter(l->port.user);
if (!l->recording) { l->port.leave(l->port.user,irq); return GL_BUSY; }
for (i=0;i<l->config.channels;i++) {
if (!l->port.source(l->port.user,l->config.map[i].gas,&values[i])) {
l->source_errors++; l->port.leave(l->port.user,irq); return GL_SOURCE_ERROR;
}
}
record=l->banks[l->active]+(uint32_t)l->next*width(l);
record[0]=(uint16_t)time; record[1]=(uint16_t)(time>>16);
record[2]=(uint16_t)l->sequence; record[3]=(uint16_t)(l->sequence>>16);
record[4]=event;
for (i=0;i<l->config.channels;i++) record[GL_HEADER_WORDS+i]=values[i];
l->sequence++;
if (++l->next == l->config.capacity) l->next=0;
if (l->count < l->config.capacity) l->count++;
l->port.leave(l->port.user,irq); return GL_OK;
}
static gl_result snapshot(gas_logger *l)
{
if (l->pinned) { l->missed++; return GL_BUSY; }
if (!l->count) return GL_EMPTY;
l->snap_count=l->count; l->snap_next=l->next;
l->active^=1U; l->count=0; l->next=0; l->pinned=1;
l->generation++; if (!l->generation) l->generation=1;
return GL_OK;
}
gl_result gl_snapshot(gas_logger *l)
{
uint32_t irq=l->port.enter(l->port.user);
gl_result r=snapshot(l); l->port.leave(l->port.user,irq); return r;
}
static uint16_t status(const gas_logger *l, uint16_t offset)
{
switch(offset) {
case 0:return GL_MAGIC; case 1:return GL_VERSION;
case 2:return l->recording | (l->pinned<<1);
case 3:return l->config.channels; case 4:return width(l);
case 5:return l->config.capacity; case 6:return l->count; case 7:return l->snap_count;
case 8:return (uint16_t)l->generation; case 9:return (uint16_t)(l->generation>>16);
case 10:return (uint16_t)l->config.schema_id; case 11:return (uint16_t)(l->config.schema_id>>16);
case 12:return (uint16_t)l->sequence; case 13:return (uint16_t)(l->sequence>>16);
case 14:return (uint16_t)l->missed; case 15:return (uint16_t)(l->missed>>16);
case 16:return (uint16_t)l->source_errors; case 17:return (uint16_t)(l->source_errors>>16);
default:return 0;
}
}
static gl_result read_locked(gas_logger *l, uint16_t a, uint16_t *out, uint16_t n)
{
uint16_t i,j,slot;
uint32_t off;
if (a>=l->config.base && (uint32_t)a+n <= (uint32_t)l->config.base+18) {
for(i=0;i<n;i++) out[i]=status(l,(uint16_t)(a+i-l->config.base));
return GL_OK;
}
if ((uint32_t)a >= (uint32_t)l->config.base+GL_DATA_OFFSET) {
off=(uint32_t)a-l->config.base-GL_DATA_OFFSET;
if (l->pinned && off+n <= (uint32_t)l->snap_count*width(l)) {
for(i=0;i<n;i++) {
slot=(uint16_t)((off+i)/width(l));
if(l->snap_count==l->config.capacity)
slot=(uint16_t)(((uint32_t)slot+l->snap_next)%l->config.capacity);
out[i]=l->banks[l->active^1U][(uint32_t)slot*width(l)+(off+i)%width(l)];
}
return GL_OK;
}
}
/* A live signal block must be completely mapped before any read. */
for(i=0;i<n;i++) {
for(j=0;j<l->config.channels;j++) if(l->config.map[j].gas==a+i) break;
if(j==l->config.channels) return GL_ADDRESS;
}
for(i=0;i<n;i++)
if(!l->port.source(l->port.user,(uint16_t)(a+i),&out[i])) return GL_SOURCE_ERROR;
return GL_OK;
}
gl_result gl_read(gas_logger *l, uint16_t a, uint16_t *out, uint16_t n)
{
uint16_t temp[125],i;
uint32_t irq;
gl_result r;
if(!out || !n || n>125 || (uint32_t)a+n>65536UL) return GL_ARGUMENT;
irq=l->port.enter(l->port.user); r=read_locked(l,a,temp,n);
if(r==GL_OK) for(i=0;i<n;i++) out[i]=temp[i];
l->port.leave(l->port.user,irq); return r;
}
gl_result gl_write(gas_logger *l, uint16_t a, uint16_t v)
{
uint32_t irq=l->port.enter(l->port.user);
gl_result r=GL_OK;
if((uint32_t)a==(uint32_t)l->config.base+GL_RELEASE_LO)
l->release_generation=(l->release_generation&0xFFFF0000UL)|v;
else if((uint32_t)a==(uint32_t)l->config.base+GL_RELEASE_HI)
l->release_generation=(l->release_generation&0xFFFFUL)|((uint32_t)v<<16);
else if((uint32_t)a!=(uint32_t)l->config.base+GL_COMMAND) r=GL_ADDRESS;
else if(v==GL_START) l->recording=1;
else if(v==GL_SNAPSHOT) r=snapshot(l);
else if(v==GL_RELEASE) {
if(!l->pinned || l->release_generation!=l->generation) r=GL_STALE;
else { l->pinned=0; l->snap_count=0; }
} else r=GL_ARGUMENT;
l->port.leave(l->port.user,irq); return r;
}
static gl_result alias_read(gas_logger *l,uint16_t a,uint16_t *out,uint16_t n,int can)
{
uint16_t i,j,gas=0,found;
if(!n || n>125 || (uint32_t)a+n>65536UL) return GL_ARGUMENT;
if(service(&l->config,a)) return gl_read(l,a,out,n);
for(i=0;i<n;i++) {
found=0;
for(j=0;j<l->config.channels;j++)
if((can?l->config.map[j].can:l->config.map[j].modbus)==a+i) {
if(!i) gas=l->config.map[j].gas;
if((uint32_t)gas+i!=l->config.map[j].gas) return GL_ADDRESS;
found=1;break;
}
if(!found) return GL_ADDRESS;
}
return gl_read(l,gas,out,n);
}
gl_result gl_modbus_read(gas_logger *l,uint16_t a,uint16_t *o,uint16_t n)
{ return alias_read(l,a,o,n,0); }
gl_result gl_can_read(gas_logger *l,uint16_t a,uint16_t *o,uint16_t n)
{ return alias_read(l,a,o,n,1); }
gl_result gl_modbus_write(gas_logger *l,uint16_t a,uint16_t v)
{ return gl_write(l,a,v); }

View File

@@ -0,0 +1,13 @@
#include "gas_logger_pcan.h"
static pcan_gas_status_t result(gl_result r)
{ return r==GL_OK ? PCAN_GAS_OK : r==GL_ADDRESS ? PCAN_GAS_NO_REG : PCAN_GAS_REJECTED; }
static pcan_gas_status_t read_word(const pcan_gas_region_t *r,uint16_t o,uint16_t *v)
{ return result(gl_can_read(r->user,(uint16_t)(r->base+o),v,1)); }
static pcan_gas_status_t write_word(const pcan_gas_region_t *r,uint16_t o,uint16_t v)
{ return result(gl_write(r->user,(uint16_t)(r->base+o),v)); }
void gl_pcan_region_init(pcan_gas_region_t *r,gas_logger *l)
{
r->base=l->config.base;
r->count=(uint16_t)(GL_DATA_OFFSET+(uint32_t)l->config.capacity*(l->config.channels+GL_HEADER_WORDS));
r->storage=0;r->read=read_word;r->write=write_word;r->flags=0;r->user=l;r->name="gas-logger";
}

View File

@@ -0,0 +1,22 @@
/* Host-only firmware harness for the actual PM35 port and Python client. */
#include "gas_logger_pm35.h"
#include "gas_logger_pcan.h"
#include <assert.h>
static gas_logger log;
static uint16_t bank0[9120],bank1[9120],modbus[128],voltage[2];
static uint32_t irq;
static uint32_t enter(void *u) { uint32_t old=irq;(void)u;irq=1;return old; }
static void leave(void *u,uint32_t state) { (void)u;assert(irq==1);irq=state; }
static gl_pm35_context context={modbus,voltage,enter,leave,0};
int host_init(void) { irq=0;return gl_pm35_init(&log,&context,bank0,bank1,9120); }
int host_capture(uint32_t time,uint16_t value)
{ modbus[28]=value;modbus[127]=128;voltage[0]=1234;voltage[1]=5678;return gl_capture(&log,time,1); }
int host_read(uint16_t address,uint16_t *out,uint16_t count)
{ return gl_modbus_read(&log,address,out,count); }
int host_write(uint16_t address,uint16_t value) { return gl_modbus_write(&log,address,value); }
int host_pcan_read(uint16_t address,uint16_t *out)
{
pcan_gas_region_t region;gl_pcan_region_init(&region,&log);
if(address<region.base || (uint32_t)address>=region.base+region.count) return GL_ADDRESS;
return region.read(&region,(uint16_t)(address-region.base),out);
}

View File

@@ -0,0 +1,46 @@
"""Build in a temporary directory; run core + Python/C end-to-end tests."""
import argparse
import ctypes
import importlib.util
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
parser=argparse.ArgumentParser()
parser.add_argument('--cc',default=shutil.which('gcc'))
args=parser.parse_args()
if not args.cc:raise SystemExit('Specify --cc /path/to/gcc')
root=Path(__file__).resolve().parents[1]
templates=root.parents[1]
sys.path.insert(0,str(templates/'python'))
env=dict(os.environ)
env['PATH']=str(Path(args.cc).resolve().parent)+os.pathsep+env.get('PATH','')
flags=[args.cc,'-std=c99','-Wall','-Wextra','-Werror','-pedantic','-I'+str(root/'include')]
with tempfile.TemporaryDirectory(prefix='gas-logger-') as directory:
binary=Path(directory)/'core.exe'
subprocess.run(flags+[str(root/'src/gas_logger.c'),str(root/'tests/test_gas_logger.c'),'-o',str(binary)],check=True,env=env)
subprocess.run([str(binary)],check=True,env=env)
lib=Path(directory)/('device.dll' if os.name=='nt' else 'device.so')
subprocess.run(flags+([] if os.name=='nt' else ['-fPIC'])+['-shared','-I'+str(root/'ports/pm35'),
'-I'+str(templates/'c/set-protocol/include'),
str(root/'src/gas_logger.c'),str(root/'src/gas_logger_pcan.c'),
str(root/'ports/pm35/gas_logger_pm35.c'),str(root/'tests/host_device.c'),
'-o',str(lib)],check=True,env=env)
# Child releases the loaded DLL before TemporaryDirectory cleanup on Windows.
runner='''import ctypes,importlib.util,sys,unittest
spec=importlib.util.spec_from_file_location('test_client',sys.argv[1])
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
m.DEVICE=ctypes.CDLL(sys.argv[2])
m.DEVICE.host_capture.argtypes=[ctypes.c_uint32,ctypes.c_uint16]
m.DEVICE.host_read.argtypes=[ctypes.c_uint16,ctypes.POINTER(ctypes.c_uint16),ctypes.c_uint16]
m.DEVICE.host_write.argtypes=[ctypes.c_uint16,ctypes.c_uint16]
m.DEVICE.host_pcan_read.argtypes=[ctypes.c_uint16,ctypes.POINTER(ctypes.c_uint16)]
r=unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromModule(m))
sys.exit(not r.wasSuccessful())'''
env['PYTHONPATH']=str(templates/'python')
subprocess.run([sys.executable,'-c',runner,str(root/'tests/test_client.py'),str(lib)],check=True,env=env)
subprocess.run([sys.executable,str(root/'tools/generate_map.py'),'--check'],check=True,env=env)

View File

@@ -0,0 +1,111 @@
"""Integration: Python client -> register callbacks -> compiled C PM35 logger."""
import copy
import ctypes
import unittest
from set_devices.gas_logger import GasLoggerClient, load_map, validate_map
DEVICE = None # set by run_tests.py before loading the suite
class ClientTests(unittest.TestCase):
def setUp(self):
self.lib=DEVICE
self.assertEqual(self.lib.host_init(),0)
self.client=GasLoggerClient(self.read,self.write)
self.client.start()
for i in range(800):
self.assertEqual(self.lib.host_capture(i,i),0)
def read(self,address,count):
out=(ctypes.c_uint16*count)()
result=self.lib.host_read(address,out,count)
if result: raise RuntimeError('read '+str(result))
return list(out)
def write(self,address,value):
result=self.lib.host_write(address,value)
if result: raise RuntimeError('write '+str(result))
def test_download_keeps_recording_and_order(self):
def progress(done,total):
for i in range(20): self.assertEqual(self.lib.host_capture(900+done+i,99),0)
records=self.client.download(progress=progress)
self.assertEqual([r['time_ms'] for r in records],list(range(40,800)))
self.assertEqual([r['current'] for r in records],list(range(40,800)))
self.assertTrue(all(r['zpt2']==5678 for r in records))
self.assertEqual(self.client.status()[2],1)
self.assertGreater(self.client.status()[6],0)
def test_cancel_retains_snapshot_and_explicit_resume(self):
cancel=[False]
def progress(done,total): cancel[0]=True
with self.assertRaises(InterruptedError):
self.client.download(cancelled=lambda:cancel[0],progress=progress)
self.assertEqual(self.client.status()[2],3)
with self.assertRaises(RuntimeError): self.client.download()
self.assertEqual(len(self.client.download(resume=True)),760)
def test_partial_reply_leaves_pin(self):
original=self.client.read
self.client.read=lambda a,n: original(a,n)[:-1] if a>=0x6100 else original(a,n)
with self.assertRaises(ValueError): self.client.download()
self.client.read=original
self.assertEqual(self.client.status()[2],3)
def test_no_writes_for_wrong_schema(self):
m=load_map();m['channels'][0]['name']='Other'
writes=[]
client=GasLoggerClient(self.read,lambda *a:writes.append(a),m)
with self.assertRaises(ValueError): client.download()
self.assertEqual(writes,[])
def test_generation_mismatch_does_not_release(self):
self.write(0x6012,2)
with self.assertRaises(ValueError):self.client.release(999)
self.assertEqual(self.client.status()[2],3)
def test_classic_can_chunks_and_pcan_region(self):
def read(a,n):
words=[]
for i in range(n):
out=ctypes.c_uint16()
self.assertEqual(self.lib.host_pcan_read(a+i,ctypes.byref(out)),0)
words.append(out.value)
return words
client=GasLoggerClient(read,self.write,block_words=4)
self.assertEqual(len(client.download()),760)
def test_json_rejects_collisions_and_drift(self):
base=load_map()
for change in (
lambda m:m['channels'][0].update(gas=0x6000),
lambda m:m['channels'][1].update(gas=m['channels'][0]['gas']),
lambda m:m['service'].update(data_offset=512),
lambda m:m['service']['registers'][0].update(key='wrong'),
lambda m:m.update(capacity=65535),
):
m=copy.deepcopy(base);change(m)
with self.assertRaises(ValueError):validate_map(m)
def test_replaced_snapshot_is_rejected_before_release(self):
changed=[False]
def progress(done,total):
if changed[0]:return
changed[0]=True
self.client.release(1)
self.assertEqual(self.lib.host_capture(2000,999),0)
self.write(0x6012,2)
# Simulates a second, unsupported controlling client. We must never
# accept a mixture of generations or release the newer snapshot.
with self.assertRaises((ValueError,RuntimeError)):
self.client.download(progress=progress)
status=self.client.status()
self.assertEqual(status[2],3)
self.assertEqual(status[8],2)
def test_start_does_not_reset_history(self):
before=self.client.status()
self.client.start()
after=self.client.status()
self.assertEqual(before[6],after[6])
self.assertEqual(before[12:14],after[12:14])

View File

@@ -0,0 +1,54 @@
#include "gas_logger.h"
#include <assert.h>
#include <stdio.h>
typedef struct { uint32_t irq; uint16_t value; int fail; } board;
static uint32_t enter(void *u) { board *b=u; uint32_t old=b->irq; b->irq=1; return old; }
static void leave(void *u,uint32_t state) { board *b=u; assert(b->irq==1); b->irq=state; }
static int source(void *u,uint16_t a,uint16_t *v)
{ board *b=u; assert(b->irq==1); if(b->fail && a==11) return 0; *v=b->value+a; return 1; }
int main(void)
{
gl_channel channels[]={{10,100,200},{11,101,201}};
gl_config c={0x6000,2,3,0x12345678UL,channels};
board b={0,0,0};
gl_port p={&b,enter,leave,source};
gas_logger l;
uint16_t bank0[21],bank1[21],out[125],i;
assert(gl_init(&l,&c,&p,bank0,bank1,20)==GL_ARGUMENT);
assert(gl_init(&l,&c,&p,bank0,bank1,21)==GL_OK);
assert(gl_snapshot(&l)==GL_EMPTY);
assert(gl_capture(&l,0,0)==GL_BUSY);
assert(gl_write(&l,0x6012,GL_START)==GL_OK);
for(i=0;i<5;i++) { b.value=i; assert(gl_capture(&l,i,2)==GL_OK); }
assert(gl_snapshot(&l)==GL_OK && l.recording && l.count==0);
assert(gl_read(&l,0x6100,out,21)==GL_OK);
assert(out[0]==2 && out[7]==3 && out[14]==4 && out[5]==12);
for(i=5;i<15;i++) { b.value=i; assert(gl_capture(&l,i,3)==GL_OK); }
assert(gl_snapshot(&l)==GL_BUSY && l.missed==1 && l.recording);
assert(gl_read(&l,0x6100,out,21)==GL_OK && out[0]==2 && out[14]==4);
assert(gl_read(&l,0x6114,out,2)==GL_ADDRESS);
assert(gl_read(&l,65535,out,2)==GL_ARGUMENT);
assert(gl_write(&l,0x6012,GL_RELEASE)==GL_STALE);
assert(gl_write(&l,0x6013,1)==GL_OK);
assert(gl_write(&l,0x6014,0)==GL_OK);
assert(gl_write(&l,0x6012,GL_RELEASE)==GL_OK && l.recording);
assert(gl_read(&l,0x6100,out,1)==GL_ADDRESS);
assert(gl_snapshot(&l)==GL_OK);
assert(gl_read(&l,0x6100,out,21)==GL_OK && out[0]==12 && out[14]==14);
assert(gl_modbus_read(&l,100,out,2)==GL_OK && out[0]==24 && out[1]==25);
assert(gl_can_read(&l,200,out,2)==GL_OK && out[0]==24 && out[1]==25);
b.fail=1; out[0]=555;out[1]=666;
assert(gl_read(&l,10,out,2)==GL_SOURCE_ERROR && out[0]==555 && out[1]==666);
assert(gl_capture(&l,15,0)==GL_SOURCE_ERROR && l.count==0 && l.source_errors==1);
b.fail=0;b.irq=1;
assert(gl_capture(&l,16,0)==GL_OK && b.irq==1);
b.irq=0;l.sequence=0xffffffffUL;
assert(gl_capture(&l,0xffffffffUL,0)==GL_OK && l.sequence==0);
assert(gl_write(&l,0x6012,0)==GL_ARGUMENT && l.recording);
c.base=0xff00;
assert(gl_init(&l,&c,&p,bank0,bank1,21)==GL_ARGUMENT);
c.base=0x6000;channels[1].can=200;
assert(gl_init(&l,&c,&p,bank0,bank1,21)==GL_ARGUMENT);
assert(b.irq==0);
puts("GAS logger core: PASS");return 0;
}

View File

@@ -0,0 +1,19 @@
"""Regenerate/verify the firmware mirror of the packaged JSON map."""
from pathlib import Path
import argparse
import sys
root = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(root / 'python'))
from set_devices.gas_logger import load_map, c_header
parser = argparse.ArgumentParser()
parser.add_argument('--check', action='store_true')
args = parser.parse_args()
target = root / 'c/gas-logger/ports/pm35/pm35_map.h'
expected = c_header(load_map())
if args.check:
if not target.exists() or target.read_text(encoding='utf-8') != expected:
raise SystemExit('pm35_map.h differs from JSON; run generate_map.py')
else:
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(expected, encoding='utf-8')

View File

@@ -12,6 +12,7 @@ set(SETPROTOCOL_V2_SOURCES
src/set_protocol.c
src/set_can.c
src/set_firmware.c
src/set_boot.c
src/set_telemetry.c
src/set_plot.c
src/set_trends.c
@@ -39,6 +40,11 @@ set(SETPROTOCOL_LEGACY_SOURCES
)
set(SETPROTOCOL_SOURCES
src/set_regmap.c
src/set_emu_ump.c
src/set_emu_tms.c
src/set_emu_igbt.c
src/set_emu_server.c
${SETPROTOCOL_V2_SOURCES}
${SETPROTOCOL_LEGACY_SOURCES}
)
@@ -84,6 +90,19 @@ endif()
option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS)
enable_testing()
add_executable(test_emulator_services tests/test_emulator_services.c ports/example-emulator/service.c)
target_link_libraries(test_emulator_services PRIVATE setprotocol_static)
add_test(NAME emulator_services COMMAND test_emulator_services)
add_executable(test_boot tests/test_boot.c)
target_link_libraries(test_boot PRIVATE setprotocol_static)
add_test(NAME shared_boot COMMAND test_boot)
if(MSVC)
target_compile_options(test_emulator_services PRIVATE /UNDEBUG)
target_compile_options(test_boot PRIVATE /UNDEBUG)
else()
target_compile_options(test_emulator_services PRIVATE -UNDEBUG)
target_compile_options(test_boot PRIVATE -UNDEBUG)
endif()
add_executable(test_signal_wave tests/test_signal_wave.c)
target_link_libraries(test_signal_wave PRIVATE setprotocol_static)
add_test(NAME signal_wave COMMAND test_signal_wave)

View File

@@ -1,5 +1,8 @@
# SETProtocol
Универсальные сервисы эмуляторов: [адресное пространство, API и порт новой платы](docs/EMULATOR_SERVICES.md).
`set_regmap` объединяет УМП, 2812, IGBT и DAC за общим API для RTU, CAN и других оболочек.
Генератор произвольного сигнала, интерполяция, таблицы ЦАП и порты
STM32F407/G474: [SIGNAL_GENERATOR.md](docs/SIGNAL_GENERATOR.md).

37
c/set-protocol/TMS2812.md Normal file
View File

@@ -0,0 +1,37 @@
# Legacy TMS2812 / BALZAM protocol
`src/tms2812.c` is the shared wire implementation for the host terminal,
memory access, firmware framing and CRC validation. The wire format is the
legacy controller protocol, **not SETProtocol v2**. The shared library name
does not select or change the wire format.
- `tms2812_build_memory_request`: PEEK (56), POKE (57), host operation 0
for PING (PEEK of service word address `0x00000100`).
- `tms2812_build_test_request`: TEST_ALL (66), two uint16 timings and
twelve control bytes.
- `tms2812_build_fixed_request`: address, command, payload, CRC16/MODBUS.
- `tms2812_validate_response`: checks controller, command, length and CRC;
may accept omission of the two reserved bytes after CRC. The transport
must wait for a receive gap before sending the next command.
- UPLOAD (52) retains its separate validator: four reserved trailing bytes.
Bindings:
- Python: `python/protocan/tms_terminal.py` calls the exported `pcan_abi.h`
functions through ctypes. `set_devices.tms_terminal` keeps telemetry
layouts/parsing; it uses the C framing for CMD_STD.
- Kotlin: `ports/android/kotlin/ru/setcorp/setflash/core/Tms2812Protocol.kt`
calls the same C functions through `setprotocol_jni.c` and
`NativeSetProtocol`. Consumers include the templates Kotlin source tree;
they must not keep a second Tms2812Protocol class in their app source tree.
Consumers own UART I/O, timeout/settle timers, sequential write/readback
transactions, GUI field names and JSON display profiles. They do not own
copies of request builders or response CRC checks.
Rebuild the native library after updating templates. There is no fallback
to a separate local encoder if the terminal ABI is missing.
Tests: `tests/test_tms2812.c`, `python/tests/test_tms_terminal_shared.py`,
and the Android `TmsTerminalSharedTest`. All use the same Android/captured
PING and PEEK vectors and TEST_ALL field layout.

View File

@@ -0,0 +1,228 @@
# Универсальное адресное пространство эмуляторов
Общий код находится в `templates/c/set-protocol`. Прошивка платы задаёт память,
GPIO и аппаратные callbacks; протокол и модели не включают STM32/HAL/USB/ОС,
не выделяют heap и не читают системное время. Все вызовы одного экземпляра
сериализуются главным циклом или блокировкой вызывающего.
## Слои и точка расширения
- `set_regmap.h/.c`: таблица непересекающихся регионов и общий вызов
`set_regmap_request(map, function, address, value, out, capacity, &count)`.
Function 3 читает `value` слов, function 6 пишет одно слово.
- `set_emu_ump`, `set_emu_tms`, `set_emu_igbt`, `set_wavegen`:
независимые экземпляры моделей. Callbacks получают **относительный offset**,
а не внешний адрес. Один модуль можно разместить несколько раз по разным адресам.
- `set_emu_server`: готовая композиция для текущего стенда, режим УМП/2812,
подготовка настроек с применением через 500 тиков.
- `set_regmap_rtu` и `set_regmap_can`: готовые транспортные кодеки над одной картой.
`set_emu_server_rtu/can` дополнительно сохраняют legacy поведение стенда.
USB CDC, UART, TCP или иной поток только доставляет полный RTU-кадр.
Другой протокол разбирает собственную оболочку и вызывает `set_regmap_request`.
Вызов с неизвестной функцией возвращает 1; неизвестный адрес, RO-запись,
чтение через границу региона — 2; неверное значение или размер буфера — 3;
ошибка аппаратного порта — 4; занятый модуль — 6. При ошибке `count=0`.
Длина успешного чтения равна запрошенной, запись возвращает адрес/значение.
Максимум — 124 слова. RTU-обёртка требует буфер 253 байта до выполнения команды;
CAN — 124 слова. Недостаточный буфер не может выполнить запись без ответа.
При ошибке содержимое выходного буфера не используется.
`set_regmap_init` проверяет границы 16-битного пространства, пересечения,
права и callbacks. Таблица и состояния принадлежат вызывающему и должны жить
дольше карты. После регистрации таблица неизменна; для изменения карты
инициализируйте её заново в момент, когда нет обслуживаемых запросов.
## Каталог и стандартная карта
Каталог доступен на адресе 0 через любой транспорт.
| Слово заголовка | Значение |
|---|---|
| 0 | 0x5345, сигнатура SE |
| 1 | версия каталога 1 |
| 2 | количество регионов |
| 3 | начало дескрипторов: 8 |
| 4 | длина дескриптора: 8 |
| 5 | максимум слов в одном чтении: 124 |
| 6, 7 | резерв: 0 |
Дескриптор: `kind, version, instance, base, words, flags, 0, 0`.
Flags: bit0 чтение, bit1 запись. Права описывают возможности региона;
отдельные регистры внутри могут быть только для чтения. Пропуски внутри
региона возвращают ошибку, если модуль их не поддерживает.
| Kind | Стандартный адрес | Размер | Модуль |
|---|---|---|---|
| 1 | 0x1000 | 0x17C | УМП: статус, окно, онлайн |
| 2 | 0x1200 | 36 | управление, конфигурация и indexed capability |
| 3 | 0x1300 | 8 | генератор DAC |
| 4 | 0x1400 | 112 | IGBT, версия 3 |
| 5 | 0x1500 | 80 | 2812 через регистры |
| 6 | 0x2000 | capacity × 40 | архив/банк УМП |
| 7 | 0xA000 | wave capacity | таблица DAC |
Эта карта — готовая композиция, а не требование к новым платам.
`set_regmap` позволяет выбирать адреса, подмножество и несколько экземпляров
модуля; каталог сообщает фактическую карту. Настройка UART и выбор УМП/2812
принадлежат только готовому `set_emu_server`. Максимальная ёмкость УМП
в этой карте — 819 записей на банк; текущая STM32 использует 800.
В режиме 2812 прямое обращение к регистрам УМП возвращает busy, legacy
RTU/CAN к этим диапазонам игнорируются для совместимости. Остальные сервисы
и каталог доступны. Регистры управления 2812 принимают запись только в режиме 2812.
Чтение его состояния не запускает модель.
## IGBT
Предыдущий локальный диапазон IGBT 0x1300 конфликтовал с DAC. Его alias
сохранить нельзя: он обращается к генератору. Используйте каталог kind=4 или
новый стандартный адрес 0x1400.
| Offset | Назначение |
|---|---|
| 0, 1 | 0x4947, версия 3 |
| 2 | enable 0/1; выключение сразу очищает состояние RX и ожидаемые ACK |
| 3 | чтение последнего TX; запись подготовленной TX-маски |
| 4 | RX-маска |
| 5 | резерв 0 |
| 6 | FAULT-маска 0…255 |
| 7 | задержка ACK 0…1000 мс |
| 8 | ширина ACK 1…255 мс |
| 9 | счётчик фронтов modulo 65536; запись 0 сбрасывает |
| 10 | число каналов 8 |
| 11 | один шаг модели = 1 мс |
| 12 | источник TX: 0 переданные GPIO, 1 подготовленная маска |
| 13 | подготовленная TX-маска, чтение/запись |
| 14 | запись маски запуска одиночного TX; чтение 0 |
| 15 | длина записи канала: 12 слов |
Версия 3 сохраняет первые 14 регистров. Общие delay/width при записи
устанавливают значения для всех каналов; при чтении показывают последнее
общее задание, а не поканальные значения. Полное состояние: FC03 0x1400 / 112 слов.
Запуск TX требует enabled=1, source=1, разрешённого TX канала и низкого TX;
повторный запуск высокого TX отвергается кодом 6. Длительность отсчитывает
сама модель, не клиент COM. Ручная запись TX отменяет все текущие TX-импульсы.
Выключение эмуляции отменяет импульсы и сбрасывает их TX; статическое задание
TX сохраняется. PB остаются входами: тестовый TX — внутренняя подстановка,
физическая обратная связь RX выводится на PC0…PC7.
Поканальные регистры: `0x1410 + 12 * channel`, channel=0…7.
| Offset канала | Значение |
|---|---|
| 0 | flags: bit0 разрешение TX, bit1 разрешение RX, bit2 ACK active-low |
| 1 | RX mode: 0 ACK, 1 физический 0, 2 физическая 1, 3 пропуск ACK |
| 2, 3 | задержка ACK ON 0…1000 мс, ширина 1…1000 мс |
| 4, 5 | задержка ACK OFF 0…1000 мс, ширина 1…1000 мс |
| 6 | одиночный TX: 1…60000 мс |
| 7 | маска фронтов для ACK: bit0 ON, bit1 OFF |
| 8 | ширина FAULT 1…60000 мс; 0 — удержание до снятия FAULT |
| 9 | RO счётчик фронтов канала, modulo 65536 |
| 10, 11 | RO оставшаяся задержка / длительность ACK в мс |
Изменение flags/mode отменяет ожидаемый и активный ACK канала.
Отключённый RX и выключенная модель всегда дают физический 0.
Инверсия применяется к автоматическому ответу и idle при пропуске ACK;
ручные уровни 0/1 не инвертируются. Сброс общего счётчика сбрасывает и канальные.
FAULT моделирует длинный/удерживаемый ответ после фронта; это не модель Vce,
Vge, DESAT, внутренней защиты или блокировки конкретного силового драйвера.
`set_igbt_tick(state, pins)` получает маску входов от порта платы.
Фронт на тике T запускает ACK на T+delay. Повторный фронт до начала ACK
перезапускает задержку; это определённая модельная обработка слишком частых команд.
Каждый из восьми каналов независим. FAULT удерживает RX после его активации;
снятие FAULT освобождает только ранее аварийные каналы и сохраняет обычные ACK.
Чтобы TX через регистры не перезаписывался GPIO, выберите source=1.
**Текущая IGBT-модель имеет шаг 1 мс.** Она проверяет логическую последовательность
Vin/Vstat, но не воспроизводит ACK 250/380 нс и ширины профилей 1SP/1SD в DSLogic.
Такую аппаратную точность нельзя получить переносом модели или вызовом из SysTick:
для неё требуется отдельный быстрый аппаратный порт и соответствующая модель времени.
## 2812
Состояние едино для legacy CMD_STD и адресного API:
| Offset | Назначение |
|---|---|
| 0, 1 | 0x544D, версия 1 |
| 2, 3, 4 | running, частота и задание (int16 ×100 Гц) |
| 8…11 | четыре подготовленных analog int16 |
| 12…17 | шесть подготовленных управляющих байт (значение 0…255) |
| 18 | записать 1: атомарно применить подготовленную команду |
| 32…45 | 28 байт дискретов; младший байт слова — первый байт |
| 46…79 | 34 аналоговых int16 |
| остальные | чтение 0 |
Модель остаётся упрощённой BALZAM_7: Go, выбор remote, Setspeed,
разгон 10 Гц/с и синтетическая телеметрия. Legacy CMD_STD имеет отдельный
адрес 2812, запрос 18 и ответ 101 байт, с проверкой CRC до изменения состояния.
Буфер ответа менее 101 байта запрещает выполнение команды.
## УМП и DAC
УМП сохраняет прежние записи по 40 слов, банк/архив, фронтовой триггер,
постинтервал 1000 мс и indexed-чтение без изменения курсора. Два разных
буфера по `capacity*40` слов предоставляет плата. Размещение SRAM/CCM
не является частью библиотеки.
DAC использует прежний `set_wave_port` с `start/stop`, транзакционную загрузку,
commit, чтение таблицы и запрет изменений при работе. Регистровые callbacks
и прежний `set_wave_rtu` вызывают одну реализацию. До повторного init вызывающий
останавливает аппаратный вывод; библиотека не угадывает текущее состояние DMA.
## Обёртки и новая плата
Пример: [ports/example-emulator/service.c](../ports/example-emulator/service.c).
Он регистрирует два экземпляра IGBT и показывает прямой, RTU и CAN вызов.
Порт новой платы должен:
1. Создать состояния и таблицу регионов, вызвать init и проверить результат.
2. Предоставить буферы и аппаратный DAC-порт, если нужен генератор.
3. Передавать реальные тики и входы; читать результат модели и обновлять GPIO.
4. Доставлять запросы через выбранную оболочку в одну карту, сериализуя доступ.
CAN-envelope совместим с UMP: BE address/value/0x1F00/token/function.
Идентификаторы CAN и маршрутизацию задаёт порт. Ответ делится по два слова,
нечётное чтение дополняется нулём до чётного числа; клиент отбрасывает последнее
слово по длине исходного запроса. Токен 1…255; один незавершённый ответ на соединение.
Проверенная копия слов хранится до отправки всех фрагментов.
`set_emu_stream` — только сборщик известных запросов FC03/FC06 и CMD_STD,
по одному экземпляру на поток. Он принимает монотонные uint32 миллисекунды,
сохраняет кадр между USB-пакетами, сбрасывает незавершённый после паузы >1000 мс
и восстанавливается после плохого CRC. Это не универсальный Modbus-парсер
FC16/FC23: для другого формата нужна другая тонкая оболочка.
## Сборка и проверки
C99-ядро подключается через `setprotocol_static`/DLL, определения публичного
API экспортируются через PCAN_ABI_API. Включить `set_regmap.h` или
`set_emu_server.h`. Для иной архитектуры проверить наличие uint8/16/32_t;
существующий 16-битный C28x octet-порт UMP остаётся отдельным, этот новый
байтовый API пока не проверен TI-компилятором.
`test_emulator_services.c` проверяет каталог, пересечения, границы и короткие
буферы, два экземпляра, переназначение адресов, RTU/CAN/stream, совместное
состояние 2812 и DAC, длинные задержки IGBT и освобождение FAULT.
Существующие тесты прошивки дополнительно проверяют архив УМП, USB-фрагментацию,
смену режима/адресов и совместимость с декодерами SETGUI.
## Выбор единственного эмулятора
В SETGUI: «Эмулятор → Настройки STM → Тип эмулятора», затем
«Применить в STM и проверить» и «Открыть управление эмулятором».
Конфигурация версии 3: режим 0 — УМП, 1 — 2812, 2 — IGBT, 3 — ЦАП.
Регистр 0x1219 подготавливает режим; 0x1218 = 0xA55A применяет его через
500 мс после подтверждения. Статус FC03 0x1210 / 10 слов содержит версию 3,
действующий режим в слове 8 и подготовленный в слове 9.
При смене режима останавливаются TIM6/DMA/DAC, ЦАП устанавливается в ноль,
IGBT отключается и его выходы обнуляются, 2812 сбрасывается, УМП останавливается.
Новый режим не запускается автоматически. Для IGBT нужно явно включить
эмуляцию; для ЦАП заново загрузить таблицу и дать команду пуска.
Повторное применение того же режима не прерывает текущую работу.
Записи невыбранным IGBT/ЦАП/2812 отклоняются с кодом 6; чтение статуса
и явные команды остановки IGBT/ЦАП доступны. Настройки связи доступны всегда.
После сброса выбран УМП, IGBT выключен, ЦАП не запущен.

View File

@@ -102,6 +102,14 @@ PCAN_ABI_API const char *pcan_abi_periph28335_command_name(
size_t project_index, size_t command_index);
/* PM67/TMS320F2812 legacy memory upload. */
PCAN_ABI_API size_t pcan_abi_tms2812_build_fixed(uint8_t controller, uint8_t command,
const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size);
PCAN_ABI_API size_t pcan_abi_tms2812_build_memory(uint8_t controller, uint8_t command,
uint32_t word_address, uint16_t value, uint8_t *output, size_t output_size);
PCAN_ABI_API size_t pcan_abi_tms2812_build_test(uint8_t controller, uint16_t duration,
uint16_t pause, const uint8_t *digits, size_t digit_count, uint8_t *output, size_t output_size);
PCAN_ABI_API int pcan_abi_tms2812_validate_response(const uint8_t *data, size_t size,
uint8_t controller, uint8_t command, size_t full_size, int allow_short_tail);
PCAN_ABI_API uint16_t pcan_abi_tms2812_crc16(
const uint8_t *data, size_t size);
PCAN_ABI_API size_t pcan_abi_tms2812_build_upload(

View File

@@ -0,0 +1,26 @@
#ifndef SET_BOOT_H
#define SET_BOOT_H
#include "set_firmware.h"
/* Transport-independent, single-slot updater. invalidate MUST become durable
* before erase; commit is called only after read-back verification. */
typedef struct {
bool (*invalidate)(void *user);
bool (*erase)(void *user, uint32_t size);
bool (*write)(void *user, uint32_t offset, const uint8_t *data, uint16_t size);
bool (*verify)(void *user, uint32_t size, uint32_t crc);
bool (*commit)(void *user, uint32_t size, uint32_t crc);
void (*activate)(void *user);
} setp_boot_ops;
typedef struct {
setp_boot_ops ops;
void *user;
uint32_t base, capacity, crc;
uint16_t block_size;
setp_fw_status_t status;
} setp_boot;
void setp_boot_init(setp_boot *b, const setp_boot_ops *ops, void *user,
uint32_t base, uint32_t capacity);
/* Returns SET status; FW_STATUS body is encoded by the caller. */
uint16_t setp_boot_command(setp_boot *b, uint16_t type, const uint8_t *p, uint16_t n);
#endif

View File

@@ -0,0 +1,32 @@
#ifndef SET_EMU_IGBT_H
#define SET_EMU_IGBT_H
#include "set_regmap.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_IGBT_CHANNELS 8u
#define SET_IGBT_WORDS 112u
#define SET_IGBT_CHANNEL_WORDS 12u
typedef struct {
uint16_t flags, mode, on_delay, on_width, off_delay, off_width;
uint16_t tx_width, edges, fault_width, count;
} set_igbt_channel;
typedef struct {
uint8_t enabled, tx, previous_tx, rx, fault, injected_tx, input_source;
uint16_t pending[SET_IGBT_CHANNELS], active[SET_IGBT_CHANNELS];
uint16_t delay_ms, width_ms, event_count;
uint8_t logical_rx;
uint16_t pulse[SET_IGBT_CHANNELS], response_width[SET_IGBT_CHANNELS];
set_igbt_channel channel[SET_IGBT_CHANNELS];
} set_igbt_emu;
PCAN_ABI_API void set_igbt_init(set_igbt_emu *);
/* One model tick = 1 ms, no system clock. This coarse logical model does NOT
* reproduce the sub-microsecond timing of 1SP/1SD DSLogic hardware profiles.
* input_source=0 takes the supplied pins, 1 uses register-injected TX. */
PCAN_ABI_API void set_igbt_tick(set_igbt_emu *, uint8_t pins);
PCAN_ABI_API unsigned set_igbt_read(void *, uint16_t offset, uint16_t count, uint16_t *);
PCAN_ABI_API unsigned set_igbt_write(void *, uint16_t offset, uint16_t value);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,48 @@
/* Reference service composition for emulator boards; use set_regmap directly
* for a different map, subset, or multiple instances of any module. */
#ifndef SET_EMU_SERVER_H
#define SET_EMU_SERVER_H
#include "set_emu_ump.h"
#include "set_emu_tms.h"
#include "set_emu_igbt.h"
#include "set_wavegen.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_EMU_CONTROL_BASE 0x1200u
#define SET_EMU_IGBT_BASE 0x1400u
#define SET_EMU_TMS_BASE 0x1500u
#define SET_EMU_INDEXED_BASE 0x2000u
enum { SET_EMU_KIND_UMP=1,SET_EMU_KIND_CONTROL=2,SET_EMU_KIND_WAVE=3,
SET_EMU_KIND_IGBT=4,SET_EMU_KIND_TMS=5,SET_EMU_KIND_UMP_DATA=6,
SET_EMU_KIND_WAVE_DATA=7 };
typedef struct {
set_regmap map;
set_reg_region regions[7];
set_ump_emu ump;
set_tms_emu tms;
set_igbt_emu igbt;
set_wave_state *wave;
/* config[3]: 0=UMP, 1=TMS2812, 2=IGBT, 3=DAC. Configuration version 3. */
uint16_t config[4],staged[4],apply_delay;
} set_emu_server;
/* bank_capacity in records, each bank has capacity*40 words.
* Wave may be NULL. UART rate index 0..4. No pins, heap or global instances.
* Init is for a quiescent instance: caller stops hardware before reinitializing. */
PCAN_ABI_API unsigned set_emu_server_init(set_emu_server *,uint16_t *bank0,
uint16_t *bank1,uint16_t bank_capacity,set_wave_state *,unsigned address,
unsigned tms_address,unsigned baud_index,unsigned can_mode,unsigned enabled);
PCAN_ABI_API void set_emu_server_tick(set_emu_server *,uint8_t igbt_pins);
PCAN_ABI_API uint32_t set_emu_server_baud(const set_emu_server *);
PCAN_ABI_API size_t set_emu_server_rtu(set_emu_server *,const uint8_t *,size_t,uint8_t *,size_t);
PCAN_ABI_API size_t set_emu_server_can(set_emu_server *,const uint8_t *,size_t,
uint16_t *,size_t,unsigned *,unsigned *);
/* Stream adapter for FC03/06 (8 octets) and TMS CMD_STD (18 octets). One
* context per connection. Transport passes monotonic milliseconds. */
typedef struct { uint8_t data[18],used;uint32_t last_byte; } set_emu_stream;
PCAN_ABI_API size_t set_emu_stream_feed(set_emu_stream *,set_emu_server *,
uint8_t byte,uint32_t now_ms,uint8_t *reply,size_t capacity);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,22 @@
#ifndef SET_EMU_TMS_H
#define SET_EMU_TMS_H
#include "set_regmap.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_TMS_WORDS 80u
typedef struct {
uint8_t controls[6], running;
uint16_t commands[10], divider;
int32_t frequency, target;
} set_tms_emu;
PCAN_ABI_API void set_tms_init(set_tms_emu *);
PCAN_ABI_API void set_tms_tick(set_tms_emu *);
PCAN_ABI_API unsigned set_tms_read(void *,uint16_t,uint16_t,uint16_t *);
PCAN_ABI_API unsigned set_tms_write(void *,uint16_t,uint16_t);
PCAN_ABI_API size_t set_tms_rtu(set_tms_emu *,unsigned device,const uint8_t *,
size_t,uint8_t *,size_t);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,25 @@
#ifndef SET_EMU_UMP_H
#define SET_EMU_UMP_H
#include "set_regmap.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_UMP_WORDS 40u
typedef struct {
uint16_t (*banks[2])[SET_UMP_WORDS];
uint16_t capacity,can_mode;
uint8_t enabled;
uint16_t state,used,next,bank,archive_bank,archive_count,archive_next,period,divider,cursor,source,generation,post,dropped,prev_command,prev_input,prev_output;
uint32_t elapsed,sequence,scenario;
} set_ump_emu;
/* Two distinct caller-owned arrays, capacity*40 words EACH. No heap/MCU layout. */
PCAN_ABI_API unsigned set_ump_init(set_ump_emu *,uint16_t *,uint16_t *,uint16_t capacity,
uint16_t can_mode,uint8_t enabled);
PCAN_ABI_API void set_ump_tick(set_ump_emu *); /* one millisecond */
PCAN_ABI_API unsigned set_ump_read(void *,uint16_t,uint16_t,uint16_t *);
PCAN_ABI_API unsigned set_ump_indexed_read(void *,uint16_t,uint16_t,uint16_t *);
PCAN_ABI_API unsigned set_ump_write(void *,uint16_t,uint16_t);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,48 @@
/* Transport-independent 16-bit word address space. No heap, clocks or board headers. */
#ifndef SET_REGMAP_H
#define SET_REGMAP_H
#include "pcan_abi.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SET_REG_MAX_WORDS 124u
#define SET_REG_READ 1u
#define SET_REG_WRITE 2u
enum { SET_REG_OK=0, SET_REG_FUNCTION=1, SET_REG_ADDRESS=2,
SET_REG_VALUE=3, SET_REG_FAILURE=4, SET_REG_BUSY=6 };
typedef unsigned (*set_reg_read_fn)(void *, uint16_t, uint16_t, uint16_t *);
typedef unsigned (*set_reg_write_fn)(void *, uint16_t, uint16_t);
typedef struct {
uint16_t base, words, kind, version, instance, flags;
void *context;
set_reg_read_fn read;
set_reg_write_fn write;
} set_reg_region;
typedef struct {
const set_reg_region *regions;
size_t count;
} set_regmap;
/* Directory at 0: 8-word header, then 8 words per region. Reject overlaps,
* wraparound, invalid callbacks and overlap with the directory itself.
* Regions and contexts must outlive the map and remain unchanged. */
PCAN_ABI_API unsigned set_regmap_init(set_regmap *, const set_reg_region *, size_t);
/* One region per request. Preflight all bounds/capacity before calling a module.
* Writes return absolute address/value echo. Reads must be side-effect free.
* A module validates the entire write before mutation. Caller serializes access. */
PCAN_ABI_API unsigned set_regmap_request(const set_regmap *, unsigned function,
uint16_t address, uint16_t value, uint16_t *out, size_t capacity, size_t *count);
/* Complete Modbus RTU FC03/06 frame; no broadcast. Capacity >=253 bytes.
* CRC/address/capacity validation precedes dispatch. */
PCAN_ABI_API size_t set_regmap_rtu(const set_regmap *, unsigned device,
const uint8_t *, size_t, uint8_t *, size_t);
/* Legacy UMP CAN envelope, usable by any region: BE address/value/1F00/token/fn.
* Returns padded EVEN words, last padding word=0. Caller retains original
* requested count; token/error accompany fragments. Output capacity >=124. */
PCAN_ABI_API size_t set_regmap_can(const set_regmap *, const uint8_t *, size_t,
uint16_t *, size_t, unsigned *token, unsigned *error);
PCAN_ABI_API size_t set_regmap_can_fragment(const uint16_t *, size_t count,
size_t index, unsigned token, unsigned error, uint8_t *, size_t capacity);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -4,6 +4,12 @@
*/
#ifndef SETPROTOCOL_H
#define SETPROTOCOL_H
#include "set_regmap.h"
#include "set_emu_server.h"
#include "balsam_can.h"
#include "periph28335.h"
#include "tms2812.h"
#include "balsam_can.h"
#include "periph28335.h"

View File

@@ -1,6 +1,6 @@
/**
* @file tms2812.h
* @brief Shared PM67/TMS320F2812 legacy upload protocol.
* @brief Shared PM67/TMS320F2812 legacy terminal and memory protocol.
*/
#ifndef TMS2812_H
#define TMS2812_H
@@ -13,6 +13,10 @@ extern "C" {
#endif
#define TMS2812_CMD_UPLOAD 52U
#define TMS2812_CMD_PEEK 56U
#define TMS2812_CMD_POKE 57U
#define TMS2812_CMD_TEST_ALL 66U
#define TMS2812_PING_ADDRESS 0x00000100UL
#define TMS2812_UPLOAD_REQUEST_SIZE 12U
#define TMS2812_UPLOAD_RESPONSE_OVERHEAD 8U
@@ -28,6 +32,20 @@ typedef enum {
uint16_t tms2812_crc16(const uint8_t *data, size_t size);
size_t tms2812_build_fixed_request(uint8_t controller, uint8_t command,
const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size);
/* command=0 is the host PING operation; on wire it is PEEK at 0x100. */
size_t tms2812_build_memory_request(uint8_t controller, uint8_t command,
uint32_t word_address, uint16_t value, uint8_t *output, size_t output_size);
size_t tms2812_build_test_request(uint8_t controller, uint16_t duration,
uint16_t pause, const uint8_t *digits, size_t digit_count,
uint8_t *output, size_t output_size);
/* Ordinary BALZAM responses reserve two bytes after CRC. If allow_short_tail
* is true only those bytes may be omitted; caller must observe the UART gap.
* UPLOAD responses use tms2812_validate_upload_response instead. */
int tms2812_validate_response(const uint8_t *data, size_t size,
uint8_t controller, uint8_t command, size_t full_size, int allow_short_tail);
size_t tms2812_build_upload_request(
uint8_t controller, uint32_t word_address, uint32_t byte_count,
uint8_t *output, size_t output_size);

View File

@@ -0,0 +1,55 @@
/* UMP logger v2 (PM35), independent of PM67 and SET protocol v2.
* Octets use 16-bit storage: C28x has CHAR_BIT=16 and no uint8_t.
* All lengths below count wire octets or register words, never sizeof bytes.
*/
#ifndef UMP_PROTOCOL_H
#define UMP_PROTOCOL_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define UMP_BASE 0x1000U
#define UMP_DATA 0x1010U
#define UMP_LIVE 0x1100U
#define UMP_MARKER 0x1F00U
#define UMP_MAX_WORDS 124U
#define UMP_RTU_CAPACITY 253U
typedef uint16_t ump_octet;
/* Called under caller's snapshot lock. write returns 1 on success, 0 otherwise. */
typedef struct {
void *context;
uint16_t (*read)(void *context, uint16_t address);
int (*write)(void *context, uint16_t address, uint16_t value);
} ump_backend;
/* Returns Modbus exception 0/1/2/3. CAN has stricter whole-block reads. */
unsigned ump_validate(unsigned function, uint16_t address, uint16_t value, int can);
/* reply capacity is >=124 words. Errors and writes return address/value echo. */
unsigned ump_prepare(const ump_backend *backend, unsigned function,
uint16_t address, uint16_t value, int can, uint16_t *reply, unsigned *error);
uint16_t ump_crc(const ump_octet *data, size_t count);
/* Mode 1..16; kind 0=request, 1=normal telemetry, 2=logger reply; invalid ->0. */
uint32_t ump_can_id(unsigned mode, unsigned kind);
/* Returns 0 for non-service frames, malformed size, or zero token. */
unsigned ump_can_request(const ump_backend *backend, const ump_octet *request,
size_t size, uint16_t *reply, unsigned *token, unsigned *error);
/* count must be an even number 2..124; index counts pairs. Returns 8 or 0. */
unsigned ump_can_fragment(const uint16_t *words, unsigned count, unsigned index,
unsigned token, unsigned error, ump_octet *reply);
/* Complete RTU frame only, after UART framing. Ignores bad CRC/foreign address.
* Caller provides >=253 octets; no writes occur on insufficient capacity.
* Device address 1..247; no broadcasts or legacy padding in this extension.
*/
size_t ump_rtu(const ump_backend *backend, unsigned device,
const ump_octet *request, size_t size, ump_octet *reply, size_t capacity);
/* Response for an already CRC-checked/framed request (legacy UART receiver). */
size_t ump_rtu_response(const ump_backend *backend, unsigned device, unsigned function,
uint16_t address, uint16_t value, ump_octet *reply, size_t capacity);
/* Serialize a captured reply after releasing the snapshot lock. Returns 0 on
* invalid count/capacity; error replies need no words. */
size_t ump_rtu_encode(unsigned device, unsigned function, unsigned error,
const uint16_t *words, unsigned count, ump_octet *reply, size_t capacity);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,263 @@
package ru.setcorp.setflash.core
import ru.setcorp.setprotocol.NativeSetProtocol
/** Shared templates facade for BALZAM TMS320F2812; framing and CRC use C99. */
object Tms2812Protocol {
const val CMD_LOAD = 51
const val CMD_UPLOAD = 52
const val CMD_RUN = 53
const val CMD_XFLASH = 54
const val CMD_TFLASH = 55
const val CMD_PEEK = 56
const val CMD_POKE = 57
const val CMD_INITLOAD = 58
const val CMD_INIT = 59
const val CMD_EXTEND = 60
const val CMD_VECTOR = 61
const val CMD_IMPULSE = 62
const val CMD_STD = 65
const val CMD_TEST_ALL = 66
// BALZAM CHIEF bios.c returns 0x0055 at ADR_FOR_SPECIAL without reading
// the peripheral bus. Address 0 goes through read_bus_special(), whose
// failure suppresses the reply entirely, so it is unsuitable for PING.
const val PING_MEMORY_ADDRESS = 0x00000100L
// extendbios() operation selectors from BALZAM_167/Src/mybalzam_7/bios.c.
const val EXTEND_FLASH_REMOTE_EEPROM = 6
const val EXTEND_READ_REMOTE_EEPROM = 7
const val EXTEND_RESET_TO_INTERNAL_FLASH = 9
const val EXTEND_FLASH_SPARTAN6 = 10
const val EXTEND_VERIFY_REMOTE_EEPROM = 17
const val ACK_SIZE = 6
const val PEEK_RESPONSE_SIZE = 10
const val STD_RESPONSE_SIZE = 112
const val UPLOAD_RESPONSE_OVERHEAD = 8
const val EXTEND_RESPONSE_SIZE = 18
data class StandardResponse(
val digital: ByteArray,
val analog: List<Int>,
)
data class ExtendResponse(
val processedWords: Long,
val errorCode: Long,
val repeatErrors: Long,
)
fun crc16(data: ByteArray, initial: Int = 0xFFFF): Int {
require(initial == 0xFFFF) {
"Произвольное начальное значение CRC не входит в протокол ПМ67"
}
requireNative()
return NativeSetProtocol.nativeTms2812Crc16(data)
}
fun initLoad(address: Int, ramAddress: Long, length: Int): ByteArray = fixed(
address,
CMD_INITLOAD,
le32(ramAddress) + le32(length.toLong()),
12,
)
/**
* LOAD is address + command, followed by raw data and CRC. RS485.c consumes
* the first two bytes as a normal header, switches RS_FlagBegin off for
* CMD_LOAD, and stores the remaining Length + 2 bytes in RAM.
*/
fun loadData(address: Int, image: ByteArray): ByteArray {
return fixed(address, CMD_LOAD, image, image.size + 4)
}
fun tFlash(address: Int, ramAddress: Long, flashAddress: Long, length: Int): ByteArray = fixed(
address,
CMD_TFLASH,
le32(ramAddress) + le32(flashAddress) + le32(length.toLong()),
16,
)
fun upload(address: Int, memoryAddress: Long, length: Int): ByteArray {
require(address in 0..255) { "Адрес контроллера должен быть 0…255" }
require(memoryAddress in 0..0xFFFF_FFFFL) { "Адрес слова вне диапазона uint32" }
require(length > 0) { "Размер блока должен быть положительным" }
requireNative()
return requireNotNull(
NativeSetProtocol.nativeTms2812BuildUpload(
address, memoryAddress, length.toLong(),
),
) { "SETProtocol отклонил CMD_UPLOAD ПМ67" }
}
/**
* bios.c sends UPLOAD as address, command, packed bytes, CRC16 and four
* trailing bytes. Unlike ordinary replies its CRC immediately follows data.
*/
fun validateUploadResponse(packet: ByteArray, address: Int, length: Int): Boolean {
if (length <= 0 || address !in 0..255) return false
requireNative()
return NativeSetProtocol.nativeTms2812ValidateUpload(packet, address, length)
}
fun uploadData(packet: ByteArray, address: Int, length: Int): ByteArray {
requireNative()
return requireNotNull(
NativeSetProtocol.nativeTms2812DecodeUpload(packet, address, length),
) { "Поврежден ответ CMD_UPLOAD" }
}
fun peek(address: Int, memoryAddress: Long): ByteArray =
memory(address, CMD_PEEK, memoryAddress, 0)
fun ping(address: Int): ByteArray = memory(address, 0, 0, 0)
fun poke(address: Int, memoryAddress: Long, value: Int): ByteArray =
memory(address, CMD_POKE, memoryAddress, value)
private fun memory(address: Int, command: Int, memoryAddress: Long, value: Int): ByteArray {
require(address in 0..255 && memoryAddress in 0..0xFFFF_FFFFL && value in 0..65535) {
"Адрес или значение TMS2812 вне диапазона"
}
requireNative()
return requireNotNull(NativeSetProtocol.nativeTms2812BuildMemory(address, command, memoryAddress, value))
}
fun testAll(address: Int, duration: Int, pause: Int, digits: ByteArray): ByteArray {
require(address in 0..255 && duration in 0..65535 && pause in 0..65535 && digits.size == 12)
requireNative()
return requireNotNull(NativeSetProtocol.nativeTms2812BuildTest(address, duration, pause, digits))
}
fun extend(
address: Int,
firstAddress: Long,
secondAddress: Long,
length: Long,
code1: Int,
code2: Int,
): ByteArray = fixed(
address,
CMD_EXTEND,
le32(firstAddress) + le32(secondAddress) + le32(length) + byteArrayOf(code1.toByte(), code2.toByte()),
18,
)
fun parseExtendResponse(packet: ByteArray, address: Int): ExtendResponse {
require(packet.size == EXTEND_RESPONSE_SIZE && validateResponse(packet, address, CMD_EXTEND)) {
"Поврежден ответ CMD_EXTEND"
}
return ExtendResponse(
processedWords = le32Value(packet, 2),
errorCode = le32Value(packet, 6),
repeatErrors = le32Value(packet, 10),
)
}
fun standard(address: Int, analog: List<Int>, flags: Set<Int>): ByteArray {
require(analog.size == 4) { "CMD_STD должен содержать четыре аналоговых значения" }
val payload = ByteArray(14)
analog.forEachIndexed { index, value ->
require(value in Short.MIN_VALUE..Short.MAX_VALUE) { "Аналог ${index + 1} вне диапазона Int16" }
payload[index * 2] = value.toByte()
payload[index * 2 + 1] = (value ushr 8).toByte()
}
flags.forEach { oneBased ->
require(oneBased in 1..48) { "Номер флага должен быть от 1 до 48" }
val bit = oneBased - 1
payload[8 + bit / 8] = (payload[8 + bit / 8].toInt() or (1 shl (bit % 8))).toByte()
}
return fixed(address, CMD_STD, payload, 18)
}
fun validateResponse(packet: ByteArray, address: Int, command: Int): Boolean {
requireNative()
return NativeSetProtocol.nativeTms2812ValidateResponse(packet, address, command, packet.size, false)
}
/**
* BALZAM replies can omit final reserved bytes after CRC (two in ordinary
* replies, four in UPLOAD data replies).
* Call after the receive settle interval so a delayed tail cannot overlap
* the next request. Never pad missing data or CRC bytes. UPLOAD uses its
* own validator because its CRC precedes four trailing bytes, not two.
*/
fun normalizeResponseTail(
packet: ByteArray,
address: Int,
command: Int,
fullSize: Int,
): ByteArray? {
val uploadData = command == CMD_UPLOAD && fullSize >= UPLOAD_RESPONSE_OVERHEAD
val reservedBytes = if (uploadData) 4 else 2
if (fullSize < ACK_SIZE || packet.size !in (fullSize - reservedBytes)..fullSize) return null
val normalized = packet.copyOf(fullSize)
val valid = if (uploadData) {
validateUploadResponse(normalized, address, fullSize - UPLOAD_RESPONSE_OVERHEAD)
} else {
requireNative()
NativeSetProtocol.nativeTms2812ValidateResponse(packet, address, command, fullSize, true)
}
return normalized.takeIf { valid }
}
fun peekValue(packet: ByteArray, address: Int): Int {
require(packet.size == PEEK_RESPONSE_SIZE && validateResponse(packet, address, CMD_PEEK)) {
"Поврежден ответ CMD_PEEK"
}
return (packet[2].toInt() and 0xFF) or ((packet[3].toInt() and 0xFF) shl 8)
}
fun parseStandard(packet: ByteArray, address: Int): StandardResponse {
require(packet.size == STD_RESPONSE_SIZE && validateResponse(packet, address, CMD_STD)) {
"Поврежден ответ CMD_STD"
}
val digital = packet.copyOfRange(2, 40)
val analog = (0 until 34).map { index ->
val offset = 40 + index * 2
(((packet[offset].toInt() and 0xFF) or
((packet[offset + 1].toInt() and 0xFF) shl 8)).toShort()).toInt()
}
return StandardResponse(digital, analog)
}
fun commandName(command: Int): String = when (command) {
CMD_LOAD -> "CMD_LOAD"
CMD_UPLOAD -> "CMD_UPLOAD"
CMD_RUN -> "CMD_RUN"
CMD_XFLASH -> "CMD_XFLASH"
CMD_TFLASH -> "CMD_TFLASH"
CMD_PEEK -> "CMD_PEEK"
CMD_POKE -> "CMD_POKE"
CMD_INITLOAD -> "CMD_INITLOAD"
CMD_INIT -> "CMD_INIT"
CMD_EXTEND -> "CMD_EXTEND"
CMD_VECTOR -> "CMD_VECTOR"
CMD_IMPULSE -> "CMD_IMPULSE"
CMD_STD -> "CMD_STD"
CMD_TEST_ALL -> "CMD_TEST_ALL"
else -> "CMD_$command"
}
private fun fixed(address: Int, command: Int, payload: ByteArray, size: Int): ByteArray {
require(address in 0..255) { "Адрес контроллера должен быть 0…255" }
require(payload.size + 4 == size) { "Неверная длина пакета" }
requireNative()
return requireNotNull(NativeSetProtocol.nativeTms2812BuildFixed(address, command, payload))
}
private fun le32(value: Long): ByteArray = ByteArray(4) { index ->
(value ushr (index * 8)).toByte()
}
private fun le32Value(data: ByteArray, offset: Int): Long =
(data[offset].toLong() and 0xFF) or
((data[offset + 1].toLong() and 0xFF) shl 8) or
((data[offset + 2].toLong() and 0xFF) shl 16) or
((data[offset + 3].toLong() and 0xFF) shl 24)
private fun requireNative() {
check(NativeSetProtocol.available) { "Нативное ядро SETProtocol недоступно" }
}
}

View File

@@ -53,6 +53,11 @@ object NativeSetProtocol {
external fun nativePeriph28335ProjectName(projectIndex: Int): String?
external fun nativePeriph28335CommandName(projectIndex: Int, commandIndex: Int): String?
external fun nativeTms2812Crc16(input: ByteArray): Int
external fun nativeTms2812BuildFixed(controller: Int, command: Int, payload: ByteArray): ByteArray?
external fun nativeTms2812BuildMemory(controller: Int, command: Int, address: Long, value: Int): ByteArray?
external fun nativeTms2812BuildTest(controller: Int, duration: Int, pause: Int, digits: ByteArray): ByteArray?
external fun nativeTms2812ValidateResponse(input: ByteArray, controller: Int, command: Int,
fullSize: Int, allowShortTail: Boolean): Boolean
external fun nativeTms2812BuildUpload(controller: Int, wordAddress: Long, byteCount: Long): ByteArray?
external fun nativeTms2812ExpectedUploadSize(byteCount: Long): Int
external fun nativeTms2812ValidateUpload(input: ByteArray, controller: Int, byteCount: Int): Boolean

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

@@ -10,6 +10,79 @@
#include "periph28335.h"
#include "tms2812.h"
static jbyteArray tms_result(JNIEnv *env, const uint8_t *data, size_t size)
{
if (size == 0U || size > INT32_MAX) return NULL;
jbyteArray result = (*env)->NewByteArray(env, (jsize)size);
if (result != NULL) (*env)->SetByteArrayRegion(env, result, 0, (jsize)size, (const jbyte *)data);
return result;
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812BuildFixed(
JNIEnv *env, jobject self, jint controller, jint command, jbyteArray input)
{
(void)self;
if (input == NULL || controller < 0 || controller > 255 || command < 0 || command > 255) return NULL;
jsize size = (*env)->GetArrayLength(env, input);
if (size > INT32_MAX - 4) return NULL;
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if (data == NULL) return NULL;
uint8_t *output = (uint8_t *)malloc((size_t)size + 4U);
if (output == NULL) { (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); return NULL; }
size_t written = tms2812_build_fixed_request((uint8_t)controller, (uint8_t)command,
(const uint8_t *)data, (size_t)size, output, (size_t)size + 4U);
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
jbyteArray result = tms_result(env, output, written);
free(output);
return result;
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812BuildMemory(
JNIEnv *env, jobject self, jint controller, jint command, jlong address, jint value)
{
(void)self;
uint8_t output[12];
if (controller < 0 || controller > 255 || command < 0 || command > 255 ||
address < 0 || (uint64_t)address > UINT32_MAX || value < 0 || value > 65535) return NULL;
size_t written = tms2812_build_memory_request((uint8_t)controller, (uint8_t)command,
(uint32_t)address, (uint16_t)value, output, sizeof output);
return tms_result(env, output, written);
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812BuildTest(
JNIEnv *env, jobject self, jint controller, jint duration, jint pause, jbyteArray input)
{
(void)self;
uint8_t output[20];
if (input == NULL || controller < 0 || controller > 255 || duration < 0 ||
duration > 65535 || pause < 0 || pause > 65535 || (*env)->GetArrayLength(env, input) != 12) return NULL;
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if (data == NULL) return NULL;
size_t written = tms2812_build_test_request((uint8_t)controller, (uint16_t)duration,
(uint16_t)pause, (const uint8_t *)data, 12U, output, sizeof output);
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
return tms_result(env, output, written);
}
JNIEXPORT jboolean JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812ValidateResponse(
JNIEnv *env, jobject self, jbyteArray input, jint controller, jint command,
jint full_size, jboolean allow_short_tail)
{
(void)self;
if (input == NULL || controller < 0 || controller > 255 || command < 0 || command > 255 || full_size < 6) return JNI_FALSE;
jsize size = (*env)->GetArrayLength(env, input);
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if (data == NULL) return JNI_FALSE;
int status = tms2812_validate_response((const uint8_t *)data, (size_t)size,
(uint8_t)controller, (uint8_t)command, (size_t)full_size, allow_short_tail != JNI_FALSE);
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
return status == TMS2812_OK ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jint JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812Crc16(
JNIEnv *env, jobject self, jbyteArray input)

View File

@@ -0,0 +1,26 @@
package ru.setcorp.setflash.core
import org.junit.Assert.*
import org.junit.Test
class TmsTerminalSharedTest {
@Test fun testAllUsesSharedNativeFraming() {
val digits = byteArrayOf(0x81.toByte(), 2, 4, 8, 9, 0x20, 0x10, 0x20, 0x40, 0x80.toByte(), 3, 4)
val packet = Tms2812Protocol.testAll(7, 25, 18, digits)
assertEquals(20, packet.size)
assertArrayEquals(byteArrayOf(7, 66, 25, 0, 18, 0) + digits, packet.copyOf(18))
assertEquals(0, Tms2812Protocol.crc16(packet))
}
@Test fun pokeKeepsWordAddressAndZeroPadding() {
val packet = Tms2812Protocol.poke(10, 0x12345678L, 0xABCD)
assertArrayEquals(byteArrayOf(10, 57, 0x78, 0x56, 0x34, 0x12,
0xCD.toByte(), 0xAB.toByte(), 0, 0), packet.copyOf(10))
assertEquals(0, Tms2812Protocol.crc16(packet))
}
@Test(expected = IllegalArgumentException::class)
fun invalidAddressIsNotTruncated() {
Tms2812Protocol.peek(10, 0x1_0000_0000L)
}
}

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

@@ -0,0 +1,41 @@
/* Minimal board-independent composition. This compiles without any MCU headers.
* Adapt these entry points to the selected board's network/USB/UART/CAN port.
* Run every call in one task (or protect the entire call with the same lock). */
#include "set_emu_igbt.h"
static set_igbt_emu drivers[2];
static set_reg_region regions[2];
static set_regmap map;
unsigned example_service_init(void)
{
unsigned i;
for(i=0;i<2;++i) {
set_reg_region *r=&regions[i];
set_igbt_init(&drivers[i]);
r->base=(uint16_t)(0x300+i*0x100);r->words=SET_IGBT_WORDS;
r->kind=4;r->version=2;r->instance=(uint16_t)i;r->flags=3;
r->context=&drivers[i];r->read=set_igbt_read;r->write=set_igbt_write;
}
return set_regmap_init(&map,regions,2);
}
void example_service_tick(uint8_t inputs0,uint8_t inputs1,uint8_t outputs[2])
{
set_igbt_tick(&drivers[0],inputs0);set_igbt_tick(&drivers[1],inputs1);
outputs[0]=drivers[0].rx;outputs[1]=drivers[1].rx;
}
unsigned example_custom_protocol(unsigned function,uint16_t address,uint16_t value,
uint16_t *words,size_t capacity,size_t *count)
{
/* Caller has already decoded/authenticated its own transport envelope. */
return set_regmap_request(&map,function,address,value,words,capacity,count);
}
size_t example_uart_or_usb(const uint8_t *frame,size_t length,uint8_t *reply,size_t capacity)
{
return set_regmap_rtu(&map,16,frame,length,reply,capacity);
}
size_t example_can(const uint8_t frame[8],uint16_t *snapshot,size_t capacity,
unsigned *token,unsigned *error)
{
/* Select the CAN request ID outside the model; send the returned snapshot
* using set_regmap_can_fragment and the board's chosen response CAN ID. */
return set_regmap_can(&map,frame,8,snapshot,capacity,token,error);
}

View File

@@ -0,0 +1,54 @@
/* Shared SET USB boot dispatcher. The MCU wrapper supplies BOOT_* macros. */
#include "usb_setp_stream.h"
#include <string.h>
static setp_boot boot;
static bool reset_pending;
static uint32_t reset_at;
typedef struct { uint8_t *out; uint16_t size; uint32_t now; } reply_context;
void boot_protocol_init(void) { setp_boot_init(&boot,&BOOT_OPS,0,BOOT_APP_BASE,BOOT_APP_SIZE); }
void boot_protocol_poll(uint32_t now) {
if(reset_pending && (int32_t)(now-reset_at)>=0) BOOT_RESET();
}
static void dispatch(const setp_frame_t *r,void *user) {
reply_context *c=user;
uint8_t body[128]; uint16_t status=SETP_STATUS_OK; size_t n=0;
setp_frame_t response={0};
if((r->destination!=0 && r->destination!=BOOT_NODE) ||
(r->flags&(SETP_FLAG_RESPONSE|SETP_FLAG_EVENT))) return;
if(r->flags&(SETP_FLAG_ERROR|SETP_FLAG_MORE)) status=SETP_STATUS_INVALID_ARGUMENT;
else if(r->message_type>=SETP_MSG_FW_BEGIN && r->message_type<=SETP_MSG_FW_ACTIVATE) {
status=setp_boot_command(&boot,r->message_type,r->payload,r->payload_length);
if(!status && r->message_type==SETP_MSG_FW_STATUS)
n=setp_fw_status_encode(&boot.status,body+2,sizeof(body)-2);
if(!status && r->message_type==SETP_MSG_FW_ACTIVATE) {
reset_pending=true; reset_at=c->now+500;
}
} else if(r->payload_length) status=SETP_STATUS_INVALID_LENGTH;
else switch(r->message_type) {
case SETP_MSG_PING: setp_put_u32(body+2,c->now); n=4; break;
case SETP_MSG_DEVICE_INFO: {
static const uint8_t model[]=BOOT_MODEL;
const setp_device_info_t info={1,0,0,0x10000,3,0,sizeof(model)-1,model};
n=setp_device_info_encode(&info,body+2,sizeof(body)-2); break;
}
case SETP_MSG_CAPABILITIES: {
const setp_capabilities_t caps={1,SETP_MAX_PAYLOAD,
SETP_IFACE_MASK(SETP_IFACE_USB_CDC),SETP_FEATURE_FIRMWARE,0,0,0,0};
n=setp_capabilities_encode(&caps,body+2,sizeof(body)-2); break;
}
default: status=SETP_STATUS_UNSUPPORTED; break;
}
setp_put_u16(body,status); if(status) n=0;
response.flags=SETP_FLAG_RESPONSE|(status ? SETP_FLAG_ERROR : 0);
response.message_type=r->message_type; response.sequence=r->sequence;
response.source=BOOT_NODE; response.destination=r->source;
response.payload=body; response.payload_length=(uint16_t)(n+2);
c->size=(uint16_t)setp_frame_encode(&response,c->out,SETP_FRAME_MAX);
}
void usb_setp_reset(usb_setp_stream *s) { memset(s,0,sizeof(*s)); }
uint16_t usb_setp_feed(usb_setp_stream *s,uint8_t byte,uint32_t now,uint8_t *out) {
reply_context c={out,0,now};
if(now-s->last_byte>1000) setp_parser_reset(&s->parser);
s->last_byte=now; (void)setp_parser_feed(&s->parser,&byte,1,dispatch,&c);
return c.size;
}

View File

@@ -0,0 +1,13 @@
# Порт STM32F407 / эмулятор периферии
Добавьте `src/ump_protocol.c` и `ump_stm32f407.c` в сборку.
Порт получает backend с read/write и переводит 8-битные буферы STM32 в
октеты ядра. Приём UART, CRC framing, CAN ID/DLC/RTR-фильтры и очередь
передачи находятся в проекте; ядро проверяет CRC полного RTU кадра.
Вызывать из главного цикла. В эмуляторе SysTick лишь накапливает тики,
поэтому read/write и генератор не выполняются одновременно.
Ответ RTU требует 253 байта; CAN snapshot — 124 uint16_t. Не передавайте
неполный snapshot в fragment, count должен быть фактическим числом слов.
Подключено в `home/407vet6_emul_TMS_Periph/Src/shared_ump.c`.
[Контракт, пример и проверки](../../docs/UMP_PROTOCOL.md).

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