Добавить общие графики, декодер KONOR и порт STM32 bxCAN
This commit is contained in:
@@ -34,6 +34,7 @@ templates/
|
||||
| [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 |
|
||||
| [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
|
||||
| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android и STM32F4-порты в комплекте** |
|
||||
| [`c/set-protocol/ports/stm32-bxcan`](c/set-protocol/ports/stm32-bxcan) | порт прикладного ProtoCAN для STM32, бывший SETCAN; сохранён API `PROTOCAN_*` | STM32 HAL CAN/RTC/TIM + общее ядро `pcan_id` | classic bxCAN; настройки платы предоставляет прошивка |
|
||||
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
|
||||
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
|
||||
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
|
||||
@@ -44,6 +45,10 @@ templates/
|
||||
| Модуль | Что делает | Зависимости |
|
||||
|---|---|---|
|
||||
| [`python/protocan`](python/protocan) | разбор ProtoCAN, транспортный кадр моста, кадр SETGUI, кодеки каталога | stdlib, Python 3.9+ |
|
||||
| [`python/protocan/trends.py`](python/protocan/trends.py) | общие настройки графиков, ограниченная история, ctypes-декодер GAS/raw CAN | stdlib, опционально SETProtocol DLL/SO |
|
||||
|
||||
Общие тренды для Android GUI и SETGUI: [формат JSON, C99-ядро и адаптеры](c/set-protocol/docs/GUI_TRENDS.md).
|
||||
Общие масштабирование и маркеры: [C99, JNI и Python/Qt](c/set-protocol/docs/GUI_PLOT.md).
|
||||
|
||||
Кодировщики `c/set-protocol` и `python/protocan` дают побайтово
|
||||
одинаковый результат — это зафиксировано эталонами в тестах на C.
|
||||
@@ -94,7 +99,6 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
|
||||
|
||||
| Код | Почему не переносится | Что можно вытащить |
|
||||
|---|---|---|
|
||||
| `SETCAN/Src/protocan.c` | 59 вызовов `HAL_*`, привязка к `CAN_HandleTypeDef`, `RTC_HandleTypeDef`, `NVIC_SystemReset()` | разбиение длинных посылок на кадры и проверка даты — чистая логика, нужны два интерфейса: «отправить кадр» и «часы». Раскладка идентификатора уже вынесена в `pcan_id` |
|
||||
| `1921vk028/drivers/user/src/one_wire_user.c` | аппаратный 1-Wire `OWI_*` из `plib028`, мигание светодиодом прямо внутри опроса датчика | логика DS18B20 уже есть в `c/ds18b20` — переносится порт, а не алгоритм |
|
||||
| `1921vk028/drivers/GPIO`, `uart`, `optical`, `Interrupt` | целиком на регистрах `plib028` | переносить стоит не код, а приём: таблица описаний портов вместо `#define`, разбросанных по файлу |
|
||||
| `Drivers/` внутри прошивок | вендорный HAL и CMSIS | ничего, приходит со своим SDK |
|
||||
|
||||
@@ -10,6 +10,9 @@ set(SETPROTOCOL_V2_SOURCES
|
||||
src/set_can.c
|
||||
src/set_firmware.c
|
||||
src/set_telemetry.c
|
||||
src/set_plot.c
|
||||
src/set_trends.c
|
||||
src/set_spectrum.c
|
||||
)
|
||||
|
||||
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
|
||||
@@ -32,6 +35,9 @@ set(SETPROTOCOL_SOURCES
|
||||
|
||||
add_library(setprotocol_static STATIC ${SETPROTOCOL_SOURCES})
|
||||
target_include_directories(setprotocol_static PUBLIC include)
|
||||
if(NOT MSVC)
|
||||
target_link_libraries(setprotocol_static PUBLIC m)
|
||||
endif()
|
||||
|
||||
# Совместимое имя цели для проектов, уже использовавших SET protocol v2.
|
||||
add_library(set_protocol ALIAS setprotocol_static)
|
||||
@@ -40,6 +46,9 @@ option(SETP_BUILD_SHARED "Build stable host ABI shared library" ON)
|
||||
if(SETP_BUILD_SHARED)
|
||||
add_library(setprotocol SHARED ${SETPROTOCOL_SOURCES})
|
||||
target_include_directories(setprotocol PUBLIC include)
|
||||
if(NOT MSVC)
|
||||
target_link_libraries(setprotocol PRIVATE m)
|
||||
endif()
|
||||
target_compile_definitions(setprotocol PRIVATE PCAN_ABI_BUILD_DLL)
|
||||
set_target_properties(setprotocol PROPERTIES
|
||||
C_VISIBILITY_PRESET hidden
|
||||
@@ -66,6 +75,10 @@ option(SETP_BUILD_TESTS "Build host tests" ON)
|
||||
if(SETP_BUILD_TESTS)
|
||||
enable_testing()
|
||||
|
||||
add_executable(test_plot tests/test_plot.c)
|
||||
target_link_libraries(test_plot PRIVATE setprotocol_static)
|
||||
add_test(NAME shared_plot COMMAND test_plot)
|
||||
|
||||
add_executable(test_set_protocol tests/test_set_protocol.c)
|
||||
target_link_libraries(test_set_protocol PRIVATE setprotocol_static)
|
||||
add_test(NAME set_protocol_v2 COMMAND test_set_protocol)
|
||||
@@ -81,4 +94,10 @@ if(SETP_BUILD_TESTS)
|
||||
add_executable(test_abi tests/test_abi.c)
|
||||
target_link_libraries(test_abi PRIVATE setprotocol_static)
|
||||
add_test(NAME stable_abi COMMAND test_abi)
|
||||
add_executable(test_trends tests/test_trends.c)
|
||||
target_link_libraries(test_trends PRIVATE setprotocol_static)
|
||||
add_test(NAME shared_trends COMMAND test_trends)
|
||||
add_executable(test_spectrum tests/test_spectrum.c)
|
||||
target_link_libraries(test_spectrum PRIVATE setprotocol_static)
|
||||
add_test(NAME shared_spectrum COMMAND test_spectrum)
|
||||
endif()
|
||||
|
||||
@@ -36,6 +36,13 @@ multicast запрещены WRITE, FW_BEGIN и FW_ACTIVATE.
|
||||
|
||||
## CAN
|
||||
|
||||
Для существующего прикладного ProtoCAN доступен
|
||||
[порт STM32 bxCAN](ports/stm32-bxcan/README.md), перенесённый из SETCAN.
|
||||
Он сохраняет API `PROTOCAN_*`, использует HAL CAN/RTC/TIM и общий `pcan_id`.
|
||||
Порт рассчитан на classic bxCAN; он не реализует сегментацию SETProtocol v2.
|
||||
В проект устройства добавляются `ports/stm32-bxcan/protocan.c` и `src/pcan_id.c`,
|
||||
а в include paths — `ports/stm32-bxcan` и `include`.
|
||||
|
||||
Не вызывайте прикладной router из ISR. ISR принимает CAN-кадры в очередь,
|
||||
сегментация собирается в главном цикле, и только полный SETP-кадр передаётся
|
||||
parser/router. Сборка должна иметь timeout, контроль номера сегмента и одного
|
||||
|
||||
@@ -7,12 +7,20 @@
|
||||
- совместимый ProtoCAN transport для существующих CAN-мостов;
|
||||
- SETGUI transport v1 для приборов переходного периода;
|
||||
- стабильный host ABI для Python/JNI/других FFI;
|
||||
- GAS, кольцевые буферы, CRC и потоковые parser.
|
||||
- GAS, кольцевые буферы, CRC и потоковые parser;
|
||||
- host-анализ спектра: FFT, окна, фильтры и адаптеры JNI/ctypes.
|
||||
|
||||
Ядро не зависит от HAL, ОС или конкретного адаптера, не использует
|
||||
Протокольное ядро не зависит от HAL, ОС или конкретного адаптера, не использует
|
||||
динамическую память и не открывает COM/CAN само. SLCAN, SocketCAN, USB CDC,
|
||||
TCP, UART/DMA и аппаратный CAN подключаются портами.
|
||||
|
||||
Отдельный host-модуль `set_spectrum.c` использует ограниченный временный буфер
|
||||
до 2×16384 `double`; он не нужен для wire-протокола/MCU.
|
||||
Контракт и использование из обоих GUI: [GUI_SPECTRUM.md](docs/GUI_SPECTRUM.md).
|
||||
|
||||
Масштабирование, координаты и измерительные маркеры Android/SETGUI используют
|
||||
общий `set_plot.c`: [границы модулей, ABI и проверки](docs/GUI_PLOT.md).
|
||||
|
||||
## Структура
|
||||
|
||||
| Каталог | Назначение |
|
||||
@@ -24,6 +32,7 @@ TCP, UART/DMA и аппаратный CAN подключаются портам
|
||||
| `src/` | общая реализация всех протокольных модулей |
|
||||
| `ports/android` | JNI и Kotlin facade |
|
||||
| `ports/stm32f4` | UART/DMA port для legacy byte stream |
|
||||
| [`ports/stm32-bxcan`](ports/stm32-bxcan/README.md) | STM32 HAL bxCAN/RTC/TIM, прикладной ProtoCAN; бывший отдельный SETCAN |
|
||||
| `docs/SETPROTOCOL.md` | архитектура, ABI, память и переносимость |
|
||||
| `docs/legacy` | нормативные документы ProtoCAN/SETGUI v1 |
|
||||
| `PROTOCOL.md` | нормативный wire contract SET protocol v2 |
|
||||
|
||||
80
c/set-protocol/docs/GUI_PLOT.md
Normal file
80
c/set-protocol/docs/GUI_PLOT.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Общая работа с графиками Android и SETGUI
|
||||
|
||||
Численная логика находится в `include/set_plot.h` и `src/set_plot.c`.
|
||||
Это модуль C99 без Qt, Android, транспорта, динамической памяти и глобального
|
||||
состояния. Он собирается в существующую библиотеку SETProtocol; отдельная DLL
|
||||
для графиков не требуется. Версия ABI графиков — `set_plot_abi_version() == 1`.
|
||||
|
||||
| Общее в templates | Адаптер приложения |
|
||||
|---|---|
|
||||
| Нормализованная область просмотра, масштаб 1…128, ограничение панорамирования | Размер поля и отступы в пикселях |
|
||||
| Независимый масштаб X/Y с сохранением координаты под пальцами | События Compose или Qt, колесо, кнопки |
|
||||
| Выбор преобладающей оси по изменению расстояния между пальцами | Накопление движения до порога; фиксация выбранной оси до конца жеста |
|
||||
| Перевод значения в долю экрана и обратно, инверсия Y | Canvas/QPainter и оформление шкал |
|
||||
| Перемещение маркера от начальной координаты, ограничение видимой областью | Захват линии пальцем/мышью, редактор положения |
|
||||
| Разность B−A, D−C и множитель единиц, шаги шкалы 1/2/5 | Подписи, цвета, миллисекунды/герцы/единицы сигнала |
|
||||
| Модели маркеров и их размещение в Kotlin/Python-портах | Жизненный цикл экрана, очистка и выбор источника |
|
||||
|
||||
Модель маркеров: A/B — координаты X и вертикальные линии во всю высоту поля;
|
||||
C/D — координаты Y и горизонтальные линии. Они могут пересекаться: отрицательная
|
||||
разность сохраняется. Начальные координаты — 1/3 и 2/3 диапазона. При изменении
|
||||
масштаба уже установленные координаты сохраняются. «Маркеры в видимую область»
|
||||
заново размещает их. Координаты и область просмотра времени/спектра независимы.
|
||||
|
||||
`set_plot_eval()` принимает операцию из `enum set_plot_operation`, точное число
|
||||
входных `double` и буфер вывода. Контракт каждой операции указан в заголовке.
|
||||
Возвращает число записанных элементов, 0 — неправильные аргументы. Некорректный
|
||||
коэффициент масштабирования оставляет корректный viewport неизменным.
|
||||
NaN/Inf и вырожденные диапазоны не передаются дальше в отрисовку.
|
||||
FRACTION/VALUE не обрезают координаты: это позволяет правильно отсекать линии
|
||||
по границе поля, а не создавать ложные горизонтальные участки на его краю.
|
||||
DRAG, напротив, ограничивает положение маркера видимым диапазоном.
|
||||
|
||||
## Порты
|
||||
|
||||
- Android/JVM: `ports/android/kotlin/ru/setcorp/setprotocol/trends/NativePlot.kt`,
|
||||
`PlotViewport.kt`, `TrendMarkers.kt`; тонкий JNI — `ports/android/set_plot_jni.c`.
|
||||
Android использует `System.loadLibrary("setprotocol")`. Для JVM-тестов можно
|
||||
передать абсолютный путь DLL/SO системным свойством `setplot.library`.
|
||||
- Python: `python/protocan/plot.py`: `PlotMath(library)`, `Viewport`, `Bounds`,
|
||||
`Markers`. Приложение передаёт уже загруженную SETProtocol `ctypes.CDLL`.
|
||||
Зависимости — стандартная библиотека; Qt в этот модуль не импортируется.
|
||||
|
||||
Новые изменения не должны добавлять вторую реализацию численных операций
|
||||
в Kotlin/Python или в приложение. Состояние и адаптация событий остаются в портах.
|
||||
Добавление операции требует обновления C ABI, обоих портов и общих эталонов.
|
||||
|
||||
## Интеграция приложений
|
||||
|
||||
Android: `TrendGraph` использует общие viewport, bounds и маркеры, а Compose
|
||||
рисует линии и панель. При измерениях время отображается в миллисекундах от
|
||||
начала выбранного участка; сохранённые X-координаты включают абсолютное начало.
|
||||
|
||||
SETGUI: `ui/plot_interaction.py` адаптирует общий модуль к Qt. Его используют
|
||||
`SignalPlot` (ТМС и датчики), `TrendPlot` (логи) и `SpectrumPlot`.
|
||||
Колесо по вертикали масштабирует Y, Shift+колесо и горизонтальное колесо — X;
|
||||
сенсорный pinch выбирает ось по движению. Временные графики хранят метку приёма
|
||||
каждого отсчёта. Пропущенные значения не растягивают остальные серии:
|
||||
все сигналы имеют единую шкалу времени. При повторном построении ТМС из журнала
|
||||
используются исходные метки приёма. Дискретные дорожки имеют общую X-шкалу,
|
||||
а маркеры уровня Y относятся к аналоговому полю.
|
||||
|
||||
Расчёт FFT не является частью этого модуля. Android вычисляет спектр через
|
||||
`set_spectrum.c`; вкладка спектра SETGUI получает готовые уровни в дБмВ от
|
||||
прибора. Общая интерактивная часть не меняет эти данные или единицы.
|
||||
|
||||
## Проверка и сборка
|
||||
|
||||
- CMake/CTest: `shared_plot` проверяет ABI, размеры буферов и координаты.
|
||||
- `tests/fixtures/plot-v1.json` содержит общие численные эталоны. Их читают
|
||||
`PlotContractTest` (фактический JNI) и `python/tests/test_plot.py` (ctypes).
|
||||
- Host DLL/SO для Python: `python tools/build_host.py --output <путь>`.
|
||||
- Для JVM: та же команда с `--java-home <JDK>` добавляет JNI-порт графиков.
|
||||
Нужен C-компилятор (MSVC на Windows, cc/clang/gcc на Unix) и JDK с `jni.h`.
|
||||
- Android-проект автоматически собирает host-библиотеку перед JVM-тестами;
|
||||
интерпретатор Python можно задать свойством Gradle `setflash.python`.
|
||||
- SETGUI проверяет события мыши, колеса и двух пальцев через QtTest без
|
||||
физического прибора; численные тесты не требуют GUI.
|
||||
|
||||
Исходники должны приходить из одного коммита подмодуля `templates` в обоих
|
||||
проектах. Локальная замена файлов без обновления gitlink не закрепляет версию.
|
||||
53
c/set-protocol/docs/GUI_SPECTRUM.md
Normal file
53
c/set-protocol/docs/GUI_SPECTRUM.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Shared GUI spectrum
|
||||
|
||||
`include/set_spectrum.h` / `src/set_spectrum.c` implement the same numerical
|
||||
pipeline for desktop and Android. CMake, `tools/build_host.py`, and the Android
|
||||
NDK build include it. The wire protocol is unchanged. The host-only analyzer
|
||||
allocates at most two FFT arrays (2×N doubles), releasing them before return.
|
||||
|
||||
The last N samples are used: the largest power of two within available data and
|
||||
`max_size`, 16…16384. Timestamps are seconds; inputs are immutable. Fs is measured
|
||||
from the selected endpoints. Non-monotonic/duplicate times and interval deviations
|
||||
over 50% of the mean are rejected. Lesser jitter is linearly interpolated onto an
|
||||
even grid (this can affect high-frequency amplitude; inspect returned jitter).
|
||||
|
||||
Processing order: interpolation, optional mean subtraction, filter, periodic
|
||||
window, forward FFT, one-sided peak amplitude. Windows: rectangular, Hann,
|
||||
Hamming, Blackman, Flat Top. Amplitudes are divided by window sum, doubled except
|
||||
DC and Nyquist. Output is not RMS/PSD/dB. Coherent gain correction is exact for
|
||||
bin-centered tones away from DC/Nyquist; off-bin tones still have scalloping.
|
||||
|
||||
Filters run before the window, on the unmodified sample copy. LP/HP are second
|
||||
order Butterworth; band pass cascades HP and LP; notch uses Q=30.
|
||||
Coefficients: [W3C Audio EQ Cookbook](https://www.w3.org/TR/audio-eq-cookbook/).
|
||||
Initial state is the first sample's steady state, not a carried streaming state;
|
||||
block-start transients may occur. Cutoffs must be strictly within (0, Fs/2).
|
||||
|
||||
## Python
|
||||
|
||||
```python
|
||||
from protocan.spectrum import NativeSpectrum, Window, Filter
|
||||
|
||||
# lib = an existing ctypes.CDLL from protocan.native.NativeProtocol
|
||||
spectrum = NativeSpectrum(lib).analyze(times_seconds, values,
|
||||
window=Window.HANN, filter=Filter.LOW_PASS, high_hz=100,
|
||||
max_size=4096, remove_mean=True)
|
||||
plot(spectrum.frequencies, spectrum.amplitudes)
|
||||
```
|
||||
|
||||
Adapter errors are ValueError, not empty/misleading arrays. No NumPy/Qt dependency.
|
||||
|
||||
## Android
|
||||
|
||||
`SpectrumAnalyzer.analyze(List<TrendPoint>, SpectrumOptions)` calls JNI using
|
||||
relative seconds from millisecond timestamps. `TrendSpectrum` contains N, Fs,
|
||||
maximum relative jitter, amplitudes or a displayable error. Call on a worker
|
||||
dispatcher. `PlotViewport` provides axis-independent zoom/pan, focus anchoring,
|
||||
128× limit and bounds clamping without any Compose dependency.
|
||||
|
||||
## Checks
|
||||
|
||||
Build CMake with tests and run CTest in Debug (assertions enabled). Python
|
||||
`tests/test_spectrum.py` runs against an explicitly supplied `SETPROTOCOL_LIBRARY`:
|
||||
independent direct DFT, amplitude correction, DC/Nyquist, off-bin leakage, filter
|
||||
bands, timing rejection/interpolation, bounded tail selection and immutable inputs.
|
||||
78
c/set-protocol/docs/GUI_TRENDS.md
Normal file
78
c/set-protocol/docs/GUI_TRENDS.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Общие тренды для Android GUI и SETGUI
|
||||
|
||||
Модуль задаёт совместимые настройки и числовые источники графиков. Он не
|
||||
рисует экран, не открывает файлы или порты и не управляет прошивкой устройства.
|
||||
|
||||
Слои: GUI → Kotlin/Python adapter → `set_trends.c` → уже разобранные кадры.
|
||||
Транспорт, таймер опроса, выбор файла, хранение настроек и Canvas/Qt остаются
|
||||
в приложении. У ядра нет callbacks, динамической памяти или глобального состояния.
|
||||
|
||||
| Файл | Назначение и зависимости |
|
||||
|---|---|
|
||||
| `include/set_trends.h`, `src/set_trends.c` | C99: фильтрация GAS/raw CAN, signed/unsigned word, payload подписки SET GUI; использует `pcan_id` и ABI export macro |
|
||||
| `ports/android/kotlin/ru/setcorp/setprotocol/trends/` | Модель, валидация JSON, ограниченная история, GAS_WATCH; JVM + org.json, без Android/Compose |
|
||||
| `ports/android/setprotocol_jni.c` | Только преобразование JNI-аргументов |
|
||||
| `python/protocan/trends.py` | Модель, JSON, история и `NativeTrends`; Python 3.9+, stdlib, без Qt |
|
||||
| `tests/fixtures/trends-v1.json` | Один образец для тестов обоих GUI и обмена настройками |
|
||||
|
||||
## Формат файла
|
||||
|
||||
UTF-8 JSON: `format = "setflash-trends"`, `version = 1`, `profiles` — словарь.
|
||||
Ключи профилей стабильны: `TMS2812`, `SET_V1`, `CAN_BRIDGE`, `GS_USB_CAN`,
|
||||
`SLCAN`, `CANGAROO_SLCAN`, `BALZAM_CAN`. Приложение явно сопоставляет своё
|
||||
подключение с этим ключом; импорт не меняет подключение.
|
||||
|
||||
Поля сигнала обязательны: `id`, `order`, `name`, `source`, `address`, `color`,
|
||||
`visible`, `valueType`, `deviceType`, `device`, `byteOffset`, `extended`.
|
||||
`id` уникален во всём файле. `order` — 1…9999, уникален внутри профиля.
|
||||
`name` — непустое изменяемое имя (до 100 символов), `color` — `#RRGGBB`.
|
||||
`visible` управляет только отрисовкой, не сбором. История и соединение в файл
|
||||
не входят. Максимум 64 сигнала на профиль и 1 МиБ на файл. Неизвестные версии,
|
||||
профили, источники и неверные значения отклоняются до замены настроек.
|
||||
|
||||
| Профиль / источник | Адрес и значение |
|
||||
|---|---|
|
||||
| TMS2812 / TMS_MEMORY | 32-битный адрес слова, CMD_PEEK; контроллер из подключения |
|
||||
| SET_V1 / SET_GAS | 16-битный GAS; GAS_WATCH_SET `0x12`, DATA `0x13` |
|
||||
| SET_V1 / SET_SENSOR | ROM DS18B20, 16 HEX-цифр (дефисы допустимы), °C |
|
||||
| Современные CAN / CAN_GAS | GAS 0…65535, DeviceType 0…7, DeviceID 0…15 |
|
||||
| Все CAN / CAN_RAW | CAN ID, STD/EXT, byteOffset 0…6; слово Little-endian |
|
||||
|
||||
Числовые адреса — десятичные либо HEX с `0x`. Регистры трактуются как
|
||||
`UINT16` или `INT16`. Для RAW/CAN_GAS C-функция возвращает значение либо
|
||||
`SET_TREND_NO_VALUE`, никогда не подставляет ноль вместо отсутствующего кадра.
|
||||
|
||||
CAN — пассивный приём, без записи GAS и автоматической отправки запросов.
|
||||
GAS принимает только входящие FROM_DEVICE; TX/RTR/ошибки исключены.
|
||||
SET GUI оформляет подписку в порядке адресов. ACK должен подтвердить весь
|
||||
список: при частичном принятии нельзя определить пропущенные адреса, поэтому
|
||||
строить график по смещённым индексам запрещено. При паузе порт приложения
|
||||
отправляет подписку с периодом 0, когда транспорт доступен и не занят прошивкой.
|
||||
|
||||
## Быстрый старт SETGUI
|
||||
|
||||
```python
|
||||
from protocan.native import NativeProtocol
|
||||
from protocan.trends import TrendSignal, NativeTrends, encode_settings
|
||||
|
||||
core = NativeProtocol() # библиотека, пересобранная с set_trends.c
|
||||
decoder = NativeTrends(core.lib)
|
||||
signal = TrendSignal("current", source="CAN_GAS", address="0x1235")
|
||||
text = encode_settings({"CAN_BRIDGE": [signal]})
|
||||
value = decoder.can_value(signal, 0x1FD31234, 1, bytes([1, 0, 254, 255]))
|
||||
assert value == 65534.0
|
||||
# GUI сохраняет text выбранным способом и передаёт value своему графику.
|
||||
```
|
||||
|
||||
Android подключает Kotlin-каталог напрямую через Gradle и C-файлы через NDK.
|
||||
Приложение SETGUI может импортировать модуль через уже используемый путь
|
||||
`third_party/templates/python`; в этом изменении его экран не изменяется.
|
||||
После публикации новой ревизии templates оба проекта должны обновить сабмодуль
|
||||
и пересобрать собственные native-библиотеки — вручную копировать реализации не нужно.
|
||||
|
||||
## Проверки
|
||||
|
||||
- CMake/CTest: `shared_trends`, плюс существующие protocol/ABI тесты.
|
||||
- JVM: `TrendSettingsJsonTest`, `GuiGasWatchTest`; один JSON fixture с Python.
|
||||
- Python: `PYTHONPATH=python python -m unittest discover -s python/tests`.
|
||||
Для проверки настоящего C-декодера задайте `SETPROTOCOL_LIBRARY` путём DLL/SO.
|
||||
@@ -48,7 +48,7 @@ uint16_t n = pcan_gas_read_block(&map, 0x0000, block, 4);
|
||||
Тип сообщения `PCAN_MSG_GAS` (`0b0011`). Адрес первого регистра лежит
|
||||
в `MsgBody`, данные — до 4 регистров подряд, младшим байтом вперёд.
|
||||
Это ровно то, что делает `PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()`
|
||||
в `SETCAN/Src/protocan.c`, поэтому обмен совместим с существующими
|
||||
в `ports/stm32-bxcan/protocan.c` (бывший SETCAN), поэтому обмен совместим с существующими
|
||||
устройствами.
|
||||
|
||||
| Кадр | DLC | Смысл |
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* Транспорт ProtoCAN (MsgType = 0b0011) кладёт в MsgBody адрес первого
|
||||
* регистра, а в данные - до 4 регистров подряд, младшим байтом вперёд.
|
||||
* Это ровно то, что делает PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()
|
||||
* в SETCAN/Src/protocan.c, поэтому обмен совместим с существующими
|
||||
* в ports/stm32-bxcan/protocan.c, поэтому обмен совместим с существующими
|
||||
* устройствами.
|
||||
*
|
||||
* Запрос на чтение кодируется кадром GAS с DLC = 0: в исходном коде
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @file pcan_id.h
|
||||
* @brief Упаковка и разбор 29-битного идентификатора ProtoCAN.
|
||||
*
|
||||
* Раскладка (соответствует ProtoCanId_t из SETCAN/Inc/protocan.h):
|
||||
* Раскладка (ProtoCanId_t из ports/stm32-bxcan/protocan.h, бывший SETCAN):
|
||||
*
|
||||
* биты 28 Priority
|
||||
* 27 Route
|
||||
|
||||
35
c/set-protocol/include/set_plot.h
Normal file
35
c/set-protocol/include/set_plot.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/** @file set_plot.h
|
||||
* @brief Toolkit-independent plot interaction math shared by JNI and ctypes.
|
||||
* Coordinates are doubles in the caller's units. No allocation or global state.
|
||||
*/
|
||||
#ifndef SET_PLOT_H
|
||||
#define SET_PLOT_H
|
||||
#include "pcan_abi.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
enum set_plot_operation {
|
||||
SET_PLOT_TRANSFORM = 0, /* x,y,w,h, zoomX,zoomY, panX,panY, focusX,focusY -> x,y,w,h */
|
||||
SET_PLOT_AXIS = 1, /* deltaX,deltaY,slop -> 0=none, 1=X, 2=Y */
|
||||
SET_PLOT_FRACTION = 2, /* value,low,high,inverted -> fraction (not clamped) */
|
||||
SET_PLOT_VALUE = 3, /* fraction,low,high,inverted -> value (not clamped) */
|
||||
SET_PLOT_DRAG = 4, /* initial,deltaPixels,length,low,high,inverted -> clamped value */
|
||||
SET_PLOT_TICK_STEP = 5, /* range,lengthPixels -> nice step */
|
||||
SET_PLOT_DELTA = 6 /* A,B,multiplier -> (B-A)*multiplier */
|
||||
};
|
||||
|
||||
/** Version of this plot ABI, independently of the transport ABI. */
|
||||
PCAN_ABI_API uint32_t set_plot_abi_version(void);
|
||||
/** Evaluates one operation. Returns output count, or 0 for invalid arguments.
|
||||
* TRANSFORM rejects malformed viewports; invalid gesture values return the
|
||||
* unchanged viewport. Zoom is restricted to 1..128, with focus anchoring.
|
||||
* Screen fractions increase downwards; set inverted=1 for the Y value axis.
|
||||
*/
|
||||
PCAN_ABI_API size_t set_plot_eval(uint32_t operation, const double *input,
|
||||
size_t count, double *output, size_t capacity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
34
c/set-protocol/include/set_spectrum.h
Normal file
34
c/set-protocol/include/set_spectrum.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/** Shared host-side FFT for Android/JNI and desktop/ctypes; no GUI dependencies. */
|
||||
#ifndef SET_SPECTRUM_H
|
||||
#define SET_SPECTRUM_H
|
||||
#include "pcan_abi.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define SET_SPECTRUM_MAX 16384U
|
||||
enum { SET_WINDOW_RECT, SET_WINDOW_HANN, SET_WINDOW_HAMMING, SET_WINDOW_BLACKMAN, SET_WINDOW_FLATTOP };
|
||||
enum { SET_FILTER_NONE, SET_FILTER_LOW_PASS, SET_FILTER_HIGH_PASS, SET_FILTER_BAND_PASS, SET_FILTER_NOTCH };
|
||||
enum { SET_SPECTRUM_OK, SET_SPECTRUM_SHORT, SET_SPECTRUM_INVALID, SET_SPECTRUM_TIMING,
|
||||
SET_SPECTRUM_CUTOFF, SET_SPECTRUM_MEMORY };
|
||||
|
||||
/** Analyze the last power-of-two block (16 <= N <= max_size).
|
||||
* times are seconds, strictly increasing; values must be finite. Fs is measured
|
||||
* from timestamps, never taken from the requested acquisition rate. Interval
|
||||
* deviations >50% of the mean are rejected; smaller jitter is linearly resampled.
|
||||
* Filter: second-order Butterworth LP/HP, their cascade for band pass, notch Q=30.
|
||||
* low_hz: HP/band lower cutoff or notch center; high_hz: LP/band upper cutoff.
|
||||
* Each block starts the causal filters at steady state for its first sample.
|
||||
* Processing order: resample, optional mean removal, filter, periodic window, FFT.
|
||||
* Output: one-sided PEAK amplitude, normalized by window sum, DC/Nyquist not doubled.
|
||||
* Caller supplies max_size/2+1 amplitudes; meta = {N, Fs, max relative jitter}.
|
||||
* Inputs are immutable. Scratch memory is bounded by 2*N doubles, allocated here.
|
||||
*/
|
||||
PCAN_ABI_API int set_spectrum_analyze(const double *times, const double *values, size_t count,
|
||||
size_t max_size, int window, int filter, double low_hz, double high_hz, int remove_mean,
|
||||
double *amplitudes, size_t capacity, double *meta);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
54
c/set-protocol/include/set_trends.h
Normal file
54
c/set-protocol/include/set_trends.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/** @file set_trends.h
|
||||
* @brief Shared GUI trend decoding and GUI v1 GAS subscription payloads.
|
||||
* No transport, rendering, allocation or global state. JNI/ctypes call the
|
||||
* same exported functions; flags use the existing AA55 bridge ABI.
|
||||
*/
|
||||
#ifndef SET_TRENDS_H
|
||||
#define SET_TRENDS_H
|
||||
#include "pcan_abi.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define SET_TREND_CAN_GAS 1U
|
||||
#define SET_TREND_CAN_RAW 2U
|
||||
#define SET_TREND_NO_VALUE INT32_MIN
|
||||
#define SET_TREND_WATCH_MAX 64U
|
||||
|
||||
/** Convert one wire word, without implementation-defined signed casts. */
|
||||
PCAN_ABI_API int32_t set_trend_word_value(uint16_t word, uint8_t is_signed);
|
||||
|
||||
/** Return a decoded value or SET_TREND_NO_VALUE for a nonmatching/invalid frame.
|
||||
* GAS requires Route=FROM_DEVICE, exact DeviceType/DeviceID, and an even DLC.
|
||||
* TX echoes, RTR and error frames are always ignored. RAW offset is in bytes.
|
||||
*/
|
||||
PCAN_ABI_API int32_t set_trend_can_value(
|
||||
uint8_t source, uint32_t address, uint8_t device_type, uint8_t device,
|
||||
uint8_t byte_offset, uint8_t extended, uint8_t is_signed,
|
||||
uint32_t can_id, uint8_t flags, const uint8_t *data, size_t size);
|
||||
|
||||
/** Encode period:u16 + count:u16 + addresses[count]:u16, little-endian.
|
||||
* A zero period/count is an unsubscribe. Returns 0 on invalid arguments.
|
||||
*/
|
||||
PCAN_ABI_API size_t set_trend_watch_request(
|
||||
uint16_t period_ms, const uint16_t *addresses, size_t count,
|
||||
uint8_t *output, size_t capacity);
|
||||
|
||||
/** Accept only an exact acknowledgement of the entire ordered subscription.
|
||||
* The device may omit unknown addresses. Partial acceptance must NOT be used:
|
||||
* its acknowledgement has no address list, so index-to-address mapping is lost.
|
||||
*/
|
||||
PCAN_ABI_API int set_trend_watch_ack(
|
||||
const uint8_t *payload, size_t size, uint16_t period_ms, size_t count);
|
||||
|
||||
/** Decode timestamp:u32 + count:u16 + words. Returns count, or -1 on error.
|
||||
* Timestamp is device-local; GUI plot clocks should use their own monotonic time.
|
||||
*/
|
||||
PCAN_ABI_API int set_trend_watch_values(
|
||||
const uint8_t *payload, size_t size, uint16_t *words, size_t capacity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -8,6 +8,10 @@ LOCAL_SRC_FILES := \
|
||||
../../src/set_can.c \
|
||||
../../src/set_firmware.c \
|
||||
../../src/set_telemetry.c \
|
||||
../../src/set_plot.c \
|
||||
set_plot_jni.c \
|
||||
../../src/set_trends.c \
|
||||
../../src/set_spectrum.c \
|
||||
../../src/gui_catalog.c \
|
||||
../../src/gui_frame.c \
|
||||
../../src/pcan_abi.c \
|
||||
@@ -19,5 +23,5 @@ LOCAL_SRC_FILES := \
|
||||
../../src/pcan_gas.c \
|
||||
setprotocol_jni.c
|
||||
LOCAL_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden
|
||||
LOCAL_LDLIBS := -llog
|
||||
LOCAL_LDLIBS := -llog -lm
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
|
||||
@@ -7,4 +7,9 @@ and include this `Android.mk` from the application NDK build.
|
||||
|
||||
The JNI streaming parser returns fixed 15-byte records
|
||||
`SEQ | FLAGS | CAN_ID_LE | DLC | DATA[8]`. Dynamic allocation is confined to
|
||||
the Android adapter; the portable core remains allocation free.
|
||||
the Android adapter for protocol operations; the portable protocol core remains allocation free.
|
||||
|
||||
`nativeSpectrum` calls the shared host DSP module `set_spectrum.c` (bounded heap
|
||||
workspace, up to 2×16384 doubles). `trends/SpectrumAnalyzer` maps timestamps and
|
||||
errors but does not duplicate FFT/filter math. Run it off the UI thread.
|
||||
`trends/PlotViewport` is a toolkit-free normalized zoom/pan model.
|
||||
|
||||
@@ -10,6 +10,9 @@ object MessageType {
|
||||
const val FIRMWARE_END = 0x0D
|
||||
const val FIRMWARE_ABORT = 0x0E
|
||||
const val FIRMWARE_STATUS = 0x0F
|
||||
const val GAS_CATALOG = 0x11
|
||||
const val GAS_WATCH_SET = 0x12
|
||||
const val GAS_WATCH_DATA = 0x13
|
||||
const val SENSOR_SCAN = 0x20
|
||||
const val SENSOR_LIST = 0x21
|
||||
const val SENSOR_READ = 0x22
|
||||
@@ -33,6 +36,9 @@ object MessageType {
|
||||
FIRMWARE_END -> "FIRMWARE_END"
|
||||
FIRMWARE_ABORT -> "FIRMWARE_ABORT"
|
||||
FIRMWARE_STATUS -> "FIRMWARE_STATUS"
|
||||
GAS_CATALOG -> "GAS_CATALOG"
|
||||
GAS_WATCH_SET -> "GAS_WATCH_SET"
|
||||
GAS_WATCH_DATA -> "GAS_WATCH_DATA"
|
||||
SENSOR_SCAN -> "SENSOR_SCAN"
|
||||
SENSOR_LIST -> "SENSOR_LIST"
|
||||
SENSOR_READ -> "SENSOR_READ"
|
||||
|
||||
@@ -10,6 +10,15 @@ object NativeSetProtocol {
|
||||
}
|
||||
|
||||
external fun nativeAbiVersion(): Int
|
||||
/** {status, N, Fs, jitter, amplitudes...}; status != 0 has no amplitudes. */
|
||||
external fun nativeSpectrum(times: DoubleArray, values: DoubleArray, maxSize: Int,
|
||||
window: Int, filter: Int, lowHz: Double, highHz: Double, removeMean: Boolean): DoubleArray?
|
||||
external fun nativeTrendCanValue(
|
||||
source: Int, address: Long, deviceType: Int, device: Int, byteOffset: Int,
|
||||
extended: Boolean, signed: Boolean, canId: Long, flags: Int, input: ByteArray,
|
||||
): Int
|
||||
external fun nativeTrendWatchRequest(period: Int, addresses: IntArray): ByteArray?
|
||||
external fun nativeTrendWatchValues(input: ByteArray): IntArray?
|
||||
external fun nativePackId(
|
||||
priority: Int,
|
||||
route: Int,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
import ru.setcorp.setprotocol.NativeSetProtocol
|
||||
|
||||
/** GUI v1 GAS contract, also exposed by python/protocan/gas_catalog.py. */
|
||||
object GuiGasWatch {
|
||||
const val SET = 0x12
|
||||
const val DATA = 0x13
|
||||
const val PERIOD_MS = 1000
|
||||
|
||||
fun request(periodMs: Int, addresses: List<Int>): ByteArray {
|
||||
require(periodMs in 0..65535 && addresses.size <= 64 && addresses.all { it in 0..65535 })
|
||||
if (NativeSetProtocol.available) return requireNotNull(NativeSetProtocol.nativeTrendWatchRequest(periodMs, addresses.toIntArray()))
|
||||
return ByteArray(4 + addresses.size * 2).also { output ->
|
||||
put16(output, 0, periodMs)
|
||||
put16(output, 2, addresses.size)
|
||||
addresses.forEachIndexed { index, address -> put16(output, 4 + index * 2, address) }
|
||||
}
|
||||
}
|
||||
|
||||
fun validateAck(payload: ByteArray, periodMs: Int, count: Int) {
|
||||
require(payload.size == 4 && read16(payload, 0) == periodMs && read16(payload, 2) == count) {
|
||||
"Прибор принял не все адреса GAS. Проверьте карту регистров; отображение по неполной подписке невозможно"
|
||||
}
|
||||
}
|
||||
|
||||
fun values(payload: ByteArray, expectedCount: Int): List<Int> {
|
||||
val values = if (NativeSetProtocol.available) {
|
||||
requireNotNull(NativeSetProtocol.nativeTrendWatchValues(payload)) { "Повреждён GAS_WATCH_DATA" }.toList()
|
||||
} else {
|
||||
require(payload.size >= 6) { "GAS_WATCH_DATA короче заголовка" }
|
||||
val count = read16(payload, 4)
|
||||
require(count <= 64 && payload.size == 6 + count * 2) { "Неверная длина GAS_WATCH_DATA" }
|
||||
List(count) { read16(payload, 6 + it * 2) }
|
||||
}
|
||||
require(values.size == expectedCount) { "Число значений GAS не соответствует подписке" }
|
||||
return values
|
||||
}
|
||||
|
||||
private fun read16(data: ByteArray, offset: Int): Int =
|
||||
(data[offset].toInt() and 0xFF) or ((data[offset + 1].toInt() and 0xFF) shl 8)
|
||||
private fun put16(data: ByteArray, offset: Int, value: Int) {
|
||||
data[offset] = value.toByte()
|
||||
data[offset + 1] = (value ushr 8).toByte()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
/** Shared C99 plot math, also used by the Python/Qt port. No UI dependency. */
|
||||
internal object NativePlot {
|
||||
init {
|
||||
val hostLibrary = System.getProperty("setplot.library")
|
||||
if (hostLibrary != null) System.load(hostLibrary) else System.loadLibrary("setprotocol")
|
||||
}
|
||||
private external fun evaluate(operation: Int, input: DoubleArray): DoubleArray?
|
||||
fun call(operation: Int, vararg input: Double): DoubleArray =
|
||||
requireNotNull(evaluate(operation, input)) { "Invalid plot operation $operation" }
|
||||
}
|
||||
|
||||
enum class PlotAxis { X, Y }
|
||||
|
||||
fun plotPinchAxis(deltaX: Double, deltaY: Double, slop: Double): PlotAxis? =
|
||||
when (NativePlot.call(1, deltaX, deltaY, slop)[0].toInt()) {
|
||||
1 -> PlotAxis.X; 2 -> PlotAxis.Y; else -> null
|
||||
}
|
||||
|
||||
data class PlotBounds(val left: Double, val right: Double, val bottom: Double, val top: Double) {
|
||||
fun fraction(value: Double, horizontal: Boolean): Double = NativePlot.call(2, value,
|
||||
if (horizontal) bottom else left, if (horizontal) top else right, if (horizontal) 1.0 else 0.0)[0]
|
||||
fun value(fraction: Double, horizontal: Boolean): Double = NativePlot.call(3, fraction,
|
||||
if (horizontal) bottom else left, if (horizontal) top else right, if (horizontal) 1.0 else 0.0)[0]
|
||||
fun drag(initial: Double, delta: Double, length: Double, horizontal: Boolean): Double = NativePlot.call(4,
|
||||
initial, delta, length, if (horizontal) bottom else left, if (horizontal) top else right,
|
||||
if (horizontal) 1.0 else 0.0)[0]
|
||||
}
|
||||
|
||||
fun plotTickStep(range: Double, pixels: Double): Double = NativePlot.call(5, range, pixels)[0]
|
||||
fun plotDelta(a: Double, b: Double, multiplier: Double = 1.0): Double = NativePlot.call(6, a, b, multiplier)[0]
|
||||
@@ -0,0 +1,10 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
/** Normalized top-left viewport; independent of pixels, units, toolkit and samples. */
|
||||
data class PlotViewport(val x: Double = 0.0, val y: Double = 0.0, val width: Double = 1.0, val height: Double = 1.0) {
|
||||
fun transform(zoomX: Double = 1.0, zoomY: Double = 1.0, panX: Double = 0.0, panY: Double = 0.0,
|
||||
focusX: Double = 0.5, focusY: Double = 0.5): PlotViewport {
|
||||
val result = NativePlot.call(0, x, y, width, height, zoomX, zoomY, panX, panY, focusX, focusY)
|
||||
return PlotViewport(result[0], result[1], result[2], result[3])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
import ru.setcorp.setprotocol.NativeSetProtocol
|
||||
|
||||
/** Shared JNI decoder with a JVM-only reference fallback for tests/source builds. */
|
||||
object TrendDecoder {
|
||||
fun canValue(signal: TrendSignal, canId: Long, flags: Int, data: ByteArray): Double? {
|
||||
if (NativeSetProtocol.available && signal.source in setOf(TrendSource.CAN_GAS, TrendSource.CAN_RAW)) {
|
||||
val address = parseTrendAddress(signal.address, if (signal.source == TrendSource.CAN_GAS) 0xFFFF else 0x1FFF_FFFF) ?: return null
|
||||
val value = NativeSetProtocol.nativeTrendCanValue(
|
||||
if (signal.source == TrendSource.CAN_GAS) 1 else 2, address, signal.deviceType, signal.device,
|
||||
signal.byteOffset, signal.extended, signal.valueType == TrendValueType.INT16,
|
||||
canId, flags, data,
|
||||
)
|
||||
return value.takeIf { it != Int.MIN_VALUE }?.toDouble()
|
||||
}
|
||||
if (flags and 0x0E != 0 || data.size !in 2..8) return null
|
||||
val extended = flags and 1 != 0
|
||||
if (canId !in 0..(if (extended) 0x1FFF_FFFFL else 0x7FFL)) return null
|
||||
val offset = when (signal.source) {
|
||||
TrendSource.CAN_GAS -> {
|
||||
if (!extended || data.size < 2 || data.size % 2 != 0) return null
|
||||
if ((canId ushr 16) and 15L != 3L || (canId ushr 27) and 1L != 1L ||
|
||||
((canId ushr 24) and 7L).toInt() != signal.deviceType ||
|
||||
((canId ushr 20) and 15L).toInt() != signal.device) return null
|
||||
val address = parseTrendAddress(signal.address, 0xFFFF)?.toInt() ?: return null
|
||||
(address - (canId and 0xFFFF).toInt()) * 2
|
||||
}
|
||||
TrendSource.CAN_RAW -> {
|
||||
if (extended != signal.extended || canId != parseTrendAddress(signal.address, 0x1FFF_FFFF)) return null
|
||||
signal.byteOffset
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
if (offset < 0 || offset + 2 > data.size) return null
|
||||
val word = (data[offset].toInt() and 0xFF) or ((data[offset + 1].toInt() and 0xFF) shl 8)
|
||||
return signal.valueType.decode(word)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
import kotlin.math.abs
|
||||
|
||||
enum class TrendMarker(val title: String, val horizontal: Boolean) {
|
||||
A("A", false), B("B", false), C("C", false), D("D", false),
|
||||
E("E", true), F("F", true), G("G", true), H("H", true)
|
||||
}
|
||||
|
||||
data class TrendMarkers(
|
||||
val xEnabled: Boolean = true,
|
||||
val yEnabled: Boolean = false,
|
||||
val selected: TrendMarker = TrendMarker.A,
|
||||
val a: Double? = null,
|
||||
val b: Double? = null,
|
||||
val c: Double? = null,
|
||||
val d: Double? = null,
|
||||
val e: Double? = null,
|
||||
val f: Double? = null,
|
||||
val g: Double? = null,
|
||||
val h: Double? = null,
|
||||
) {
|
||||
fun value(marker: TrendMarker): Double? = when (marker) {
|
||||
TrendMarker.A -> a; TrendMarker.B -> b; TrendMarker.C -> c; TrendMarker.D -> d
|
||||
TrendMarker.E -> e; TrendMarker.F -> f; TrendMarker.G -> g; TrendMarker.H -> h
|
||||
}
|
||||
fun move(marker: TrendMarker, value: Double): TrendMarkers = when (marker) {
|
||||
TrendMarker.A -> copy(a = value); TrendMarker.B -> copy(b = value)
|
||||
TrendMarker.C -> copy(c = value); TrendMarker.D -> copy(d = value)
|
||||
TrendMarker.E -> copy(e = value); TrendMarker.F -> copy(f = value)
|
||||
TrendMarker.G -> copy(g = value); TrendMarker.H -> copy(h = value)
|
||||
}
|
||||
fun positioned(bounds: PlotBounds): TrendMarkers = copy(
|
||||
a = a ?: bounds.value(0.2, false), b = b ?: bounds.value(0.4, false),
|
||||
c = c ?: bounds.value(0.6, false), d = d ?: bounds.value(0.8, false),
|
||||
e = e ?: bounds.value(0.2, true), f = f ?: bounds.value(0.4, true),
|
||||
g = g ?: bounds.value(0.6, true), h = h ?: bounds.value(0.8, true))
|
||||
fun reset(bounds: PlotBounds): TrendMarkers = copy(
|
||||
a = null, b = null, c = null, d = null, e = null, f = null, g = null, h = null).positioned(bounds)
|
||||
fun fraction(marker: TrendMarker, bounds: PlotBounds): Double = bounds.fraction(requireNotNull(value(marker)), marker.horizontal)
|
||||
fun drag(marker: TrendMarker, delta: Double, length: Double, bounds: PlotBounds): TrendMarkers =
|
||||
move(marker, bounds.drag(requireNotNull(value(marker)), delta, length, marker.horizontal)).copy(selected = marker)
|
||||
fun hit(x: Double, y: Double, width: Double, height: Double, radius: Double, bounds: PlotBounds): TrendMarker? {
|
||||
if (x !in 0.0..width || y !in 0.0..height) return null
|
||||
return TrendMarker.entries.filter { enabled(it) && value(it) != null }.mapNotNull { marker ->
|
||||
val fraction = fraction(marker, bounds)
|
||||
val distance = abs(if (marker.horizontal) y - fraction * height else x - fraction * width)
|
||||
if (fraction in 0.0..1.0 && distance <= radius) marker to distance else null
|
||||
}.sortedWith(compareBy<Pair<TrendMarker, Double>> { it.second }
|
||||
.thenBy { if (it.first == selected) 0 else 1 }).firstOrNull()?.first
|
||||
}
|
||||
fun enabled(marker: TrendMarker): Boolean = if (marker.horizontal) yEnabled else xEnabled
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
enum class TrendSection { SIGNALS, CHART }
|
||||
|
||||
/** File-format IDs; host applications map their connection profiles to these IDs. */
|
||||
enum class TrendProfile(val title: String) {
|
||||
SET_V1("SET GUI v1"), TMS2812("TMS320F2812 / BALZAM"), CAN_BRIDGE("CAN ↔ RS485"),
|
||||
GS_USB_CAN("CANgaroo / gs_usb"), SLCAN("SKLab SLCAN"), CANGAROO_SLCAN("CANgaroo / SLCAN"),
|
||||
BALZAM_CAN("Старый CAN BALZAM"),
|
||||
}
|
||||
|
||||
enum class TrendSource(val title: String) {
|
||||
TMS_MEMORY("Память TMS · CMD_PEEK"),
|
||||
CAN_GAS("Регистр GAS · ProtoCAN"),
|
||||
CAN_RAW("Слово из CAN-кадра"),
|
||||
SET_SENSOR("Температура DS18B20"),
|
||||
SET_GAS("Регистр GAS · SET GUI"),
|
||||
}
|
||||
|
||||
enum class TrendValueType(val title: String) {
|
||||
UINT16("UInt16 · 0…65535"), INT16("Int16 · −32768…32767");
|
||||
|
||||
fun decode(word: Int): Double = when (this) {
|
||||
UINT16 -> word.toDouble()
|
||||
INT16 -> word.toShort().toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
fun TrendProfile.trendSources(): List<TrendSource> = when (this) {
|
||||
TrendProfile.TMS2812 -> listOf(TrendSource.TMS_MEMORY)
|
||||
TrendProfile.SET_V1 -> listOf(TrendSource.SET_GAS, TrendSource.SET_SENSOR)
|
||||
TrendProfile.BALZAM_CAN -> listOf(TrendSource.CAN_RAW)
|
||||
else -> listOf(TrendSource.CAN_GAS, TrendSource.CAN_RAW)
|
||||
}
|
||||
|
||||
/** Stable IDs keep samples attached to a signal when its name/order changes. */
|
||||
data class TrendSignal(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val order: Int = 1,
|
||||
val name: String = "Тренд 1",
|
||||
val source: TrendSource = TrendSource.TMS_MEMORY,
|
||||
val address: String = "0x00000100",
|
||||
val color: String = "#2F91FF",
|
||||
val visible: Boolean = true,
|
||||
val valueType: TrendValueType = TrendValueType.UINT16,
|
||||
val deviceType: Int = 7,
|
||||
val device: Int = 13,
|
||||
val byteOffset: Int = 0,
|
||||
val multiplier: Double = 1.0,
|
||||
val iq: Int = 0,
|
||||
val extended: Boolean = true,
|
||||
) {
|
||||
fun validationError(profile: TrendProfile): String? = when {
|
||||
id.isBlank() || id.length > 80 -> "Неверный идентификатор тренда"
|
||||
order !in 1..9999 -> "Номер должен быть от 1 до 9999"
|
||||
name.isBlank() || name.length > 100 -> "Имя должно содержать от 1 до 100 символов"
|
||||
address.length > 64 -> "Адрес слишком длинный"
|
||||
source !in profile.trendSources() -> "Источник несовместим с протоколом ${profile.title}"
|
||||
!color.matches(Regex("#[0-9a-fA-F]{6}")) -> "Цвет задаётся как #RRGGBB"
|
||||
source == TrendSource.SET_SENSOR -> if (normalizeRom(address).matches(Regex("[0-9A-F]{16}"))) null
|
||||
else "ROM датчика должен содержать 16 HEX-цифр"
|
||||
source == TrendSource.TMS_MEMORY && parseTrendAddress(address, 0xFFFF_FFFFL) == null ->
|
||||
"Адрес памяти: 0…0xFFFFFFFF"
|
||||
source in setOf(TrendSource.CAN_GAS, TrendSource.SET_GAS) && parseTrendAddress(address, 0xFFFF) == null ->
|
||||
"Адрес GAS: 0…0xFFFF"
|
||||
source == TrendSource.CAN_GAS && (deviceType !in 0..7 || device !in 0..15) ->
|
||||
"Тип устройства: 0…7, номер устройства: 0…15"
|
||||
source == TrendSource.CAN_RAW && parseTrendAddress(address, if (extended) 0x1FFF_FFFF else 0x7FF) == null ->
|
||||
"CAN ID вне диапазона ${if (extended) "29" else "11"} бит"
|
||||
source == TrendSource.CAN_RAW && byteOffset !in 0..6 -> "Смещение слова: 0…6 байт"
|
||||
!multiplier.isFinite() -> "Множитель должен быть конечным числом"
|
||||
iq !in 0..30 -> "IQ должен быть в диапазоне 0…30"
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun displayValue(rawValue: Double): Double =
|
||||
rawValue * multiplier / (1L shl iq).toDouble()
|
||||
|
||||
/** Only source changes invalidate the historical series. */
|
||||
fun sameInput(other: TrendSignal): Boolean =
|
||||
copy(
|
||||
order = other.order,
|
||||
name = other.name,
|
||||
color = other.color,
|
||||
visible = other.visible,
|
||||
multiplier = other.multiplier,
|
||||
iq = other.iq,
|
||||
) == other
|
||||
|
||||
companion object {
|
||||
val COLORS = listOf("#2F91FF", "#FFB547", "#52D6A4", "#F8798D", "#B79AFF", "#4AD9E8", "#E8DA68", "#E7ECF3")
|
||||
fun new(profile: TrendProfile, existing: List<TrendSignal>): TrendSignal {
|
||||
val order = (1..9999).first { number -> existing.none { it.order == number } }
|
||||
val source = profile.trendSources().first()
|
||||
return TrendSignal(
|
||||
order = order, name = "Тренд $order", source = source,
|
||||
address = when (source) {
|
||||
TrendSource.TMS_MEMORY -> "0x00000100"
|
||||
TrendSource.CAN_GAS -> "0x0000"
|
||||
TrendSource.SET_GAS -> "0x0000"
|
||||
TrendSource.CAN_RAW -> "0x00BA0010"
|
||||
TrendSource.SET_SENSOR -> ""
|
||||
},
|
||||
color = COLORS[(order - 1) % COLORS.size],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseTrendAddress(value: String, max: Long): Long? {
|
||||
val text = value.trim()
|
||||
val hex = text.startsWith("0x", ignoreCase = true)
|
||||
val digits = if (hex) text.drop(2) else text
|
||||
if (digits.isEmpty() || !digits.all { if (hex) it in "0123456789abcdefABCDEF" else it in '0'..'9' }) return null
|
||||
return digits.toLongOrNull(if (hex) 16 else 10)?.takeIf { it in 0..max }
|
||||
}
|
||||
|
||||
fun normalizeRom(value: String): String = value.trim().replace("-", "").replace(" ", "").uppercase()
|
||||
|
||||
data class TrendPoint(
|
||||
val timestampMs: Long,
|
||||
val value: Double,
|
||||
/** Nanoseconds inside [timestampMs], preserving spacing above 1 kHz without overflowing epoch nanoseconds. */
|
||||
val subMillisecondNanos: Int = 0,
|
||||
) {
|
||||
init { require(subMillisecondNanos in 0..999_999) }
|
||||
fun nanosSince(other: TrendPoint): Long =
|
||||
(timestampMs - other.timestampMs) * 1_000_000L + subMillisecondNanos - other.subMillisecondNanos
|
||||
}
|
||||
|
||||
data class TrendUiState(
|
||||
val section: TrendSection = TrendSection.SIGNALS,
|
||||
val settings: Map<TrendProfile, List<TrendSignal>> = emptyMap(),
|
||||
val history: Map<String, List<TrendPoint>> = emptyMap(),
|
||||
val running: Boolean = false,
|
||||
val status: String = "Добавьте сигналы и откройте вкладку «График»",
|
||||
val fileBusy: Boolean = false,
|
||||
) {
|
||||
fun signals(profile: TrendProfile): List<TrendSignal> = settings[profile].orEmpty().sortedBy { it.order }
|
||||
|
||||
fun append(values: Map<String, Double>, timestampMs: Long): TrendUiState {
|
||||
if (!running) return this
|
||||
val next = history.toMutableMap()
|
||||
values.forEach { (id, value) ->
|
||||
if (value.isFinite()) next[id] = (next[id].orEmpty() + TrendPoint(timestampMs, value)).takeLast(MAX_POINTS)
|
||||
}
|
||||
return copy(history = next)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_SIGNALS = 64
|
||||
const val MAX_POINTS = 600
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
/** Versioned, human-readable configuration. Import is validated before replacing any state. */
|
||||
object TrendSettingsJson {
|
||||
const val MAX_FILE_BYTES = 1024 * 1024
|
||||
|
||||
fun encode(settings: Map<TrendProfile, List<TrendSignal>>): String {
|
||||
validate(settings)
|
||||
val profiles = JSONObject()
|
||||
settings.forEach { (profile, signals) ->
|
||||
profiles.put(profile.name, JSONArray().apply {
|
||||
signals.sortedBy { it.order }.forEach { signal ->
|
||||
put(JSONObject().apply {
|
||||
put("id", signal.id)
|
||||
put("order", signal.order)
|
||||
put("name", signal.name)
|
||||
put("source", signal.source.name)
|
||||
put("address", signal.address)
|
||||
put("color", signal.color)
|
||||
put("visible", signal.visible)
|
||||
put("valueType", signal.valueType.name)
|
||||
put("deviceType", signal.deviceType)
|
||||
put("device", signal.device)
|
||||
put("byteOffset", signal.byteOffset)
|
||||
put("multiplier", signal.multiplier)
|
||||
put("iq", signal.iq)
|
||||
put("extended", signal.extended)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return JSONObject().put("format", "setflash-trends").put("version", 1).put("profiles", profiles).toString(2)
|
||||
}
|
||||
|
||||
fun decode(text: String): Map<TrendProfile, List<TrendSignal>> {
|
||||
require(text.toByteArray(Charsets.UTF_8).size <= MAX_FILE_BYTES) { "Файл настроек больше 1 МБ" }
|
||||
val root = JSONObject(text.removePrefix("\uFEFF"))
|
||||
require(root.strictString("format") == "setflash-trends" && root.strictInt("version") == 1) {
|
||||
"Неизвестный формат или версия настроек графиков"
|
||||
}
|
||||
val profiles = root.getJSONObject("profiles")
|
||||
val result = profiles.keys().asSequence().associate { key ->
|
||||
val profile = TrendProfile.entries.firstOrNull { it.name == key }
|
||||
?: error("Неизвестный протокол: $key")
|
||||
val array = profiles.getJSONArray(key)
|
||||
require(array.length() <= TrendUiState.MAX_SIGNALS) { "Не более 64 сигналов на протокол" }
|
||||
profile to (0 until array.length()).map { index ->
|
||||
val item = array.getJSONObject(index)
|
||||
TrendSignal(
|
||||
id = item.strictString("id"), order = item.strictInt("order"), name = item.strictString("name"),
|
||||
source = TrendSource.valueOf(item.strictString("source")), address = item.strictString("address"),
|
||||
color = item.strictString("color"), visible = item.strictBoolean("visible"),
|
||||
valueType = TrendValueType.valueOf(item.strictString("valueType")),
|
||||
deviceType = item.strictInt("deviceType"), device = item.strictInt("device"),
|
||||
byteOffset = item.strictInt("byteOffset"),
|
||||
multiplier = if (item.has("multiplier")) item.strictDouble("multiplier") else 1.0,
|
||||
iq = if (item.has("iq")) item.strictInt("iq") else 0,
|
||||
extended = item.strictBoolean("extended"),
|
||||
)
|
||||
}
|
||||
}
|
||||
validate(result)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun JSONObject.strictString(key: String): String =
|
||||
get(key).let { require(it is String) { "$key должен быть строкой" }; it }
|
||||
|
||||
private fun JSONObject.strictBoolean(key: String): Boolean =
|
||||
get(key).let { require(it is Boolean) { "$key должен быть логическим флагом" }; it }
|
||||
|
||||
private fun JSONObject.strictInt(key: String): Int {
|
||||
val value = get(key)
|
||||
require(value is Int || value is Long) { "$key должен быть целым числом" }
|
||||
val number = (value as Number).toLong()
|
||||
require(number in Int.MIN_VALUE..Int.MAX_VALUE) { "$key вне диапазона" }
|
||||
return number.toInt()
|
||||
}
|
||||
|
||||
private fun JSONObject.strictDouble(key: String): Double {
|
||||
val value = get(key)
|
||||
require(value is Number) { "$key должен быть числом" }
|
||||
return value.toDouble().also { require(it.isFinite()) { "$key должен быть конечным числом" } }
|
||||
}
|
||||
|
||||
fun validate(settings: Map<TrendProfile, List<TrendSignal>>) {
|
||||
val allIds = settings.values.flatten().map { it.id }
|
||||
require(allIds.distinct().size == allIds.size) { "Идентификаторы трендов не должны повторяться" }
|
||||
settings.forEach { (profile, signals) ->
|
||||
require(signals.size <= TrendUiState.MAX_SIGNALS) { "Не более 64 сигналов на протокол" }
|
||||
require(signals.map { it.order }.distinct().size == signals.size) { "Номера трендов одного протокола должны различаться" }
|
||||
signals.forEach { signal ->
|
||||
require(signal.validationError(profile) == null) { "${signal.name}: ${signal.validationError(profile)}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
import ru.setcorp.setprotocol.NativeSetProtocol
|
||||
|
||||
/** Enum order is the stable set_spectrum.h ABI; titles belong to this GUI adapter. */
|
||||
enum class SpectrumWindow(val title: String) {
|
||||
RECT("Прямоугольное"), HANN("Hann"), HAMMING("Hamming"), BLACKMAN("Blackman"), FLATTOP("Flat Top"),
|
||||
}
|
||||
enum class SpectrumFilter(val title: String) {
|
||||
NONE("Без фильтра"), LOW_PASS("ФНЧ"), HIGH_PASS("ФВЧ"), BAND_PASS("Полосовой"), NOTCH("Режекторный"),
|
||||
}
|
||||
data class SpectrumOptions(
|
||||
val window: SpectrumWindow = SpectrumWindow.HANN,
|
||||
val filter: SpectrumFilter = SpectrumFilter.NONE,
|
||||
val lowHz: Double = 10.0,
|
||||
val highHz: Double = 100.0,
|
||||
val removeMean: Boolean = true,
|
||||
val maxSize: Int = 4096,
|
||||
) {
|
||||
fun validate() {
|
||||
require(maxSize in 16..16384 && maxSize and (maxSize - 1) == 0) { "Размер FFT: степень 2 от 16 до 16384" }
|
||||
if (filter in listOf(SpectrumFilter.HIGH_PASS, SpectrumFilter.BAND_PASS, SpectrumFilter.NOTCH))
|
||||
require(lowHz.isFinite() && lowHz > 0) { "Частота должна быть больше 0" }
|
||||
if (filter in listOf(SpectrumFilter.LOW_PASS, SpectrumFilter.BAND_PASS))
|
||||
require(highHz.isFinite() && highHz > 0) { "Частота должна быть больше 0" }
|
||||
if (filter == SpectrumFilter.BAND_PASS) require(lowHz < highHz) { "Нижняя частота должна быть меньше верхней" }
|
||||
}
|
||||
}
|
||||
|
||||
data class TrendSpectrum(
|
||||
val size: Int = 0, val sampleRate: Double = 0.0, val jitter: Double = 0.0,
|
||||
val amplitudes: List<Double> = emptyList(), val error: String? = null,
|
||||
) {
|
||||
val binHz: Double get() = if (size > 0) sampleRate / size else 0.0
|
||||
}
|
||||
|
||||
/** Math is implemented once in C and used unchanged by JNI and ctypes. */
|
||||
object SpectrumAnalyzer {
|
||||
fun analyze(points: List<TrendPoint>, options: SpectrumOptions): TrendSpectrum {
|
||||
options.validate()
|
||||
if (points.size < 16) return TrendSpectrum(error = "Нужно минимум 16 точек")
|
||||
if (!NativeSetProtocol.available) return TrendSpectrum(error = "Модуль FFT недоступен")
|
||||
val count = minOf(points.size, options.maxSize)
|
||||
val start = points.size - count
|
||||
val origin = points[start]
|
||||
val times = DoubleArray(count) { points[start + it].nanosSince(origin) / 1_000_000_000.0 }
|
||||
val values = DoubleArray(count) { points[start + it].value }
|
||||
val output = NativeSetProtocol.nativeSpectrum(times, values, options.maxSize,
|
||||
options.window.ordinal, options.filter.ordinal, options.lowHz, options.highHz, options.removeMean)
|
||||
?: return TrendSpectrum(error = "Не удалось вычислить FFT")
|
||||
return decode(output)
|
||||
}
|
||||
|
||||
internal fun decode(output: DoubleArray): TrendSpectrum {
|
||||
require(output.size >= 4)
|
||||
val error = when (output[0].toInt()) {
|
||||
0 -> null
|
||||
1 -> "Нужно минимум 16 точек"
|
||||
3 -> "Разрывы или неравномерные метки времени: выберите другой участок"
|
||||
4 -> "Частота фильтра должна быть ниже Fs/2 = %.3f Гц".format(java.util.Locale.ROOT, output[2] / 2)
|
||||
5 -> "Недостаточно памяти для FFT"
|
||||
else -> "Некорректные данные FFT"
|
||||
}
|
||||
return TrendSpectrum(output[1].toInt(), output[2], output[3],
|
||||
if (error == null) output.drop(4) else emptyList(), error)
|
||||
}
|
||||
}
|
||||
22
c/set-protocol/ports/android/set_plot_jni.c
Normal file
22
c/set-protocol/ports/android/set_plot_jni.c
Normal file
@@ -0,0 +1,22 @@
|
||||
#include <jni.h>
|
||||
#include "set_plot.h"
|
||||
|
||||
JNIEXPORT jdoubleArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_trends_NativePlot_evaluate(JNIEnv *env, jobject self,
|
||||
jint operation, jdoubleArray input) {
|
||||
double values[10], result[4];
|
||||
jsize n;
|
||||
size_t count;
|
||||
jdoubleArray output;
|
||||
(void)self;
|
||||
if (!input) return NULL;
|
||||
n = (*env)->GetArrayLength(env, input);
|
||||
if (n < 0 || n > 10) return NULL;
|
||||
(*env)->GetDoubleArrayRegion(env, input, 0, n, values);
|
||||
if ((*env)->ExceptionCheck(env)) return NULL;
|
||||
count = set_plot_eval((uint32_t)operation, values, (size_t)n, result, 4);
|
||||
if (!count) return NULL;
|
||||
output = (*env)->NewDoubleArray(env, (jsize)count);
|
||||
if (output) (*env)->SetDoubleArrayRegion(env, output, 0, (jsize)count, result);
|
||||
return output;
|
||||
}
|
||||
@@ -4,6 +4,95 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "setprotocol_abi.h"
|
||||
#include "set_trends.h"
|
||||
#include "set_spectrum.h"
|
||||
|
||||
JNIEXPORT jdoubleArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeSpectrum(
|
||||
JNIEnv *env, jobject self, jdoubleArray times, jdoubleArray values, jint max_size,
|
||||
jint window, jint filter, jdouble low_hz, jdouble high_hz, jboolean remove_mean)
|
||||
{
|
||||
(void)self;
|
||||
if (times == NULL || values == NULL || max_size < 16 || max_size > (jint)SET_SPECTRUM_MAX ||
|
||||
(max_size & (max_size - 1)) != 0) return NULL;
|
||||
jsize count = (*env)->GetArrayLength(env, times);
|
||||
if ((*env)->GetArrayLength(env, values) != count) return NULL;
|
||||
/* Only the analyzed tail crosses JNI; allocations cannot grow with file length. */
|
||||
jsize used = count < max_size ? count : max_size;
|
||||
size_t bins = (size_t)max_size / 2 + 1;
|
||||
double *buffer = (double *)calloc((size_t)used * 2 + bins + 4, sizeof(double));
|
||||
if (buffer == NULL) return NULL;
|
||||
double *t = buffer, *v = t + used, *out = v + used;
|
||||
(*env)->GetDoubleArrayRegion(env, times, count - used, used, t);
|
||||
(*env)->GetDoubleArrayRegion(env, values, count - used, used, v);
|
||||
if ((*env)->ExceptionCheck(env)) { free(buffer); return NULL; }
|
||||
int status = set_spectrum_analyze(t, v, (size_t)used, (size_t)max_size,
|
||||
window, filter, low_hz, high_hz, remove_mean, out + 4, bins, out + 1);
|
||||
out[0] = (double)status;
|
||||
jsize length = status == SET_SPECTRUM_OK ? (jsize)out[1] / 2 + 5 : 4;
|
||||
jdoubleArray result = (*env)->NewDoubleArray(env, length);
|
||||
if (result != NULL) (*env)->SetDoubleArrayRegion(env, result, 0, length, out);
|
||||
free(buffer);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendCanValue(
|
||||
JNIEnv *env, jobject self, jint source, jlong address, jint device_type,
|
||||
jint device, jint byte_offset, jboolean extended, jboolean is_signed,
|
||||
jlong can_id, jint flags, jbyteArray input)
|
||||
{
|
||||
(void)self;
|
||||
jsize size = (*env)->GetArrayLength(env, input);
|
||||
if (size < 2 || size > 8) return SET_TREND_NO_VALUE;
|
||||
jbyte data[8];
|
||||
(*env)->GetByteArrayRegion(env, input, 0, size, data);
|
||||
return set_trend_can_value((uint8_t)source, (uint32_t)address,
|
||||
(uint8_t)device_type, (uint8_t)device, (uint8_t)byte_offset,
|
||||
(uint8_t)extended, (uint8_t)is_signed, (uint32_t)can_id,
|
||||
(uint8_t)flags, (const uint8_t *)data, (size_t)size);
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchRequest(
|
||||
JNIEnv *env, jobject self, jint period, jintArray input)
|
||||
{
|
||||
(void)self;
|
||||
jsize count = (*env)->GetArrayLength(env, input);
|
||||
if (count > (jsize)SET_TREND_WATCH_MAX || period < 0 || period > 65535) return NULL;
|
||||
jint values[SET_TREND_WATCH_MAX];
|
||||
uint16_t addresses[SET_TREND_WATCH_MAX];
|
||||
uint8_t output[4U + 2U * SET_TREND_WATCH_MAX];
|
||||
(*env)->GetIntArrayRegion(env, input, 0, count, values);
|
||||
for (jsize i = 0; i < count; ++i) {
|
||||
if (values[i] < 0 || values[i] > 65535) return NULL;
|
||||
addresses[i] = (uint16_t)values[i];
|
||||
}
|
||||
size_t size = set_trend_watch_request((uint16_t)period, addresses, (size_t)count, output, sizeof(output));
|
||||
if (size == 0U) return NULL;
|
||||
jbyteArray result = (*env)->NewByteArray(env, (jsize)size);
|
||||
if (result != NULL) (*env)->SetByteArrayRegion(env, result, 0, (jsize)size, (const jbyte *)output);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchValues(
|
||||
JNIEnv *env, jobject self, jbyteArray input)
|
||||
{
|
||||
(void)self;
|
||||
jsize size = (*env)->GetArrayLength(env, input);
|
||||
if (size < 6 || size > (jsize)(6U + 2U * SET_TREND_WATCH_MAX)) return NULL;
|
||||
jbyte payload[6U + 2U * SET_TREND_WATCH_MAX];
|
||||
uint16_t words[SET_TREND_WATCH_MAX];
|
||||
jint values[SET_TREND_WATCH_MAX];
|
||||
(*env)->GetByteArrayRegion(env, input, 0, size, payload);
|
||||
int count = set_trend_watch_values((const uint8_t *)payload, (size_t)size, words, SET_TREND_WATCH_MAX);
|
||||
if (count < 0) return NULL;
|
||||
for (int i = 0; i < count; ++i) values[i] = words[i];
|
||||
jintArray result = (*env)->NewIntArray(env, count);
|
||||
if (result != NULL) (*env)->SetIntArrayRegion(env, result, 0, count, values);
|
||||
return result;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint8_t *storage;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class GuiGasWatchTest {
|
||||
@Test fun requestMatchesPythonAndCContract() {
|
||||
assertArrayEquals(byteArrayOf(-24, 3, 2, 0, 0x34, 0x12, -1, -1), GuiGasWatch.request(1000, listOf(0x1234, 0xFFFF)))
|
||||
assertArrayEquals(byteArrayOf(0, 0, 0, 0), GuiGasWatch.request(0, emptyList()))
|
||||
assertTrue(runCatching { GuiGasWatch.request(1, (0..64).toList()) }.isFailure)
|
||||
}
|
||||
|
||||
@Test fun partialAckMustNeverMislabelValues() {
|
||||
GuiGasWatch.validateAck(byteArrayOf(-24, 3, 2, 0), 1000, 2)
|
||||
assertTrue(runCatching { GuiGasWatch.validateAck(byteArrayOf(-24, 3, 1, 0), 1000, 2) }.isFailure)
|
||||
assertTrue(runCatching { GuiGasWatch.validateAck(byteArrayOf(0, 0, 2, 0), 1000, 2) }.isFailure)
|
||||
}
|
||||
|
||||
@Test fun valuesRequireExactLengthAndCount() {
|
||||
val payload = byteArrayOf(1, 2, 3, 4, 2, 0, 0x34, 0x12, -1, -1)
|
||||
assertEquals(listOf(0x1234, 65535), GuiGasWatch.values(payload, 2))
|
||||
assertTrue(runCatching { GuiGasWatch.values(payload, 1) }.isFailure)
|
||||
assertTrue(runCatching { GuiGasWatch.values(payload.dropLast(1).toByteArray(), 2) }.isFailure)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class PlotContractTest {
|
||||
@Test fun jniUsesTheSameNumericContractAsPython() {
|
||||
val text = javaClass.getResourceAsStream("/plot-v1.json")!!.bufferedReader().use { it.readText() }
|
||||
val cases = JSONObject(text).getJSONArray("cases")
|
||||
for (i in 0 until cases.length()) {
|
||||
val case = cases.getJSONObject(i)
|
||||
val input = case.getJSONArray("input")
|
||||
val result = runCatching { NativePlot.call(case.getInt("op"),
|
||||
*DoubleArray(input.length()) { input.getDouble(it) }) }
|
||||
if (case.isNull("output")) assertTrue(case.getString("name"), result.isFailure)
|
||||
else {
|
||||
val expected = case.getJSONArray("output")
|
||||
assertArrayEquals(case.getString("name"), DoubleArray(expected.length()) { expected.getDouble(it) },
|
||||
result.getOrThrow(), 1e-10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun markerCoordinatesSurviveZoomAndDragUsesGestureStart() {
|
||||
val full = PlotBounds(1000.0, 2000.0, -10.0, 10.0)
|
||||
val markers = TrendMarkers().positioned(full)
|
||||
val zoomed = PlotBounds(1250.0, 1750.0, -5.0, 5.0)
|
||||
assertEquals(markers, markers.positioned(zoomed))
|
||||
val dragged = markers.drag(TrendMarker.A, 50.0, 500.0, zoomed)
|
||||
assertEquals(markers.a!! + 50, dragged.a!!, 1e-9)
|
||||
assertEquals(markers.b, dragged.b)
|
||||
assertEquals(TrendMarker.A, dragged.hit(0.0, 50.0, 500.0, 100.0, 20.0, zoomed))
|
||||
val crossed = markers.move(TrendMarker.A, 1900.0).move(TrendMarker.B, 1100.0)
|
||||
assertEquals(-800.0, plotDelta(crossed.a!!, crossed.b!!), 0.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class PlotViewportTest {
|
||||
@Test fun zoomAnchorsToFingerAndAxesAreIndependent() {
|
||||
val zoom = PlotViewport().transform(2.0, 1.0, focusX = 0.25)
|
||||
assertEquals(0.125, zoom.x, 1e-12)
|
||||
assertEquals(0.5, zoom.width, 1e-12)
|
||||
assertEquals(1.0, zoom.height, 1e-12)
|
||||
assertEquals(0.0, zoom.y, 1e-12)
|
||||
}
|
||||
@Test fun zoomAndPanClampToBoundsAndResetToFullView() {
|
||||
val zoom = PlotViewport().transform(1e6, 1e6)
|
||||
assertEquals(1.0 / 128, zoom.width, 1e-12)
|
||||
val panned = zoom.transform(panX = 10000.0, panY = -10000.0)
|
||||
assertEquals(0.0, panned.x, 1e-12)
|
||||
assertEquals(1.0 - panned.height, panned.y, 1e-12)
|
||||
assertEquals(PlotViewport(), panned.transform(1e-6, 1e-6))
|
||||
assertEquals(zoom, zoom.transform(Double.NaN, 1.0))
|
||||
assertEquals(zoom, zoom.transform(0.0, 1.0))
|
||||
}
|
||||
@Test fun nativeResponseAndErrorsAreDecodedWithoutInventingSpectrum() {
|
||||
val result = SpectrumAnalyzer.decode(doubleArrayOf(0.0, 16.0, 100.0, 0.0) + DoubleArray(9) { 2.0 })
|
||||
assertEquals(6.25, result.binHz, 0.0)
|
||||
assertEquals(9, result.amplitudes.size)
|
||||
val failed = SpectrumAnalyzer.decode(doubleArrayOf(4.0, 16.0, 100.0, 0.0))
|
||||
assertTrue(failed.error!!.contains("50.000"))
|
||||
assertTrue(failed.amplitudes.isEmpty())
|
||||
}
|
||||
@Test fun spectrumSettingsRejectInvalidBandsAndSizes() {
|
||||
listOf(SpectrumOptions(maxSize = 30), SpectrumOptions(filter = SpectrumFilter.LOW_PASS, highHz = Double.NaN),
|
||||
SpectrumOptions(filter = SpectrumFilter.BAND_PASS, lowHz = 100.0, highHz = 10.0)).forEach {
|
||||
assertTrue(runCatching { it.validate() }.isFailure)
|
||||
}
|
||||
SpectrumOptions().validate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class TrendSettingsJsonTest {
|
||||
@Test fun readsTheSameFixtureAsDesktopPython() {
|
||||
val text = requireNotNull(javaClass.getResourceAsStream("/trends-v1.json")).bufferedReader().use { it.readText() }
|
||||
val decoded = TrendSettingsJson.decode(text)
|
||||
assertEquals(5, decoded.values.sumOf { it.size })
|
||||
assertEquals("Ток фазы А", decoded.getValue(TrendProfile.TMS2812).single().name)
|
||||
assertEquals(TrendSource.SET_GAS, decoded.getValue(TrendProfile.SET_V1).first().source)
|
||||
assertEquals(decoded, TrendSettingsJson.decode(TrendSettingsJson.encode(decoded)))
|
||||
}
|
||||
|
||||
@Test fun rejectsCoercionOfBooleanFractionAndStringNumbers() {
|
||||
val json = TrendSettingsJson.encode(settings)
|
||||
listOf("1", 1.5, true).forEach { value ->
|
||||
val root = JSONObject(json)
|
||||
root.getJSONObject("profiles").getJSONArray("TMS2812").getJSONObject(0).put("order", value)
|
||||
expectInvalid { TrendSettingsJson.decode(root.toString()) }
|
||||
}
|
||||
expectInvalid { TrendSettingsJson.decode(JSONObject(json).put("version", 1.5).toString()) }
|
||||
}
|
||||
private val signal = TrendSignal(id = "tms-1", name = "Ток \"фаза А\"", color = "#FF1234", visible = false)
|
||||
private val settings = mapOf(TrendProfile.TMS2812 to listOf(signal))
|
||||
|
||||
private fun expectInvalid(block: () -> Unit) {
|
||||
assertTrue("Invalid settings must fail", runCatching(block).isFailure)
|
||||
}
|
||||
|
||||
@Test fun roundTripIncludesEveryFieldAndIndependentProtocols() {
|
||||
val full = settings + mapOf(
|
||||
TrendProfile.CAN_BRIDGE to listOf(TrendSignal(id = "gas", source = TrendSource.CAN_GAS, address = "0x0123", device = 3, deviceType = 2, valueType = TrendValueType.INT16)),
|
||||
TrendProfile.BALZAM_CAN to listOf(TrendSignal(id = "raw", source = TrendSource.CAN_RAW, address = "0x321", extended = false, byteOffset = 6)),
|
||||
TrendProfile.SET_V1 to listOf(TrendSignal(id = "sensor", source = TrendSource.SET_SENSOR, address = "28-01-02-03-04-05-06-07")),
|
||||
)
|
||||
assertEquals(full, TrendSettingsJson.decode(TrendSettingsJson.encode(full)))
|
||||
}
|
||||
|
||||
@Test fun supportsEmptyConfigurationAndUtf8Bom() {
|
||||
assertTrue(TrendSettingsJson.decode(TrendSettingsJson.encode(emptyMap())).isEmpty())
|
||||
assertEquals(settings, TrendSettingsJson.decode("\uFEFF" + TrendSettingsJson.encode(settings)))
|
||||
}
|
||||
|
||||
@Test fun encodingSortsByOrder() {
|
||||
val unordered = mapOf(TrendProfile.TMS2812 to listOf(signal.copy(id = "b", order = 2), signal))
|
||||
assertEquals(listOf(1, 2), TrendSettingsJson.decode(TrendSettingsJson.encode(unordered)).getValue(TrendProfile.TMS2812).map { it.order })
|
||||
}
|
||||
|
||||
@Test fun rejectsUnknownVersionFormatProfileAndMissingFields() {
|
||||
val json = TrendSettingsJson.encode(settings)
|
||||
expectInvalid { TrendSettingsJson.decode(JSONObject(json).put("version", 2).toString()) }
|
||||
expectInvalid { TrendSettingsJson.decode(JSONObject(json).put("format", "other").toString()) }
|
||||
expectInvalid { TrendSettingsJson.decode(json.replace("TMS2812", "UNKNOWN")) }
|
||||
val missing = JSONObject(json)
|
||||
missing.getJSONObject("profiles").getJSONArray("TMS2812").getJSONObject(0).remove("address")
|
||||
expectInvalid { TrendSettingsJson.decode(missing.toString()) }
|
||||
}
|
||||
|
||||
@Test fun rejectsDuplicateOrderAndCrossProtocolIds() {
|
||||
expectInvalid { TrendSettingsJson.encode(mapOf(TrendProfile.TMS2812 to listOf(signal, signal.copy(id = "another")))) }
|
||||
expectInvalid { TrendSettingsJson.encode(settings + (TrendProfile.CAN_BRIDGE to listOf(signal.copy(source = TrendSource.CAN_GAS)))) }
|
||||
}
|
||||
|
||||
@Test fun importRunsValidationBeforeReturningAnySettings() {
|
||||
val validJson = TrendSettingsJson.encode(settings)
|
||||
listOf("address" to "0x100000000", "color" to "#BAD", "source" to "CAN_GAS", "name" to "").forEach { (field, value) ->
|
||||
val bad = JSONObject(validJson)
|
||||
bad.getJSONObject("profiles").getJSONArray("TMS2812").getJSONObject(0).put(field, value)
|
||||
expectInvalid { TrendSettingsJson.decode(bad.toString()) }
|
||||
}
|
||||
assertEquals(settings, TrendSettingsJson.decode(validJson))
|
||||
}
|
||||
|
||||
@Test fun enforcesSignalCountAndFileSizeLimits() {
|
||||
expectInvalid {
|
||||
TrendSettingsJson.encode(mapOf(TrendProfile.TMS2812 to (1..65).map { signal.copy(id = "$it", order = it) }))
|
||||
}
|
||||
expectInvalid { TrendSettingsJson.decode(" ".repeat(TrendSettingsJson.MAX_FILE_BYTES + 1)) }
|
||||
}
|
||||
|
||||
@Test fun malformedJsonIsNotAccepted() {
|
||||
listOf("", "not JSON", "[]", "{\"format\":").forEach { text -> expectInvalid { TrendSettingsJson.decode(text) } }
|
||||
}
|
||||
}
|
||||
602
c/set-protocol/ports/stm32-bxcan/README.md
Normal file
602
c/set-protocol/ports/stm32-bxcan/README.md
Normal file
File diff suppressed because one or more lines are too long
1667
c/set-protocol/ports/stm32-bxcan/protocan.c
Normal file
1667
c/set-protocol/ports/stm32-bxcan/protocan.c
Normal file
@@ -0,0 +1,1667 @@
|
||||
#include "protocan.h"
|
||||
|
||||
ProtoCanDevice_t CurrentDevice;
|
||||
ProtoCanDevice_t Device_on_the_Network[8][16];
|
||||
struct controlflags ControlFlags;
|
||||
|
||||
CAN_HandleTypeDef *_HCAN = 0;
|
||||
RTC_HandleTypeDef *_HRTC = 0;
|
||||
TIM_HandleTypeDef *_HTIM = 0;
|
||||
|
||||
/* CurrentStep - индекс следующего сообщения на обработку (пишет основной код).
|
||||
LastStep - индекс последнего записанного сообщения (пишет ISR).
|
||||
Оба индекса volatile: без этого компилятор вправе закэшировать значение в
|
||||
регистре, т.к. в его модели исполнения прерывание в код не входит.
|
||||
Сам массив rxMsg volatile НЕ объявлен - это ухудшило бы кодогенерацию и
|
||||
мешало бы копированию структуры по значению. Порядок "заполнить слот, затем
|
||||
опубликовать индекс" обеспечивается барьерами __DMB() в точках публикации и
|
||||
чтения. */
|
||||
volatile uint16_t CurrentStep = 1;
|
||||
volatile uint16_t LastStep = 0;
|
||||
struct RXMsg rxMsg[PROTOCAN_RX_BUFFER_SIZE];
|
||||
|
||||
/**
|
||||
* @brief Проверяет, является ли год високосным.
|
||||
* В системе год хранится как смещение от 2000. Например, значение 24 соответствует 2024.
|
||||
* Эта функция добавляет 2000 к значению и проверяет, високосный ли получившийся год.
|
||||
* @param year Смещение от 2000, тип uint8_t (от 0 до 255).
|
||||
* @return _Bool true, если год високосный; false — иначе.
|
||||
*/
|
||||
_Bool IsLeapYear(uint8_t year)
|
||||
{
|
||||
year += 2000;
|
||||
return (year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Возвращает количество доступных сообщений в буфере приёма CAN.
|
||||
* Используется кольцевой буфер.
|
||||
* Возвращаемое значение — количество элементов между указателями LastStep и CurrentStep.
|
||||
* @return uint16_t Количество доступных сообщений, ожидающих обработки, в буфере.
|
||||
*/
|
||||
uint16_t AvailableCanRxMsg(void)
|
||||
{
|
||||
/* Однократное чтение индекса, обновляемого из ISR. */
|
||||
uint16_t last = LastStep;
|
||||
uint16_t current = CurrentStep;
|
||||
/* Индекс прочитан до обращения к данным слота. */
|
||||
__DMB();
|
||||
return (uint16_t)(((unsigned)PROTOCAN_RX_BUFFER_SIZE + last - current + 1u) % PROTOCAN_RX_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
void PROTOCAN_DEINIT(uint8_t stage)
|
||||
{
|
||||
switch(stage)
|
||||
{
|
||||
case 3:
|
||||
#if (USE_HAL_TIM_REGISTER_CALLBACKS == 1)
|
||||
HAL_TIM_UnRegisterCallback(_HTIM, HAL_TIM_PERIOD_ELAPSED_CB_ID);
|
||||
#endif
|
||||
case 2:
|
||||
case 1:
|
||||
#if (USE_HAL_CAN_REGISTER_CALLBACKS == 1)
|
||||
HAL_CAN_UnRegisterCallback(_HCAN, HAL_CAN_RX_FIFO0_MSG_PENDING_CB_ID);
|
||||
#endif
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Инициализация переферии
|
||||
* @details Инициализация указателей на HCAN, HRTC, установка фильтров CAN.
|
||||
* @note Фильтры CAN описаны в разделе PROTOCAN_CAN_FILTERS().
|
||||
*/
|
||||
PROTOCAN_INIT_StatusTypeDef PROTOCAN_INIT(CAN_HandleTypeDef *tmp_hcan, RTC_HandleTypeDef *tmp_hrtc, TIM_HandleTypeDef *tmp_tim)
|
||||
{
|
||||
unsigned initStage = 0;
|
||||
if(tmp_hcan)
|
||||
{
|
||||
_HCAN = tmp_hcan;
|
||||
#if (USE_HAL_CAN_REGISTER_CALLBACKS == 1)
|
||||
HAL_StatusTypeDef CAN_RC_RESULT = HAL_CAN_RegisterCallback(_HCAN, HAL_CAN_RX_FIFO0_MSG_PENDING_CB_ID, ProtoCanRxFifo0MsgPendingCallback);
|
||||
if(CAN_RC_RESULT != HAL_OK)
|
||||
{
|
||||
PROTOCAN_DEINIT(initStage);
|
||||
return PROTOCAN_INIT_HCAN_ERROR;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
PROTOCAN_DEINIT(initStage);
|
||||
return PROTOCAN_INIT_HCAN_ERROR;
|
||||
}
|
||||
initStage++;
|
||||
if(tmp_hrtc)
|
||||
{
|
||||
_HRTC = tmp_hrtc;
|
||||
}
|
||||
else
|
||||
{
|
||||
PROTOCAN_DEINIT(initStage);
|
||||
return PROTOCAN_INIT_HRTC_ERROR;
|
||||
}
|
||||
initStage++;
|
||||
if(tmp_tim)
|
||||
{
|
||||
_HTIM = tmp_tim;
|
||||
#if (USE_HAL_TIM_REGISTER_CALLBACKS == 1)
|
||||
HAL_StatusTypeDef TIM_RC_RESULT = HAL_TIM_RegisterCallback(_HTIM, HAL_TIM_PERIOD_ELAPSED_CB_ID, ProtoCanPulseCallback);
|
||||
if(TIM_RC_RESULT != HAL_OK)
|
||||
{
|
||||
PROTOCAN_DEINIT(initStage);
|
||||
return PROTOCAN_INIT_TIM_ERROR;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
PROTOCAN_DEINIT(initStage);
|
||||
return PROTOCAN_INIT_TIM_ERROR;
|
||||
}
|
||||
initStage++;
|
||||
PROTOCAN_FILTERS();
|
||||
ControlFlags.IsPulse = 1;
|
||||
return PROTOCAN_INIT_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Бесконечный цикл обработки входящих CAN-сообщений.
|
||||
*
|
||||
* Эта функция постоянно проверяет наличие доступных сообщений в буфере CAN и
|
||||
* вызывает соответствующие обработчики в зависимости от типа полученного сообщения.
|
||||
* Обрабатываемые типы сообщений:
|
||||
* - Аналоговые данные
|
||||
* - Широковещательные сообщения
|
||||
* - Дискретные данные
|
||||
* - Общее адресное пространство
|
||||
* - Модбас сообщения
|
||||
* - Ошибки
|
||||
*
|
||||
* Обработка сообщений осуществляется внутри бесконечного цикла с использованием
|
||||
* функции AvailableCanRxMsg() для определения наличия новых сообщений.
|
||||
*
|
||||
* В конце каждого обработанного сообщения индекс текущего элемента буфера инкрементируется
|
||||
* с учетом размера буфера через операцию по модулю.
|
||||
*/
|
||||
void PROTOCAN_LoopProcessRxMsgs(void)
|
||||
{
|
||||
while(1)
|
||||
{
|
||||
if(AvailableCanRxMsg())
|
||||
{
|
||||
if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ANALOG)
|
||||
{
|
||||
PROTOCAN_AnalogProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_BROADCAST)
|
||||
{
|
||||
PROTOCAN_BroadcastProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_DISCRETE)
|
||||
{
|
||||
PROTOCAN_DiscreticProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE)
|
||||
{
|
||||
ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS)
|
||||
{
|
||||
PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL ||
|
||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE ||
|
||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING ||
|
||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT)
|
||||
{
|
||||
PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ERROR)
|
||||
{
|
||||
PROTOCAN_RequestError(rxMsg[CurrentStep]);
|
||||
}
|
||||
// Обновление индекса текущего сообщения с циклическим масштабированием
|
||||
CurrentStep = (uint16_t)(CurrentStep + 1) % PROTOCAN_RX_BUFFER_SIZE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обрабатывает все доступные входящие CAN-сообщения.
|
||||
*
|
||||
* Эта функция последовательно обрабатывает все сообщения, доступные в буфере,
|
||||
* вызывая соответствующие функции обработчики в зависимости от типа сообщения.
|
||||
* После обработки каждого сообщения индекс текущего элемента буфера обновляется
|
||||
* циклически.
|
||||
*
|
||||
* @return PROTOCAN_OK, если все сообщения были обработаны успешно.
|
||||
* В противном случае возвращается код ошибки, полученный при обработке сообщения.
|
||||
*
|
||||
* Обработка происходит до тех пор, пока есть доступные сообщения, полученные функцией
|
||||
* AvailableCanRxMsg().
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_ProcessAllRxMsgs(void)
|
||||
{
|
||||
PROTOCAN_StatusTypeDef result = PROTOCAN_TIMEOUT;
|
||||
while(AvailableCanRxMsg())
|
||||
{
|
||||
if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ANALOG)
|
||||
{
|
||||
result = PROTOCAN_AnalogProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_BROADCAST)
|
||||
{
|
||||
result = PROTOCAN_BroadcastProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_DISCRETE)
|
||||
{
|
||||
result = PROTOCAN_DiscreticProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE)
|
||||
{
|
||||
result = ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS)
|
||||
{
|
||||
result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL ||
|
||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE ||
|
||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING ||
|
||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT)
|
||||
{
|
||||
result = PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ERROR)
|
||||
{
|
||||
result = PROTOCAN_RequestError(rxMsg[CurrentStep]);
|
||||
}
|
||||
CurrentStep = (uint16_t)(CurrentStep + 1) % PROTOCAN_RX_BUFFER_SIZE;
|
||||
if(result != PROTOCAN_OK)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обрабатывает одно доступное CAN-сообщение.
|
||||
*
|
||||
* Эта функция проверяет наличие доступных сообщений в буфере и, если таковое есть,
|
||||
* выполняет обработку текущего сообщения в зависимости от его типа. После обработки
|
||||
* индекс текущего сообщения обновляется циклически.
|
||||
*
|
||||
* @return Прошедший статус обработки:
|
||||
* - PROTOCAN_OK, если сообщение успешно обработано,
|
||||
* - PROTOCAN_TIMEOUT, если сообщений не было,
|
||||
* - или код ошибки, возвращенный функцией обработки.
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_ProcessSingleRxMsg(void)
|
||||
{
|
||||
PROTOCAN_StatusTypeDef result = PROTOCAN_TIMEOUT;
|
||||
if(AvailableCanRxMsg())
|
||||
{
|
||||
if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ANALOG)
|
||||
{
|
||||
result = PROTOCAN_AnalogProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_BROADCAST)
|
||||
{
|
||||
result = PROTOCAN_BroadcastProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_DISCRETE)
|
||||
{
|
||||
result = PROTOCAN_DiscreticProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE)
|
||||
{
|
||||
result = ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS)
|
||||
{
|
||||
result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL ||
|
||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE ||
|
||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING ||
|
||||
rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_INPUT)
|
||||
{
|
||||
result = PROTOCAN_ModbusProcessing(rxMsg[CurrentStep]);
|
||||
}
|
||||
else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_ERROR)
|
||||
{
|
||||
result = PROTOCAN_RequestError(rxMsg[CurrentStep]);
|
||||
}
|
||||
CurrentStep = (uint16_t)(CurrentStep + 1) % PROTOCAN_RX_BUFFER_SIZE;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Функция обработки широковещательных запросов.
|
||||
* @param struct RXMsg _rxMsg - структура для полученного сообщения.
|
||||
* @details Функция, выполняющая команды, переданные в широковещательном формате с головного (master) устройства. Типы команд: Запрос статуса, запрос на включение или выключение, рестарт устройств, установка времени.
|
||||
* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка).
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_BroadcastProcessing(struct RXMsg _rxMsg)
|
||||
{
|
||||
msgBodyBroadcastType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
switch(msg.Fields.Type)
|
||||
{
|
||||
case PROTOCAN_BROADCAST_STATUS:
|
||||
{
|
||||
//Обработка запроса статуса устройства
|
||||
return ProtoCanMsgToBroadcastStatus(_rxMsg);
|
||||
}
|
||||
case PROTOCAN_BROADCAST_ONOFF:
|
||||
{
|
||||
//Обработка запроса на вкл/выкл
|
||||
ProtoCanMsgToBroadcastOnOff(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_BROADCAST_RESTARTDEVICE:
|
||||
{
|
||||
ProtoCanMsgToBroadcastRestart(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_BROADCAST_RTCSETUP:
|
||||
{
|
||||
//Обработка запроса на синхронизацию времени
|
||||
//С головным устройством
|
||||
ProtoCanMsgToBroadcastRtcSetup(_rxMsg);
|
||||
break;;
|
||||
}
|
||||
default:
|
||||
//RESERVE SENSOR TYPE.
|
||||
return PROTOCAN_ERROR;
|
||||
break;
|
||||
}
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Формирует и отправляет широковещательное CAN-сообщение для ответа на запрос статуса.
|
||||
* Устанавливает идентификатор, собирает текущие параметры времени и даты, и отправляет сообщение.
|
||||
* @param _rxMsg Структура полученного по CAN запроса.
|
||||
* @return HAL_StatusTypeDef Статус операции HAL, например HAL_OK при успехе.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastStatus(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.DLC = 7;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
tmp_eID.Fields.DeviceType = CURRENT_TYPE_DEVICE;
|
||||
tmp_eID.Fields.DeviceID = CURRENT_ID_DEVICE;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
RTC_TimeTypeDef sTime = {0};
|
||||
HAL_RTC_GetTime(_HRTC, &sTime, RTC_FORMAT_BIN);
|
||||
data[0] = sTime.Hours;
|
||||
data[1] = sTime.Minutes;
|
||||
data[2] = sTime.Seconds;
|
||||
RTC_DateTypeDef DateToUpdate = {0};
|
||||
HAL_RTC_GetDate(_HRTC, &DateToUpdate, RTC_FORMAT_BIN);
|
||||
data[3] = DateToUpdate.Year;
|
||||
data[4] = DateToUpdate.Month;
|
||||
data[5] = DateToUpdate.Date;
|
||||
data[6] = DateToUpdate.WeekDay;
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Виртуальная слабая функция для переключения режима пульсации ON/OFF.
|
||||
* Меняет состояние флага IsPulse в структуре ControlFlags.
|
||||
* В данном случае — просто инвертирует его логическое значение.
|
||||
* @param _rxMsg Структура полученного CAN-сообщения (Пока не используется в функции).
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastOnOff(struct RXMsg _rxMsg)
|
||||
{
|
||||
ControlFlags.IsPulse = !ControlFlags.IsPulse;
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief __weak функция для обработки широковещательного сообщения перезапуска устройства.
|
||||
* Проверяет длину сообщения, извлекает ID из данных и, если она совпадает с текущим устройством,
|
||||
* вызывает системный перезапуск.
|
||||
* @param _rxMsg Структура полученного CAN-сообщения с командой перезапуска.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastRestart(struct RXMsg _rxMsg)
|
||||
{
|
||||
if(_rxMsg.DLC == 0)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
msgBodyBroadcastType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
if(msg.Fields.Body == (CURRENT_ID_DEVICE / (_rxMsg.DLC*8)))
|
||||
{
|
||||
uint64_t page = 0;
|
||||
for(uint32_t i = 0; i < _rxMsg.DLC; i++)
|
||||
{
|
||||
/* Приведение обязательно: Data[i] продвигается до int, и сдвиг на
|
||||
32..56 бит без него - неопределённое поведение. */
|
||||
page |= (uint64_t)_rxMsg.Data[i] << (i * 8u);
|
||||
}
|
||||
if((page >> CURRENT_ID_DEVICE) & 1u)
|
||||
{
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
}
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief __weak функция для обработки запроса на синхронизацию RTC через широковещательное сообщение.
|
||||
* Проверяет длину сообщения и корректность данных (время и дата). Если все проверки пройдены,
|
||||
* вызывает функцию синхронизации RTC.
|
||||
* @param _rxMsg Структура полученного CAN-сообщения с данными для установки времени и даты.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastRtcSetup(struct RXMsg _rxMsg)
|
||||
{
|
||||
/* Нужны все 7 полей: чч, мм, сс, год, месяц, число, день недели.
|
||||
Прежняя проверка (DLC > 7) пропускала в том числе пустой кадр, и время
|
||||
устанавливалось из неинициализированных байт. */
|
||||
if(_rxMsg.DLC != 7)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
else
|
||||
{
|
||||
static const uint8_t DaysCount_Normal[2][12] = {{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
|
||||
{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
|
||||
/* Месяц нумеруется с 1, индекс массива - с 0. */
|
||||
if( _rxMsg.Data[0] > 23 ||
|
||||
_rxMsg.Data[1] > 59 ||
|
||||
_rxMsg.Data[2] > 59 ||
|
||||
_rxMsg.Data[3] > 99 ||
|
||||
_rxMsg.Data[4] == 0 || _rxMsg.Data[4] > 12 ||
|
||||
_rxMsg.Data[5] == 0 ||
|
||||
_rxMsg.Data[5] > DaysCount_Normal[IsLeapYear(_rxMsg.Data[3])][_rxMsg.Data[4] - 1] ||
|
||||
_rxMsg.Data[6] > 6)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
else
|
||||
{
|
||||
PROTOCAN_RTC_SYNC(_rxMsg.Data);
|
||||
}
|
||||
}
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Функция обработки дискретных запросов.
|
||||
* @param struct RXMsg _rxMsg - структура для полученного сообщения.
|
||||
* @details Функция, формирующая и отправляющая ответ на запросы. Типы запросов: Аварии, Предупреждения, Управляющие сигналы, Флаги, Рестарт устройства, Изменение режима работы устройства, Запрос на устройство.
|
||||
* @note Запрос на устройство. Головное (master) устройство запрашивает некоторое колличество параметров. В Data - 64 битовых адресса параметров, тип которых задаётся в Sensor ID. Имеется возможность запрашивать непоследовательные параметры.
|
||||
* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка).
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_DiscreticProcessing(struct RXMsg _rxMsg)
|
||||
{
|
||||
msgBodyDiscreteType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
switch(msg.Fields.Type)
|
||||
{
|
||||
case PROTOCAN_DISCRETE_ACCIDENT:
|
||||
{
|
||||
ProtoCanMsgToDiscreteAccident(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_DISCRETE_WARNING:
|
||||
{
|
||||
ProtoCanMsgToDiscreteWarning(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_DISCRETE_CONTROL_SIGNALS:
|
||||
{
|
||||
ProtoCanMsgToDiscreteControlSignals(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_DISCRETE_FLAGS:
|
||||
{
|
||||
ProtoCanMsgToDiscreteFlags(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_DISCRETE_RESET:
|
||||
{
|
||||
ProtoCanMsgToDiscreteReset(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_DISCRETE_CHANGE_MODE:
|
||||
{
|
||||
ProtoCanMsgToDiscreteChangeMode(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_DISCRETE_REQUEST_LIST_OF_PARAMETERS:
|
||||
{
|
||||
ProtoCanMsgToDiscreteRequestListOfParameters(_rxMsg);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
//RESERVE SENSOR TYPE.
|
||||
return PROTOCAN_ERROR;
|
||||
break;
|
||||
}
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработка запроса аварийных ситуаций.
|
||||
* В текущей реализации — заглушка, ничего не выполняет.
|
||||
* Можно реализовать отправку или обработку аварийных данных.
|
||||
* @param _rxMsg Входящее сообщение с данными запроса.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteAccident(struct RXMsg _rxMsg)
|
||||
{
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработка запроса предупреждений.
|
||||
* В текущей реализации — заглушка.
|
||||
* Можно реализовать обработку или ответ на предупреждения.
|
||||
* @param _rxMsg Входящее сообщение.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteWarning(struct RXMsg _rxMsg)
|
||||
{
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработка запроса управляющих сигналов.
|
||||
* В текущей реализации — заглушка.
|
||||
* Можно реализовать управление сигналами.
|
||||
* @param _rxMsg Входящее сообщение.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteControlSignals(struct RXMsg _rxMsg)
|
||||
{
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработка запроса флагов.
|
||||
* В текущей реализации — заглушка.
|
||||
* Можно реализовать работу с флагами.
|
||||
* @param _rxMsg Входящее сообщение.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteFlags(struct RXMsg _rxMsg)
|
||||
{
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработка рестарта системы по CAN-запросу.
|
||||
* Выполняет программный рестарт системы.
|
||||
* @param _rxMsg Входящее сообщение.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteReset(struct RXMsg _rxMsg)
|
||||
{
|
||||
NVIC_SystemReset();
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработка запроса на изменение режима.
|
||||
* В текущей реализации — заглушка.
|
||||
* Можно реализовать изменение режима работы.
|
||||
* @param _rxMsg Входящее сообщение.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteChangeMode(struct RXMsg _rxMsg)
|
||||
{
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработка запроса на получение списка параметров.
|
||||
* В текущей реализации — заглушка.
|
||||
* Можно реализовать отправку списка параметров.
|
||||
* @param _rxMsg Входящее сообщение.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteRequestListOfParameters(struct RXMsg _rxMsg)
|
||||
{
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Функция обработки аналоговых запросов.
|
||||
* @param struct RXMsg _rxMsg - структура для полученного сообщения.
|
||||
* @details Функция, сортирующая запросы по типам и вызывающая соответствующие функции. Типы запросов: Универсальный, Уставки, Напряжение, Ток, Температура.
|
||||
* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка).
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_AnalogProcessing(struct RXMsg _rxMsg)
|
||||
{
|
||||
msgBodyAnalogType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
switch (msg.Fields.Type)
|
||||
{
|
||||
case PROTOCAN_ANALOG_UNIVERSAL:
|
||||
{
|
||||
return ProtoCanMsgToAnalogUniversal(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_ANALOG_SETTINGS:
|
||||
{
|
||||
return ProtoCanMsgToAnalogSettings(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_ANALOG_U:
|
||||
{
|
||||
return ProtoCanMsgToAnalogUSens(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_ANALOG_I:
|
||||
{
|
||||
return ProtoCanMsgToAnalogISens(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_ANALOG_T:
|
||||
{
|
||||
return ProtoCanMsgToAnalogTSens(_rxMsg);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
//RESERVE SENSOR TYPE
|
||||
return PROTOCAN_ERROR;
|
||||
break;
|
||||
}
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Функция ответа на запрос данных со всех датчиков.
|
||||
* Создает сообщение с определенными данными и отправляет его через CAN-шину.
|
||||
* @param _rxMsg Структура входящего сообщения, используется для формирования ID.
|
||||
* @return HAL_StatusTypeDef Статус выполнения операции HAL, например HAL_OK.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogUniversal(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 6;
|
||||
data[0] = 'U';
|
||||
data[1] = 'N';
|
||||
data[2] = 'I';
|
||||
data[3] = 'V';
|
||||
data[4] = 'E';
|
||||
data[5] = 'R';
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Функция ответа на запрос уставок по CAN.
|
||||
* Создает сообщение с определенными данными и отправляет его через CAN-шину.
|
||||
* @param _rxMsg Структура входящего сообщения, используется только для формирования ID.
|
||||
* @return HAL_StatusTypeDef Статус выполнения операции HAL, например HAL_OK.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogSettings(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 7;
|
||||
data[0] = 'U';
|
||||
data[1] = 'S';
|
||||
data[2] = 'T';
|
||||
data[3] = 'A';
|
||||
data[4] = 'V';
|
||||
data[5] = 'K';
|
||||
data[6] = 'I';
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Функция ответа на запрос данных с датчиков напряжения.
|
||||
* Создает сообщение с указанными параметрами и отправляет его через CAN-шину.
|
||||
* @param _rxMsg Структура входящего сообщения, используемая для формирования идентификатора и данных.
|
||||
* @return HAL_StatusTypeDef Статус выполнения операции HAL, например HAL_OK при успешной отправке.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogUSens(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 6;
|
||||
msgBodyAnalogType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
data[0] = 'U';
|
||||
data[1] = 'S';
|
||||
data[2] = '0' + msg.Fields.SensorID / 1000;
|
||||
data[3] = '0' + msg.Fields.SensorID / 100 % 10;
|
||||
data[4] = '0' + msg.Fields.SensorID / 10 % 10;
|
||||
data[5] = '0' + msg.Fields.SensorID % 10;
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Функция ответа на запрос данных с датчиков тока.
|
||||
* Создает сообщение с определенными параметрами и отправляет его через CAN-шину.
|
||||
* @param _rxMsg Структура входящего сообщения, используется для формирования идентификатора и данных.
|
||||
* @return HAL_StatusTypeDef Статус операции HAL, например HAL_OK при успешной отправке.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogISens(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 6;
|
||||
msgBodyAnalogType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
data[0] = 'I';
|
||||
data[1] = 'S';
|
||||
data[2] = '0' + msg.Fields.SensorID / 1000;
|
||||
data[3] = '0' + msg.Fields.SensorID / 100 % 10;
|
||||
data[4] = '0' + msg.Fields.SensorID / 10 % 10;
|
||||
data[5] = '0' + msg.Fields.SensorID % 10;
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Функция ответа на запрос данных с датчиков температуры.
|
||||
* Создает сообщение с определенным заголовком, формирует данные по ID сенсора и отправляет его через CAN.
|
||||
* @param _rxMsg Структура полученного сообщения, которая используется для формирования идентификатора.
|
||||
* @return HAL_StatusTypeDef Статус выполнения функции HAL, например HAL_OK при успехе.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogTSens(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 6;
|
||||
msgBodyAnalogType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
data[0] = 'T';
|
||||
data[1] = 'S';
|
||||
data[2] = '0' + msg.Fields.SensorID / 1000;
|
||||
data[3] = '0' + msg.Fields.SensorID / 100 % 10;
|
||||
data[4] = '0' + msg.Fields.SensorID / 10 % 10;
|
||||
data[5] = '0' + msg.Fields.SensorID % 10;
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Отправляет сообщение по CAN-шине в соответствии с типом протокола.
|
||||
* Эта функция формирует и отправляет сообщение через CAN-шину, основываясь на типе
|
||||
* переданного ID и данных. Поддерживаются типы сообщений: широковещательные, дискретные
|
||||
* и аналоговые.
|
||||
* @param id Идентификатор сообщения, содержащий тип сообщения и другую информацию.
|
||||
* @param coreData Структура данных, включающая тип, тело и данные для передачи.
|
||||
* @return PROTOCAN_StatusTypeDef Статус операции.
|
||||
* @details
|
||||
* В зависимости от типа сообщения (панель broadcast, discrete или analog),
|
||||
* формируется соответствующий пакет данных и отправляется на шину CAN.
|
||||
* Для сообщений, превышающих длину 8 байт, передача выполняется пакетами.
|
||||
* В случае ошибок при добавлении сообщения в очередь передачи, возвращается ошибка.
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND_CORE(const ProtoCanId_t id, const struct ProtoCanCoreData coreData)
|
||||
{
|
||||
ProtoCanId_t localId = id;
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t canData[8];
|
||||
switch(id.Fields.MsgType)
|
||||
{
|
||||
case PROTOCAN_MSGTYPE_BROADCAST:
|
||||
{
|
||||
msgBodyBroadcastType body;
|
||||
body.Fields.Type = coreData.Type;
|
||||
body.Fields.Body = coreData.Body;
|
||||
localId.Fields.MsgBody = body.Body;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
TxHeader.DLC = 0;
|
||||
if(HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_MSGTYPE_DISCRETE:
|
||||
{
|
||||
msgBodyDiscreteType body;
|
||||
body.Fields.Type = coreData.Type;
|
||||
uint8_t regsRemaining = coreData.DataCount;
|
||||
uint8_t currentIndex = 0;
|
||||
while(regsRemaining > 0)
|
||||
{
|
||||
uint8_t regsInPacket = (regsRemaining > 8) ? 8 : regsRemaining;
|
||||
body.Fields.Body = coreData.Body + currentIndex;
|
||||
localId.Fields.MsgBody = body.Body;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
TxHeader.DLC = regsInPacket;
|
||||
for(int i = 0; i < regsInPacket; i++)
|
||||
{
|
||||
canData[i] = coreData.Data[currentIndex + i];
|
||||
}
|
||||
HAL_StatusTypeDef status = HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox);
|
||||
if(status != HAL_OK)
|
||||
{
|
||||
return (PROTOCAN_StatusTypeDef)status;
|
||||
}
|
||||
regsRemaining -= regsInPacket;
|
||||
currentIndex += regsInPacket;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_MSGTYPE_ANALOG:
|
||||
{
|
||||
msgBodyAnalogType body;
|
||||
body.Fields.Type = coreData.Type;
|
||||
uint8_t regsRemaining = coreData.DataCount;
|
||||
uint8_t currentIndex = 0;
|
||||
while(regsRemaining > 0)
|
||||
{
|
||||
uint8_t regsInPacket = (regsRemaining > 8) ? 8 : regsRemaining;
|
||||
body.Fields.SensorID = coreData.Body + currentIndex;
|
||||
localId.Fields.MsgBody = body.Body;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
TxHeader.DLC = regsInPacket;
|
||||
for(int i = 0; i < regsInPacket; i++)
|
||||
{
|
||||
canData[i] = coreData.Data[currentIndex + i];
|
||||
}
|
||||
HAL_StatusTypeDef status = HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox);
|
||||
if(status != HAL_OK)
|
||||
{
|
||||
return (PROTOCAN_StatusTypeDef)status;
|
||||
}
|
||||
regsRemaining -= regsInPacket;
|
||||
currentIndex += regsInPacket;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
}
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Разбирает однокадровый запрос SETTINGS для текущего устройства.
|
||||
*
|
||||
* Body[15:8] содержит номер сборки, Body[7:0] — позицию. DLC=0 означает
|
||||
* GET, DLC=8 с ненулевым ROM — WRITE/REPLACE, DLC=8 с нулями — CLEAR.
|
||||
* Проверка физического датчика и сохранение EEPROM выполняются приложением в
|
||||
* переопределённой функции ProtoCanMsgToSettings().
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SettingsProcessing(struct RXMsg _rxMsg)
|
||||
{
|
||||
ProtoCanSettingsMsg_t message;
|
||||
uint8_t index;
|
||||
uint8_t romIsZero = 1U;
|
||||
|
||||
if((_rxMsg.eID.BitAll & PROTOCAN_SETTINGS_HEADER_MASK)
|
||||
!= PROTOCAN_SETTINGS_REQUEST_BASE_ID)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
|
||||
message.AssemblySerial = (uint8_t)((_rxMsg.eID.Fields.MsgBody >> 8U) & 0xFFU);
|
||||
message.Position = (uint8_t)(_rxMsg.eID.Fields.MsgBody & 0xFFU);
|
||||
message.Result = PROTOCAN_SETTINGS_RESULT_OK;
|
||||
for(index = 0U; index < PROTOCAN_SETTINGS_ROM_SIZE; index++)
|
||||
{
|
||||
message.Rom[index] = 0U;
|
||||
}
|
||||
|
||||
if((_rxMsg.info.RTR != 0U)
|
||||
|| ((_rxMsg.DLC != 0U) && (_rxMsg.DLC != PROTOCAN_SETTINGS_ROM_SIZE)))
|
||||
{
|
||||
message.Operation = PROTOCAN_SETTINGS_INVALID;
|
||||
message.Result = PROTOCAN_SETTINGS_RESULT_INVALID_DLC;
|
||||
return PROTOCAN_SEND_SETTINGS_ERROR(message.AssemblySerial, message.Position,
|
||||
message.Result);
|
||||
}
|
||||
if(_rxMsg.DLC == 0U)
|
||||
{
|
||||
message.Operation = PROTOCAN_SETTINGS_GET;
|
||||
return ProtoCanMsgToSettings(&message);
|
||||
}
|
||||
|
||||
for(index = 0U; index < PROTOCAN_SETTINGS_ROM_SIZE; index++)
|
||||
{
|
||||
message.Rom[index] = _rxMsg.Data[index];
|
||||
if(message.Rom[index] != 0U)
|
||||
{
|
||||
romIsZero = 0U;
|
||||
}
|
||||
}
|
||||
message.Operation = (romIsZero != 0U) ? PROTOCAN_SETTINGS_CLEAR
|
||||
: PROTOCAN_SETTINGS_WRITE;
|
||||
return ProtoCanMsgToSettings(&message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Слабая точка подключения каталога датчиков приложения.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToSettings(
|
||||
const ProtoCanSettingsMsg_t *message)
|
||||
{
|
||||
(void)message;
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
|
||||
/** Отправляет один SETTINGS-кадр с Body из id и payload длиной 0..8. */
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS(
|
||||
ProtoCanId_t id, const struct ProtoCanSettingsData data)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0U;
|
||||
uint8_t canData[PROTOCAN_SETTINGS_ROM_SIZE] = {0U};
|
||||
uint8_t index;
|
||||
|
||||
if((id.Fields.MsgType != PROTOCAN_MSGTYPE_SETTINGS)
|
||||
|| (data.DataCount > PROTOCAN_SETTINGS_ROM_SIZE)
|
||||
|| ((data.DataCount != 0U) && (data.Data == 0)))
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
for(index = 0U; index < data.DataCount; index++)
|
||||
{
|
||||
canData[index] = data.Data[index];
|
||||
}
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&id);
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.DLC = data.DataCount;
|
||||
if(HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/** Отправляет успешный ответ SETTINGS с текущим ROM целевой локации. */
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_RESPONSE(uint8_t assemblySerial,
|
||||
uint8_t position, const uint8_t *rom)
|
||||
{
|
||||
ProtoCanId_t id;
|
||||
ProtoCanData_t data;
|
||||
|
||||
if(rom == 0)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
id.BitAll = PROTOCAN_SETTINGS_RESPONSE_BASE_ID
|
||||
| ((uint32_t)assemblySerial << 8U) | position;
|
||||
data.SettingsData.Data = rom;
|
||||
data.SettingsData.DataCount = PROTOCAN_SETTINGS_ROM_SIZE;
|
||||
return PROTOCAN_SEND_SETTINGS(id, data.SettingsData);
|
||||
}
|
||||
|
||||
/** Отправляет однобайтовую ошибку SETTINGS с сохранением Body запроса. */
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_ERROR(uint8_t assemblySerial,
|
||||
uint8_t position, ProtoCanSettingsResultType result)
|
||||
{
|
||||
ProtoCanId_t id;
|
||||
ProtoCanData_t data;
|
||||
const uint8_t error = (uint8_t)result;
|
||||
|
||||
if(result == PROTOCAN_SETTINGS_RESULT_OK)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
id.BitAll = PROTOCAN_SETTINGS_RESPONSE_BASE_ID
|
||||
| ((uint32_t)assemblySerial << 8U) | position;
|
||||
data.SettingsData.Data = &error;
|
||||
data.SettingsData.DataCount = 1U;
|
||||
return PROTOCAN_SEND_SETTINGS(id, data.SettingsData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработка и ответ на запрос общего адресного пространства.
|
||||
* Формирует и передает сообщение с данными о статусе "GAS-XXXX", где XXXX — значение из сообщения _rxMsg.
|
||||
* @param _rxMsg Структура входящего сообщения, содержит идентификатор и тело сообщения.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToGeneralAddressSpace(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 8;
|
||||
data[0] = 'G';
|
||||
data[1] = 'A';
|
||||
data[2] = 'S';
|
||||
data[3] = '-';
|
||||
for(int i = 0; i < 4; i++)
|
||||
{
|
||||
unsigned sym = (_rxMsg.eID.Fields.MsgBody >> (12 - (i * 4))) & 0xF;
|
||||
if(sym >= 10)
|
||||
{
|
||||
data[4 + i] = sym % 10 + 'A';
|
||||
}
|
||||
else
|
||||
{
|
||||
data[4 + i] = sym + '0';
|
||||
}
|
||||
}
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Отправляет произвольный диапазон регистров в пакетах по, максимум, 4 регистра за раз через CAN.
|
||||
* Функция разбивает передаваемый массив регистров на пакеты по 4 или меньше и отправляет их по отдельности.
|
||||
* Для каждого пакета формируется уникальный MsgBody, равный адресу первого регистра в пакете.
|
||||
* @param priority Приоритет сообщения CAN. ProtoCan_Priority_TypeDef
|
||||
* @param regStartAdr Адрес первого регистра из всего массива.
|
||||
* @param data Указатель на массив регистров для отправки.
|
||||
* @param regCount Общее количество регистров для отправки.
|
||||
* @return Возвращает статус отправки: HAL_OK при успехе, или код ошибки HAL_StatusTypeDef при сбое.
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND_GENERAL_ADDRESS_SPACE(ProtoCanId_t id, uint16_t regStartAdr, uint16_t *data, uint8_t regCount)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t canData[8];
|
||||
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = id.BitAll;
|
||||
|
||||
uint8_t regsRemaining = regCount;
|
||||
uint8_t currentIndex = 0;
|
||||
|
||||
while(regsRemaining > 0)
|
||||
{
|
||||
uint8_t regsInPacket = (regsRemaining > 4) ? 4 : regsRemaining;
|
||||
|
||||
tmp_eID.Fields.MsgBody = regStartAdr + currentIndex;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = regsInPacket*2;
|
||||
|
||||
for(int i = 0; i < regsInPacket; i++)
|
||||
{
|
||||
canData[(i * 2)] = LowByteOfWord(data[currentIndex + i]);
|
||||
canData[(i * 2) + 1] = HighByteOfWord(data[currentIndex + i]);
|
||||
}
|
||||
|
||||
HAL_StatusTypeDef status = HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox);
|
||||
|
||||
if(status != HAL_OK)
|
||||
{
|
||||
return (PROTOCAN_StatusTypeDef)status;
|
||||
}
|
||||
|
||||
regsRemaining -= regsInPacket;
|
||||
currentIndex += regsInPacket;
|
||||
}
|
||||
return (PROTOCAN_StatusTypeDef)HAL_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработка запроса на основе типа Modbus.
|
||||
* В зависимости от типа сообщения, вызывает соответствующую функцию
|
||||
* для отправки Modbus-запроса по CAN-шине.
|
||||
* @param _rxMsg Структура входящего сообщения RXMsg.
|
||||
* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка).
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(struct RXMsg _rxMsg)
|
||||
{
|
||||
switch(_rxMsg.eID.Fields.MsgType)
|
||||
{
|
||||
case PROTOCAN_MSGTYPE_MODBUS_COIL:
|
||||
{
|
||||
return ProtoCanMsgToModbusCoil(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_MSGTYPE_MODBUS_DISCRETE:
|
||||
{
|
||||
return ProtoCanMsgToModbusDiscrete(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_MSGTYPE_MODBUS_HOLDING:
|
||||
{
|
||||
return ProtoCanMsgToModbusHolding(_rxMsg);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_MSGTYPE_MODBUS_INPUT:
|
||||
{
|
||||
return ProtoCanMsgToModbusInput(_rxMsg);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
//ERROR
|
||||
return PROTOCAN_ERROR;
|
||||
break;
|
||||
}
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief __weak Функция отправки ответа на запрос Coil Modbus по CAN.
|
||||
* Формирует CAN ответ на запрос Coil Modbus и отправляет его.
|
||||
* @param _rxMsg Структура входящего сообщения RXMsg.
|
||||
* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка).
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusCoil(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 8;
|
||||
msgBodyModbusType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
data[0] = 'M';
|
||||
data[1] = 'C';
|
||||
data[2] = ' ';
|
||||
data[3] = 'S';
|
||||
data[4] = msg.Fields.StrAdr;
|
||||
data[5] = ' ';
|
||||
data[6] = 'C';
|
||||
data[7] = msg.Fields.RegCount;
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief __weak Функция отправки ответа на запрос Discrete Modbus по CAN.
|
||||
* Формирует CAN ответ на запрос Discrete Modbus и отправляет его.
|
||||
* @param _rxMsg Структура входящего сообщения RXMsg.
|
||||
* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка).
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusDiscrete(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 8;
|
||||
msgBodyModbusType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
data[0] = 'M';
|
||||
data[1] = 'D';
|
||||
data[2] = ' ';
|
||||
data[3] = 'S';
|
||||
data[4] = msg.Fields.StrAdr;
|
||||
data[5] = ' ';
|
||||
data[6] = 'C';
|
||||
data[7] = msg.Fields.RegCount;
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief __weak Функция отправки ответа на запрос Holding Modbus по CAN.
|
||||
* Формирует CAN ответ на запрос Holding Modbus и отправляет его.
|
||||
* @param _rxMsg Структура входящего сообщения RXMsg.
|
||||
* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка).
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusHolding(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 8;
|
||||
msgBodyModbusType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
data[0] = 'M';
|
||||
data[1] = 'H';
|
||||
data[2] = ' ';
|
||||
data[3] = 'S';
|
||||
data[4] = msg.Fields.StrAdr;
|
||||
data[5] = ' ';
|
||||
data[6] = 'C';
|
||||
data[7] = msg.Fields.RegCount;
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief __weak Функция отправки ответа на запрос Input Modbus по CAN.
|
||||
* Формирует CAN ответ на запрос Input Modbus и отправляет его.
|
||||
* @param _rxMsg Структура входящего сообщения RXMsg.
|
||||
* @return HAL_StatusTypeDef Статус выполнения команды CAN (успех или ошибка).
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusInput(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 8;
|
||||
msgBodyModbusType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
data[0] = 'M';
|
||||
data[1] = 'I';
|
||||
data[2] = ' ';
|
||||
data[3] = 'S';
|
||||
data[4] = msg.Fields.StrAdr;
|
||||
data[5] = ' ';
|
||||
data[6] = 'C';
|
||||
data[7] = msg.Fields.RegCount;
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Отправляет данные Modbus через CAN-шину в соответствии с типом сообщения.
|
||||
* Эта функция формирует и отправляет Modbus-данные по CAN-шине, поддерживая
|
||||
* различные типы сообщений: Coil, Discrete, Holding Register и Input Register.
|
||||
* Для сообщений, превышающих 4 регистров, данные отправляются пакетами.
|
||||
* @param id Идентификатор сообщения, содержащий тип сообщения и другую информацию.
|
||||
* @param modbusData Структура с данными Modbus: адрес, количество регистров и сами данные.
|
||||
* @return PROTOCAN_StatusTypeDef Статус операции.
|
||||
* @details
|
||||
* В зависимости от типа Modbus (coil/discrete или holding/input), формируется
|
||||
* соответствующий CAN-пакет. Для coil/discrete отправляется один пакет, для holding/input —
|
||||
* несколько, по 4 регистра в пакете. В случае ошибок при добавлении сообщения API HAL возвращается ошибка.
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND_MODBUS(const ProtoCanId_t id, const struct ProtoCanModbusData modbusData)
|
||||
{
|
||||
ProtoCanId_t localId = id;
|
||||
uint8_t msgType = localId.Fields.MsgType;
|
||||
// Структура для отправки
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t canData[8];
|
||||
if(msgType == PROTOCAN_MSGTYPE_MODBUS_COIL || msgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE)
|
||||
{
|
||||
// Формируем body
|
||||
msgBodyModbusType body;
|
||||
body.Fields.StrAdr = modbusData.StrAdr;
|
||||
body.Fields.RegCount = modbusData.RegCount;
|
||||
// Устанавливаем коллизию ExtId с body.Body
|
||||
localId.Fields.MsgBody = (body.Body & 0xFFFF);
|
||||
// Обновляем ExtId
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
// Остальные настройки
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.DLC = modbusData.RegCount % 8 + 1;
|
||||
// Формируем CAN данные
|
||||
canData[0] = LowByteOfWord(modbusData.Data[0]);
|
||||
canData[1] = HighByteOfWord(modbusData.Data[0]);
|
||||
if (HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
}
|
||||
else if(msgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || msgType == PROTOCAN_MSGTYPE_MODBUS_INPUT)
|
||||
{
|
||||
uint16_t* dataPtr = modbusData.Data;
|
||||
unsigned totalRegs = modbusData.RegCount;
|
||||
unsigned regsProcessed = 0;
|
||||
unsigned startAddress = modbusData.StrAdr;
|
||||
while (regsProcessed < totalRegs)
|
||||
{
|
||||
uint8_t regsInPacket = (totalRegs - regsProcessed) > 4 ? 4 : (totalRegs - regsProcessed);
|
||||
// Формируем body
|
||||
msgBodyModbusType body;
|
||||
body.Fields.StrAdr = startAddress;
|
||||
body.Fields.RegCount = regsInPacket;
|
||||
// Обновляем ExtId
|
||||
localId.Fields.MsgBody = (body.Body & 0xFFFF);
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.DLC = regsInPacket * 2;
|
||||
// Добавляем регистры
|
||||
for (int i = 0; i < regsInPacket; i++)
|
||||
{
|
||||
canData[i * 2] = LowByteOfWord(dataPtr[regsProcessed + i]);
|
||||
canData[i * 2 + 1] = HighByteOfWord(dataPtr[regsProcessed + i]);
|
||||
}
|
||||
// Отправляем сообщение
|
||||
if(HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
regsProcessed += regsInPacket;
|
||||
startAddress += regsInPacket;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return PROTOCAN_ERROR; // Неверный тип сообщения
|
||||
}
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief __weak Функция отправки сообщения об ошибке.
|
||||
* Посылает CAN сообщение с кодом ошибки. Используется, когда необходимо оповестить о неуспешной операции.
|
||||
* @param _rxMsg Структура входящего сообщения RXMsg, данными которого формируется ответ-ошибка.
|
||||
* @return HAL_StatusTypeDef Статус выполнения команды CAN.
|
||||
*/
|
||||
__weak PROTOCAN_StatusTypeDef PROTOCAN_RequestError(struct RXMsg _rxMsg)
|
||||
{
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
tmp_eID.Fields.MsgType = PROTOCAN_MSGTYPE_ERROR;
|
||||
msgBodyErrorType msg;
|
||||
msg.Fields.ErrorCode = 0xFF;
|
||||
msg.Fields.Info = 0;
|
||||
tmp_eID.Fields.MsgBody = msg.Body;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 0;
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Отправляет сообщение об ошибке по CAN-шине.
|
||||
* Эта функция формирует и отправляет сообщение об ошибке с кодом и информацией,
|
||||
* объединёнными в поле MsgBody идентификатора сообщения, и передаёт его через CAN.
|
||||
* @param id Идентификатор сообщения, содержащий базовую информацию.
|
||||
* @param errorData Структура с данными ошибки: код и дополнительная информация.
|
||||
* @return PROTOCAN_StatusTypeDef Статус операции.
|
||||
* @details
|
||||
* В качестве данных передаётся 2 байта: старшие биты — информация об ошибке, младшие — код ошибки.
|
||||
* Размер данных в CANFrames устанавливается нулевым, так как все данные инкапсулированы в MsgBody.
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND_ERROR(const ProtoCanId_t id, const struct ProtoCanErrorData errorData)
|
||||
{
|
||||
ProtoCanId_t localId = id;
|
||||
// Структура для отправки
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
uint8_t canData[8];
|
||||
localId.Fields.MsgBody = ((errorData.Info << 8) | errorData.Code);
|
||||
// Обновляем ExtId
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
// Остальные настройки
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.DLC = 0;
|
||||
if(HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
return PROTOCAN_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Записывает полученное сообщение CAN в буфер rxMsg.
|
||||
* Копирует параметры расширенного ID, типы, длину данных и сам данные в указанный элемент буфера.
|
||||
* @param tmp_eID Расширенный ID сообщения.
|
||||
* @param tmp_IDE Тип идентификатора (например, CAN_ID_EXT).
|
||||
* @param tmp_RTR Тип запроса (например, CAN_RTR_DATA).
|
||||
* @param tmp_DLC Длина данных в сообщении.
|
||||
* @param tmp_DATA Указатель на массив данных, полученных из CAN.
|
||||
* @param tmp_LastStep Индекс позиции в буфере, куда записывать сообщение.
|
||||
*/
|
||||
void TakeRxMsgToBuffer(ProtoCanId_t tmp_eID, uint32_t tmp_IDE, uint32_t tmp_RTR, uint32_t tmp_DLC, uint8_t *tmp_DATA, uint16_t tmp_LastStep)
|
||||
{
|
||||
if(tmp_DLC > 8u)
|
||||
{
|
||||
tmp_DLC = 8u;
|
||||
}
|
||||
rxMsg[tmp_LastStep].eID.BitAll = tmp_eID.BitAll;
|
||||
rxMsg[tmp_LastStep].info.EXT = (tmp_IDE == CAN_ID_EXT) ? 1U : 0U;
|
||||
rxMsg[tmp_LastStep].info.RTR = (tmp_RTR == CAN_RTR_REMOTE) ? 1U : 0U;
|
||||
rxMsg[tmp_LastStep].DLC = tmp_DLC;
|
||||
for(uint32_t i = 0; i < tmp_DLC; i++)
|
||||
{
|
||||
rxMsg[tmp_LastStep].Data[i] = tmp_DATA[i];
|
||||
}
|
||||
/* Слот заполнен - только теперь публикуем его. Без барьера компилятору
|
||||
разрешено поднять запись LastStep выше записей в rxMsg, и обработчик
|
||||
увидит наполовину заполненное сообщение. */
|
||||
__DMB();
|
||||
LastStep = tmp_LastStep;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обработка сообщений, ожидающих в FIFO, по прерыванию HAL_CAN_RxFifo0MsgPendingCallback.
|
||||
* Читает все сообщения из FIFO, проверяет их тип, обновляет статус устройств сети или сохраняет сообщение в буфер.
|
||||
* @param hcan Указатель на структуру управления CAN-Hardware.
|
||||
*/
|
||||
void ProtoCanRxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
|
||||
{
|
||||
//Обработка всех сообщений в FIFO
|
||||
CAN_RxHeaderTypeDef RxHeader;
|
||||
uint8_t RCAN_Data[8];
|
||||
while(HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RCAN_Data) == HAL_OK)
|
||||
{
|
||||
//Расширенный ID
|
||||
if(RxHeader.IDE == CAN_ID_EXT)
|
||||
{
|
||||
ProtoCanId_t ExtID_Of_RX_MSG;
|
||||
PROTOCAN_IdUnpack(RxHeader.ExtId, &ExtID_Of_RX_MSG);
|
||||
//Полученное сообщение - пульс устройств в сети.
|
||||
//Место в буфере не занимает, поэтому проверяется до проверки переполнения.
|
||||
if(ExtID_Of_RX_MSG.Fields.MsgType == PROTOCAN_MSGTYPE_PULSE)
|
||||
{
|
||||
Device_on_the_Network[ExtID_Of_RX_MSG.Fields.DeviceType][ExtID_Of_RX_MSG.Fields.DeviceID].Status = 1u;
|
||||
Device_on_the_Network[ExtID_Of_RX_MSG.Fields.DeviceType][ExtID_Of_RX_MSG.Fields.DeviceID].TimeFromLastPulse = 0;
|
||||
//continue, а не return: остаток FIFO нужно вычитать в этом же вызове,
|
||||
//иначе прерывание по непустому FIFO сработает повторно.
|
||||
continue;
|
||||
}
|
||||
//Полезная ёмкость буфера - (PROTOCAN_RX_BUFFER_SIZE - 1): при большем
|
||||
//заполнении счётчик AvailableCanRxMsg() переполняется и даёт 0 ("пусто").
|
||||
if(AvailableCanRxMsg() >= (PROTOCAN_RX_BUFFER_SIZE - 1u))
|
||||
{
|
||||
//Буфер переполнен, кадр отбрасывается. Кадр уже извлечён из
|
||||
//аппаратного FIFO, поэтому продолжаем вычитывать остаток.
|
||||
continue;
|
||||
}
|
||||
uint16_t tmp_LastStep = (uint16_t)((LastStep + 1u) % PROTOCAN_RX_BUFFER_SIZE);
|
||||
TakeRxMsgToBuffer(ExtID_Of_RX_MSG, RxHeader.IDE, RxHeader.RTR, RxHeader.DLC, RCAN_Data, tmp_LastStep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Функция отправки пульса устройства.
|
||||
* @details Пульс устройства. Есть возможность отключить пульс по запросу.
|
||||
*/
|
||||
void ProtoCanPulseCallback(TIM_HandleTypeDef *htim)
|
||||
{
|
||||
if(ControlFlags.IsPulse)
|
||||
{
|
||||
static unsigned PulseStage = 0;
|
||||
CAN_TxHeaderTypeDef TxHeader;
|
||||
uint32_t TxMailBox = 0;
|
||||
ProtoCanId_t currentID;
|
||||
currentID.BitAll = 0;
|
||||
currentID.Fields.Priority = PROTOCAN_PRIORITY_STANDARD;
|
||||
currentID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
currentID.Fields.DeviceType = CURRENT_TYPE_DEVICE;
|
||||
currentID.Fields.DeviceID = CURRENT_ID_DEVICE;
|
||||
currentID.Fields.MsgType = PROTOCAN_MSGTYPE_PULSE;
|
||||
currentID.Fields.MsgBody = 0;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(¤tID);
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
TxHeader.DLC = 1;
|
||||
PulseStage++;
|
||||
if(PulseStage > 0xFF)
|
||||
{
|
||||
PulseStage = 0;
|
||||
}
|
||||
data[0] = PulseStage;
|
||||
HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Функция установки в RTC полученной даты/время из запроса.
|
||||
* @param uint8_t *data - Байтовый массив, 7 элементов. [0] - Часы. [1] - Минуты. [2] - Секунды. [3] - Год. [4] - Месяц. [5] - Дата. [6] - День недели.
|
||||
*/
|
||||
void PROTOCAN_RTC_SYNC(uint8_t *data)
|
||||
{
|
||||
__HAL_RTC_WRITEPROTECTION_DISABLE(_HRTC);
|
||||
RTC_TimeTypeDef sTime = {0};
|
||||
RTC_DateTypeDef DateToUpdate = {0};
|
||||
sTime.Hours = data[0];
|
||||
sTime.Minutes = data[1];
|
||||
sTime.Seconds = data[2];
|
||||
if(HAL_RTC_SetTime(_HRTC, &sTime, RTC_FORMAT_BIN) != HAL_OK)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
DateToUpdate.Year = data[3];
|
||||
DateToUpdate.Month = data[4];
|
||||
DateToUpdate.Date = data[5];
|
||||
DateToUpdate.WeekDay = data[6];
|
||||
if(HAL_RTC_SetDate(_HRTC, &DateToUpdate, RTC_FORMAT_BIN) != HAL_OK)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
__HAL_RTC_WRITEPROTECTION_ENABLE(_HRTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Настройка CAN-Фильтра.
|
||||
* На основе переданных ID и маски настраивает фильтр CAN для приема только определённых сообщений.
|
||||
* @param filterBank Номер фильтра (обычно 0..27).
|
||||
* @param idFilter 32-битный идентификатор, используемый для фильтрации.
|
||||
* @param idMask 32-битная маска, определяющая биты, проверяемые фильтром.
|
||||
*/
|
||||
void PROTOCAN_CONFIG_FILTER(uint8_t filterBank, uint32_t idFilter, uint32_t idMask)
|
||||
{
|
||||
CAN_FilterTypeDef canFilterConfig;
|
||||
canFilterConfig.FilterBank = filterBank;
|
||||
canFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
|
||||
canFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
|
||||
canFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
|
||||
canFilterConfig.FilterActivation = ENABLE;
|
||||
canFilterConfig.SlaveStartFilterBank = 14;
|
||||
#define CAN_IDE_32 0b00000100 // Для 32-х битного масштаба
|
||||
// Разбиваем 32-битный ID и маску для фильтрации на High и Low 16 бит
|
||||
canFilterConfig.FilterIdHigh = (uint16_t)(((idFilter) >> 13)); // верхние 16 бит
|
||||
canFilterConfig.FilterIdLow = (uint16_t)((((idFilter) << 3)) | CAN_IDE_32); // низкие 16 бит,
|
||||
canFilterConfig.FilterMaskIdHigh = (uint16_t)(((idMask) >> 13));
|
||||
canFilterConfig.FilterMaskIdLow = (uint16_t)((((idMask) << 3)) | CAN_IDE_32);
|
||||
if(HAL_CAN_ConfigFilter(_HCAN, &canFilterConfig) != HAL_OK)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Конфигурирует три фильтра CAN для различных типов сообщений.
|
||||
*/
|
||||
void PROTOCAN_FILTERS()
|
||||
{
|
||||
#define BIT27 (1UL << 27)
|
||||
#define BITS_20_23_MASK (0xFUL << 20) // 4 бита (20..23)
|
||||
#define BITS_24_26_MASK (0x7UL << 24) // 3 бита (24..26)
|
||||
|
||||
/* Адресованный запрос приходит от ПМ, поэтому Route (бит 27) равен нулю. */
|
||||
uint32_t filter1_id = (CURRENT_TYPE_DEVICE << 24) | (CURRENT_ID_DEVICE << 20);
|
||||
uint32_t filter1_mask = BIT27 | BITS_24_26_MASK | BITS_20_23_MASK; // все проверяемые биты
|
||||
|
||||
#define BITS_16_19_MASK (0xFUL << 16)
|
||||
|
||||
uint32_t filter2_id = PROTOCAN_MSGTYPE_BROADCAST << 16;
|
||||
uint32_t filter2_mask = BITS_16_19_MASK;
|
||||
|
||||
uint32_t filter3_id = PROTOCAN_MSGTYPE_PULSE << 16;
|
||||
uint32_t filter3_mask = BITS_16_19_MASK;
|
||||
|
||||
// Первый фильтр: Route=0 (от ПМ), DeviceType и DeviceID текущего устройства.
|
||||
PROTOCAN_CONFIG_FILTER(0, filter1_id, filter1_mask);
|
||||
|
||||
// Второй фильтр: проверяем 16-19 биты, равны PROTOCAN_MSGTYPE_BROADCASTE
|
||||
PROTOCAN_CONFIG_FILTER(1, filter2_id, filter2_mask);
|
||||
|
||||
// Третий фильтр: проверяем 16-19 биты, равны PROTOCAN_MSGTYPE_PULSE
|
||||
PROTOCAN_CONFIG_FILTER(2, filter3_id, filter3_mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Отправляет CAN-сообщение в зависимости от типа сообщения.
|
||||
* Эта функция анализирует тип сообщения в идентификаторе и вызывает
|
||||
* соответствующую функцию отправки. В текущей реализации поддерживается только
|
||||
* тип PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE.
|
||||
* @param id Идентификатор CAN-сообщения, содержащий информацию о типе.
|
||||
* @param data Структура с данными, которые необходимо отправить. Зависит от типа сообщения.
|
||||
* @return Статус выполнения операции.
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND(ProtoCanId_t id, ProtoCanData_t data)
|
||||
{
|
||||
switch(id.Fields.MsgType)
|
||||
{
|
||||
case PROTOCAN_MSGTYPE_BROADCAST:
|
||||
case PROTOCAN_MSGTYPE_DISCRETE:
|
||||
case PROTOCAN_MSGTYPE_ANALOG:
|
||||
{
|
||||
return PROTOCAN_SEND_CORE(id, data.CoreData);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE:
|
||||
{
|
||||
/* Если есть регистры для отправки, указатель data должен быть валиден. */
|
||||
if((data.GeneralAddressSpaceData.RegCount > 0U) && data.GeneralAddressSpaceData.Data == 0)
|
||||
{
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
return PROTOCAN_SEND_GENERAL_ADDRESS_SPACE(id, data.GeneralAddressSpaceData.RegStartAdr,
|
||||
data.GeneralAddressSpaceData.Data,
|
||||
data.GeneralAddressSpaceData.RegCount);
|
||||
}
|
||||
case PROTOCAN_MSGTYPE_MODBUS_COIL:
|
||||
case PROTOCAN_MSGTYPE_MODBUS_DISCRETE:
|
||||
case PROTOCAN_MSGTYPE_MODBUS_HOLDING:
|
||||
case PROTOCAN_MSGTYPE_MODBUS_INPUT:
|
||||
{
|
||||
return PROTOCAN_SEND_MODBUS(id, data.ModbusData);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_MSGTYPE_ERROR:
|
||||
{
|
||||
return PROTOCAN_SEND_ERROR(id, data.ErrorData);
|
||||
break;
|
||||
}
|
||||
case PROTOCAN_MSGTYPE_SETTINGS:
|
||||
{
|
||||
return PROTOCAN_SEND_SETTINGS(id, data.SettingsData);
|
||||
}
|
||||
default:
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
return PROTOCAN_ERROR;
|
||||
}
|
||||
427
c/set-protocol/ports/stm32-bxcan/protocan.h
Normal file
427
c/set-protocol/ports/stm32-bxcan/protocan.h
Normal file
@@ -0,0 +1,427 @@
|
||||
//тест
|
||||
|
||||
#ifndef _protocan
|
||||
#define _protocan
|
||||
#include "main.h"
|
||||
#include "can.h"
|
||||
#include "pcan_id.h"
|
||||
|
||||
#define _DEMO
|
||||
|
||||
#define ONLINE 1;
|
||||
#define OFFLINE 0;
|
||||
|
||||
#define ID_MAIN_DEVICE 0x000
|
||||
#define CURRENT_TYPE_DEVICE 0b111
|
||||
#define CURRENT_ID_DEVICE 0b1111
|
||||
|
||||
#define PROTOCAN_RX_BUFFER_SIZE 128
|
||||
|
||||
#define SensorToModbusRegister(SensorType, SensorID) SensorType << 11 | SensorID
|
||||
#define HighByteOfWord(WORD) (WORD>>8)&0xFF
|
||||
#define LowByteOfWord(WORD) WORD&0xFF
|
||||
|
||||
/**
|
||||
* @brief Статус инициализации протокола CAN.
|
||||
*
|
||||
* Это перечисление описывает возможные статусы после попытки инициализации.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
PROTOCAN_INIT_OK = 0x00U, /**< Инициализация прошла успешно. */
|
||||
PROTOCAN_INIT_HCAN_ERROR = 0x01U, /**< Ошибка при инициализации модуля CAN. */
|
||||
PROTOCAN_INIT_HRTC_ERROR = 0x02U, /**< Ошибка при инициализации модуля RTC. */
|
||||
PROTOCAN_INIT_TIM_ERROR = 0x04U /**< Ошибка при инициализации таймера. */
|
||||
} PROTOCAN_INIT_StatusTypeDef;
|
||||
|
||||
/**
|
||||
* @brief Статус выполнения операции протокола CAN.
|
||||
*
|
||||
* Это перечисление описывает возможные результаты выполнения функций протокола.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
PROTOCAN_OK = 0x00U, /**< Операция прошла успешно. */
|
||||
PROTOCAN_ERROR = 0x01U, /**< Обнаружена ошибка во время выполнения. */
|
||||
PROTOCAN_BUSY = 0x02U, /**< Канал занят, выполнение невозможно в данный момент. */
|
||||
PROTOCAN_TIMEOUT = 0x03U /**< Истек таймаут ожидания. */
|
||||
} PROTOCAN_StatusTypeDef;
|
||||
|
||||
/**
|
||||
* @brief Уровень приоритета CAN-сообщения.
|
||||
*
|
||||
* Это перечисление определяет важность и приоритет обработки сообщения.
|
||||
*/
|
||||
typedef enum {
|
||||
PROTOCAN_PRIORITY_CRITICAL = 0, /**< Критический приоритет, рекомендуется обработка без задержек. */
|
||||
PROTOCAN_PRIORITY_STANDARD = 1 /**< Стандартный приоритет, менее важный. */
|
||||
} ProtoCanPriorityType;
|
||||
|
||||
/**
|
||||
* @brief Маршрут CAN-сообщения.
|
||||
*
|
||||
* Это перечисление задает направление маршрута отправки сообщения.
|
||||
*/
|
||||
typedef enum {
|
||||
PROTOCAN_ROUTE_FROM_PM = 0, /**< Сообщение отправляется от управляющего модуля (PM). */
|
||||
PROTOCAN_ROUTE_FROM_DEVICE = 1 /**< Сообщение отправляется от устройства (Device). */
|
||||
} ProtoCanRouteType;
|
||||
|
||||
/**
|
||||
* @brief Типы сообщений CAN.
|
||||
*
|
||||
* Перечисление определяет различные типы сообщений, используемые в протоколе.
|
||||
*/
|
||||
typedef enum {
|
||||
PROTOCAN_MSGTYPE_BROADCAST = 0b0000, /**< Широковещательное сообщение. */
|
||||
PROTOCAN_MSGTYPE_DISCRETE = 0b0001, /**< Дискретное цифровое сообщение. */
|
||||
PROTOCAN_MSGTYPE_ANALOG = 0b0010, /**< Аналоговое сообщение. */
|
||||
PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE = 0b0011, /**< Сообщение общего адресного пространства. */
|
||||
PROTOCAN_MSGTYPE_MODBUS_COIL = 0b0100, /**< Сообщение Modbus - Coil. */
|
||||
PROTOCAN_MSGTYPE_MODBUS_DISCRETE = 0b0101, /**< Сообщение Modbus - Discrete. */
|
||||
PROTOCAN_MSGTYPE_MODBUS_HOLDING = 0b0110, /**< Сообщение Modbus - Holding. */
|
||||
PROTOCAN_MSGTYPE_MODBUS_INPUT = 0b0111, /**< Сообщение Modbus - Input. */
|
||||
PROTOCAN_MSGTYPE_ERROR = 0b1000, /**< Сообщение об ошибке. */
|
||||
PROTOCAN_MSGTYPE_BOOT_CONTROL = 0b1001, /**< Команды ProtoCAN Boot. */
|
||||
PROTOCAN_MSGTYPE_BOOT_DATA_A = 0b1010, /**< Блок образа для слота A. */
|
||||
PROTOCAN_MSGTYPE_BOOT_DATA_B = 0b1011, /**< Блок образа для слота B. */
|
||||
PROTOCAN_MSGTYPE_BOOT_STATUS = 0b1100, /**< Статус команды загрузчика. */
|
||||
PROTOCAN_MSGTYPE_BOOT_DISCOVERY = 0b1101, /**< Идентификация загрузчика/приложения. */
|
||||
PROTOCAN_MSGTYPE_SETTINGS = 0b1110, /**< Привязка датчика к локации. */
|
||||
PROTOCAN_MSGTYPE_PULSE = 0b1111, /**< Импульсное сообщение. */
|
||||
}ProtoCanMsgType;
|
||||
|
||||
/** Длина ROM-кода 1-Wire и payload успешного ответа SETTINGS. */
|
||||
#define PROTOCAN_SETTINGS_ROM_SIZE 8U
|
||||
|
||||
/** Фиксированные базы выбранного DeviceType=0x7, DeviceID=0xF, MsgType=0xE. */
|
||||
#define PROTOCAN_SETTINGS_REQUEST_BASE_ID 0x17FE0000UL
|
||||
#define PROTOCAN_SETTINGS_RESPONSE_BASE_ID 0x1FFE0000UL
|
||||
#define PROTOCAN_SETTINGS_HEADER_MASK 0x1FFF0000UL
|
||||
|
||||
/** Операции SETTINGS, определяемые по направлению, DLC и payload. */
|
||||
typedef enum {
|
||||
PROTOCAN_SETTINGS_GET = 0,
|
||||
PROTOCAN_SETTINGS_WRITE = 1,
|
||||
PROTOCAN_SETTINGS_CLEAR = 2,
|
||||
PROTOCAN_SETTINGS_RESPONSE = 3,
|
||||
PROTOCAN_SETTINGS_ERROR = 4,
|
||||
PROTOCAN_SETTINGS_INVALID = 5
|
||||
} ProtoCanSettingsOperationType;
|
||||
|
||||
/** Коды результата SETTINGS в ответе с DLC=1. */
|
||||
typedef enum {
|
||||
PROTOCAN_SETTINGS_RESULT_OK = 0x00U,
|
||||
PROTOCAN_SETTINGS_RESULT_INVALID_DLC = 0x01U,
|
||||
PROTOCAN_SETTINGS_RESULT_INVALID_ROM_CRC = 0x02U,
|
||||
PROTOCAN_SETTINGS_RESULT_NOT_FOUND = 0x03U,
|
||||
PROTOCAN_SETTINGS_RESULT_ALREADY_ASSIGNED = 0x04U,
|
||||
PROTOCAN_SETTINGS_RESULT_EEPROM_ERROR = 0x05U,
|
||||
PROTOCAN_SETTINGS_RESULT_INVALID_LOCATION = 0x06U,
|
||||
PROTOCAN_SETTINGS_RESULT_BUSY = 0x07U
|
||||
} ProtoCanSettingsResultType;
|
||||
|
||||
/** Разобранная команда SETTINGS с локацией и ROM. */
|
||||
typedef struct {
|
||||
ProtoCanSettingsOperationType Operation;
|
||||
uint8_t AssemblySerial; /**< Body[15:8], номер сборки Z. */
|
||||
uint8_t Position; /**< Body[7:0], позиция Y. */
|
||||
ProtoCanSettingsResultType Result;
|
||||
uint8_t Rom[PROTOCAN_SETTINGS_ROM_SIZE];
|
||||
} ProtoCanSettingsMsg_t;
|
||||
|
||||
/**
|
||||
* @brief Типы широковещательных сообщений CAN.
|
||||
*
|
||||
* Перечисление задает различные типы широковещательных команд, используемых в протоколе.
|
||||
*/
|
||||
typedef enum {
|
||||
PROTOCAN_BROADCAST_STATUS = 0b000000000000, /**< Статус устройства. */
|
||||
PROTOCAN_BROADCAST_ONOFF = 0b000000000001, /**< Включение/выключение устройства. */
|
||||
PROTOCAN_BROADCAST_RESTARTDEVICE = 0b000000000010, /**< Перезагрузка устройства. */
|
||||
PROTOCAN_BROADCAST_RTCSETUP = 0b000000000011, /**< Настройка RTC (часы реального времени). */
|
||||
PROTOCAN_BROADCAST_END = 0b111111111111 /**< Конец диапазона широковещательных сообщений. */
|
||||
}ProtoCanBroadcastType;
|
||||
|
||||
/**
|
||||
* @brief Структура и представление широковещательного сообщения CAN.
|
||||
*
|
||||
* Объединение позволяет обращаться к данным сообщения либо как к отдельным полям, либо как к целому 16-битному значению.
|
||||
*/
|
||||
typedef union{
|
||||
struct msgBody_Broadcast{
|
||||
unsigned Body:4; /**< Поле для дополнительных данных или кода задачи (4 бита). */
|
||||
ProtoCanBroadcastType Type:12; /**< Тип широковещательного сообщения (12 бит). */
|
||||
} Fields; /**< Структура с разбивкой по полям. */
|
||||
unsigned Body; /**< Общее 16-битное представление сообщения. */
|
||||
} msgBodyBroadcastType;
|
||||
|
||||
/**
|
||||
* @brief Типы дискретных сообщений CAN.
|
||||
*
|
||||
* Перечисление задает различные виды дискретных команд или событий.
|
||||
*/
|
||||
typedef enum {
|
||||
PROTOCAN_DISCRETE_ACCIDENT = 0b0000, /**< Аварийное состояние. */
|
||||
PROTOCAN_DISCRETE_WARNING = 0b0001, /**< Предупредительный сигнал. */
|
||||
PROTOCAN_DISCRETE_CONTROL_SIGNALS = 0b0010, /**< Управляющие сигналы. */
|
||||
PROTOCAN_DISCRETE_FLAGS = 0b0011, /**< Флаги состояния. */
|
||||
PROTOCAN_DISCRETE_RESET = 0b0100, /**< Сброс устройства. */
|
||||
PROTOCAN_DISCRETE_CHANGE_MODE = 0b0101, /**< Смена режима работы. */
|
||||
PROTOCAN_DISCRETE_REQUEST_LIST_OF_PARAMETERS = 0b0110, /**< Запрос списка параметров. */
|
||||
PROTOCAN_DISCRETE_END = 0b1111 /**< Конец диапазона дискретных сообщений. */
|
||||
}ProtoCanDiscreteType;
|
||||
|
||||
/**
|
||||
* @brief Структура и представление дискретного сообщения CAN.
|
||||
*
|
||||
* Объединение позволяет обращаться к данным сообщения как к отдельным полям или как к целому 16-битному значению.
|
||||
*/
|
||||
typedef union{
|
||||
struct msgBody_Discrete{
|
||||
unsigned Body:12; /**< Поле данных или кода (12 бит). */
|
||||
ProtoCanDiscreteType Type:4; /**< Тип дискретного сообщения (4 бита). */
|
||||
} Fields; /**< Структура с разбивкой по полям. */
|
||||
unsigned Body; /**< Общее 16-битное представление сообщения. */
|
||||
} msgBodyDiscreteType;
|
||||
|
||||
/**
|
||||
* @brief Типы аналоговых сообщений CAN.
|
||||
*
|
||||
* Перечисление определяет различные типы аналоговых данных или команд.
|
||||
*/
|
||||
typedef enum {
|
||||
PROTOCAN_ANALOG_UNIVERSAL = 0b0000, /**< Универсальный тип. */
|
||||
PROTOCAN_ANALOG_SETTINGS = 0b0001, /**< Настройки. */
|
||||
PROTOCAN_ANALOG_U = 0b0010, /**< Напряжение (U). */
|
||||
PROTOCAN_ANALOG_I = 0b0011, /**< Ток (I). */
|
||||
PROTOCAN_ANALOG_T = 0b0100, /**< Температура (T). */
|
||||
PROTOCAN_ANALOG_END = 0b1111 /**< Конец диапазона типов. */
|
||||
}ProtoCanAnalogType;
|
||||
|
||||
/**
|
||||
* @brief Структура и представление аналогового сообщения CAN.
|
||||
*
|
||||
* Объединение позволяет обращаться к данным сообщения как к отдельным полям или как к целому 16-битному значению.
|
||||
*/
|
||||
typedef union{
|
||||
struct msgBody_Analog{
|
||||
unsigned SensorID:12; /**< Идентификатор датчика (12 бит). */
|
||||
ProtoCanAnalogType Type:4; /**< Тип аналоговых данных (4 бита). */
|
||||
} Fields; /**< Структура с разбивкой по полям. */
|
||||
unsigned Body; /**< Общее 16-битное представление сообщения. */
|
||||
}msgBodyAnalogType;
|
||||
|
||||
/**
|
||||
* @brief Структура и представление сообщения Modbus CAN.
|
||||
*
|
||||
* Объединение позволяет обращаться к данным как к отдельным полям или как к целому 16-битному значению.
|
||||
*/
|
||||
typedef union{
|
||||
struct msgBody_Modbus{
|
||||
unsigned RegCount:4; /**< Количество регистров (4 бита). */
|
||||
unsigned StrAdr:12; /**< Адрес строки или регистра (12 бит). */
|
||||
} Fields; /**< Поля структуры сообщения Modbus. */
|
||||
unsigned Body; /**< Общее 16-битное представление сообщения. */
|
||||
} msgBodyModbusType;
|
||||
|
||||
/**
|
||||
* @brief Структура и представление сообщения об ошибке CAN.
|
||||
*
|
||||
* Объединение позволяет обращаться к данным как к отдельным полям или как к целому 16-битному значению.
|
||||
*/
|
||||
typedef union{
|
||||
struct msgBody_Error{
|
||||
unsigned ErrorCode:8; /**< Код ошибки (8 бит). */
|
||||
unsigned Info:8; /**< Дополнительная информация (8 бит). */
|
||||
} Fields; /**< Поля ошибки. */
|
||||
unsigned Body; /**< Общее 16-битное представление сообщения об ошибке. */
|
||||
} msgBodyErrorType;
|
||||
|
||||
struct controlflags{
|
||||
unsigned IsPulse:1;
|
||||
unsigned IsRtrMode:1;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Поля идентификатора протокола CAN.
|
||||
*
|
||||
* Объединение позволяет обращаться к различным полям идентификатора, а также как к 29-битному целому числу.
|
||||
*/
|
||||
typedef union{
|
||||
struct protoCanIdFields{
|
||||
unsigned MsgBody:16; /**< Поле сообщения (16 бит). */
|
||||
ProtoCanMsgType MsgType:4; /**< Тип сообщения (4 бита). */
|
||||
unsigned DeviceID:4; /**< Идентификатор устройства (4 бита). */
|
||||
unsigned DeviceType:3; /**< Тип устройства (3 бита). */
|
||||
ProtoCanRouteType Route:1; /**< Маршрут (1 бит). */
|
||||
ProtoCanPriorityType Priority:1; /**< Приоритет (1 бит). */
|
||||
} Fields; /**< Структура с разбивкой по полям. */
|
||||
unsigned int BitAll:29; /**< Общее представление 29-битного идентификатора. */
|
||||
} ProtoCanId_t;
|
||||
|
||||
/**
|
||||
* Переходная граница legacy API -> переносимый формат линии.
|
||||
*
|
||||
* Прикладной код SETCAN пока может заполнять исторические Fields, но CAN ID
|
||||
* всегда собирается и разбирается общим pcan_id без зависимости от порядка
|
||||
* битовых полей конкретного компилятора.
|
||||
*/
|
||||
static inline uint32_t PROTOCAN_IdPack(const ProtoCanId_t *legacy)
|
||||
{
|
||||
pcan_id_t shared;
|
||||
shared.msg_body = (uint16_t)legacy->Fields.MsgBody;
|
||||
shared.msg_type = (uint8_t)legacy->Fields.MsgType;
|
||||
shared.device_id = (uint8_t)legacy->Fields.DeviceID;
|
||||
shared.device_type = (uint8_t)legacy->Fields.DeviceType;
|
||||
shared.route = (uint8_t)legacy->Fields.Route;
|
||||
shared.priority = (uint8_t)legacy->Fields.Priority;
|
||||
return pcan_id_pack(&shared);
|
||||
}
|
||||
|
||||
static inline void PROTOCAN_IdUnpack(uint32_t raw, ProtoCanId_t *legacy)
|
||||
{
|
||||
pcan_id_t shared;
|
||||
pcan_id_unpack(raw, &shared);
|
||||
legacy->BitAll = 0U;
|
||||
legacy->Fields.MsgBody = shared.msg_body;
|
||||
legacy->Fields.MsgType = (ProtoCanMsgType)shared.msg_type;
|
||||
legacy->Fields.DeviceID = shared.device_id;
|
||||
legacy->Fields.DeviceType = shared.device_type;
|
||||
legacy->Fields.Route = (ProtoCanRouteType)shared.route;
|
||||
legacy->Fields.Priority = (ProtoCanPriorityType)shared.priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Структура для общего адресного пространства CAN.
|
||||
*
|
||||
* Используется для хранения данных о регистре и его содержимом.
|
||||
*/
|
||||
typedef struct{
|
||||
struct ProtoCanCoreData{
|
||||
uint16_t Type;
|
||||
uint16_t Body;
|
||||
uint8_t *Data;
|
||||
unsigned DataCount;
|
||||
} CoreData;
|
||||
struct ProtoCanGeneralAddressSpaceData{
|
||||
uint16_t RegStartAdr; /**< Начальный адрес регистров. */
|
||||
uint16_t *Data; /**< Указатель на массив данных. */
|
||||
unsigned RegCount; /**< Количество регистров. */
|
||||
} GeneralAddressSpaceData;
|
||||
struct ProtoCanModbusData{
|
||||
uint16_t StrAdr;
|
||||
uint16_t *Data;
|
||||
unsigned RegCount:4;
|
||||
} ModbusData;
|
||||
struct ProtoCanErrorData{
|
||||
uint16_t Info;
|
||||
uint16_t Code;
|
||||
} ErrorData;
|
||||
struct ProtoCanSettingsData{
|
||||
const uint8_t *Data;
|
||||
uint8_t DataCount;
|
||||
} SettingsData;
|
||||
} ProtoCanData_t;
|
||||
|
||||
/**
|
||||
* @brief Структура данных устройства CAN.
|
||||
*
|
||||
* Включает статус, идентификатор, таймер последнего импульса и шаг последнего импульса.
|
||||
*/
|
||||
typedef struct{
|
||||
unsigned Status:1; /**< Статус устройства (флаг). */
|
||||
ProtoCanId_t Id; /**< Идентификатор CAN. */
|
||||
unsigned TimeFromLastPulse; /**< Время с последнего импульса (мс). */
|
||||
unsigned LastPulseStep; /**< Шаг последнего импульса. */
|
||||
} ProtoCanDevice_t;
|
||||
|
||||
/**
|
||||
* @brief Структура для хранения сообщения RXCAN.
|
||||
*/
|
||||
struct RXMsg {
|
||||
/**
|
||||
* @brief Структура, содержащая флаги информации.
|
||||
*/
|
||||
struct INFO {
|
||||
unsigned EXT : 1; /**< Бит расширенного идентификатора (EXT). */
|
||||
unsigned RTR : 1; /**< Бит запроса на передачу (RTR). */
|
||||
} info; /**< Информационные флаги сообщения. */
|
||||
ProtoCanId_t eID; /**< Идентификатор CAN-сообщения. */
|
||||
uint16_t DLC; /**< Длина данных (Data Length Code). */
|
||||
uint8_t Data[8]; /**< Массив данных (до 8 байт). */
|
||||
};
|
||||
|
||||
uint16_t AvailableCanRxMsg(void);
|
||||
|
||||
PROTOCAN_INIT_StatusTypeDef PROTOCAN_INIT(CAN_HandleTypeDef *tmp_hcan,
|
||||
RTC_HandleTypeDef *tmp_hrtc,
|
||||
TIM_HandleTypeDef *tmp_tim);
|
||||
void PROTOCAN_DEINIT(uint8_t stage);
|
||||
void PROTOCAN_FILTERS(void);
|
||||
void PROTOCAN_LoopProcessRxMsgs(void);
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_ProcessAllRxMsgs(void);
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_ProcessSingleRxMsg(void);
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND(ProtoCanId_t id, ProtoCanData_t data);
|
||||
|
||||
void ProtoCanPulseCallback(TIM_HandleTypeDef *htim);
|
||||
void ProtoCanRxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan);
|
||||
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_BroadcastProcessing(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastStatus(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastOnOff(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastRestart(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastRtcSetup(struct RXMsg _rxMsg);
|
||||
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_DiscreticProcessing(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteAccident(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteWarning(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteControlSignals(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteFlags(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteReset(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteChangeMode(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToDiscreteRequestListOfParameters(struct RXMsg _rxMsg);
|
||||
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_AnalogProcessing(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogUniversal(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogSettings(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogUSens(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogISens(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogTSens(struct RXMsg _rxMsg);
|
||||
|
||||
/** Разбирает запрос SETTINGS и вызывает прикладной обработчик. */
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SettingsProcessing(struct RXMsg _rxMsg);
|
||||
|
||||
/**
|
||||
* Прикладной обработчик GET/WRITE/CLEAR. Приложение должно переопределить
|
||||
* слабую реализацию и после проверки датчика/EEPROM отправить ответ или ошибку.
|
||||
*/
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToSettings(const ProtoCanSettingsMsg_t *message);
|
||||
|
||||
/** Отправляет произвольный SETTINGS-кадр с уже заполненным Body. */
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS(ProtoCanId_t id,
|
||||
const struct ProtoCanSettingsData data);
|
||||
|
||||
/** Отправляет успешный ответ 0x1FFEZZYY, DLC=8, с текущим ROM локации. */
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_RESPONSE(uint8_t assemblySerial,
|
||||
uint8_t position,
|
||||
const uint8_t *rom);
|
||||
|
||||
/** Отправляет ошибку 0x1FFEZZYY, DLC=1, Data[0]=result. */
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_ERROR(uint8_t assemblySerial,
|
||||
uint8_t position,
|
||||
ProtoCanSettingsResultType result);
|
||||
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToGeneralAddressSpace(struct RXMsg _rxMsg);
|
||||
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_RequestError(struct RXMsg _rxMsg);
|
||||
|
||||
PROTOCAN_StatusTypeDef PROTOCAN_ModbusProcessing(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToModbusCoil(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToModbusDiscrete(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToModbusHolding(struct RXMsg _rxMsg);
|
||||
PROTOCAN_StatusTypeDef ProtoCanMsgToModbusInput(struct RXMsg _rxMsg);
|
||||
|
||||
void PROTOCAN_RTC_SYNC(uint8_t *data);
|
||||
|
||||
#endif
|
||||
71
c/set-protocol/src/set_plot.c
Normal file
71
c/set-protocol/src/set_plot.c
Normal file
@@ -0,0 +1,71 @@
|
||||
#include "set_plot.h"
|
||||
#include <math.h>
|
||||
|
||||
static double clamp(double v, double lo, double hi) { return fmin(hi, fmax(lo, v)); }
|
||||
static int finite_values(const double *v, size_t n) {
|
||||
size_t i;
|
||||
for (i = 0; i < n; ++i) if (!isfinite(v[i])) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t set_plot_abi_version(void) { return 1U; }
|
||||
|
||||
size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t cap) {
|
||||
static const size_t sizes[] = {10, 3, 4, 4, 6, 2, 3};
|
||||
double span, fraction;
|
||||
if (op > SET_PLOT_DELTA || !v || !out || n != sizes[op] ||
|
||||
cap < (op == SET_PLOT_TRANSFORM ? 4U : 1U)) return 0;
|
||||
if (op == SET_PLOT_TRANSFORM) {
|
||||
double w, h, fx, fy;
|
||||
if (!finite_values(v, 4) || v[2] < 1.0/128 || v[2] > 1 ||
|
||||
v[3] < 1.0/128 || v[3] > 1 || v[0] < 0 || v[1] < 0 ||
|
||||
v[0] + v[2] > 1.000000000001 || v[1] + v[3] > 1.000000000001) return 0;
|
||||
out[0] = v[0]; out[1] = v[1]; out[2] = v[2]; out[3] = v[3];
|
||||
if (!finite_values(v + 4, 6) || v[4] <= 0 || v[5] <= 0) return 4;
|
||||
w = clamp(v[2] / v[4], 1.0/128, 1);
|
||||
h = clamp(v[3] / v[5], 1.0/128, 1);
|
||||
fx = clamp(v[8], 0, 1); fy = clamp(v[9], 0, 1);
|
||||
out[0] = clamp(v[0] + (v[2] - w) * fx - v[6] * w, 0, 1 - w);
|
||||
out[1] = clamp(v[1] + (v[3] - h) * fy - v[7] * h, 0, 1 - h);
|
||||
out[2] = w; out[3] = h;
|
||||
return 4;
|
||||
}
|
||||
if (!finite_values(v, n)) return 0;
|
||||
switch (op) {
|
||||
case SET_PLOT_AXIS:
|
||||
if (v[2] < 0) return 0;
|
||||
out[0] = fmax(fabs(v[0]), fabs(v[1])) <= v[2] ? 0 :
|
||||
(fabs(v[1]) > fabs(v[0]) ? 2 : 1);
|
||||
break;
|
||||
case SET_PLOT_FRACTION:
|
||||
case SET_PLOT_VALUE:
|
||||
span = v[2] - v[1];
|
||||
if (!isfinite(span) || span <= 0 || (v[3] != 0 && v[3] != 1)) return 0;
|
||||
if (op == SET_PLOT_FRACTION) {
|
||||
fraction = (v[0] - v[1]) / span;
|
||||
out[0] = v[3] ? 1 - fraction : fraction;
|
||||
} else {
|
||||
fraction = v[3] ? 1 - v[0] : v[0];
|
||||
out[0] = v[1] + fraction * span;
|
||||
}
|
||||
break;
|
||||
case SET_PLOT_DRAG:
|
||||
span = v[4] - v[3];
|
||||
if (!isfinite(span) || span <= 0 || v[2] <= 0 || (v[5] != 0 && v[5] != 1)) return 0;
|
||||
out[0] = clamp(v[0] + (v[5] ? -1 : 1) * (v[1] / v[2]) * span, v[3], v[4]);
|
||||
break;
|
||||
case SET_PLOT_TICK_STEP: {
|
||||
double raw, magnitude, scaled;
|
||||
if (v[0] <= 0 || v[1] <= 0) return 0;
|
||||
raw = v[0] / fmax(2, floor(v[1] / 100));
|
||||
magnitude = pow(10, floor(log10(raw)));
|
||||
if (magnitude <= 0 || !isfinite(magnitude)) return 0;
|
||||
scaled = raw / magnitude;
|
||||
out[0] = (scaled <= 1 ? 1 : scaled <= 2 ? 2 : scaled <= 5 ? 5 : 10) * magnitude;
|
||||
break;
|
||||
}
|
||||
case SET_PLOT_DELTA: out[0] = (v[1] - v[0]) * v[2]; break;
|
||||
default: return 0;
|
||||
}
|
||||
return isfinite(out[0]) ? 1 : 0;
|
||||
}
|
||||
126
c/set-protocol/src/set_spectrum.c
Normal file
126
c/set-protocol/src/set_spectrum.c
Normal file
@@ -0,0 +1,126 @@
|
||||
#include "set_spectrum.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define PI 3.14159265358979323846
|
||||
|
||||
static double window_value(int window, size_t index, size_t n)
|
||||
{
|
||||
double phase = 2.0 * PI * (double)index / (double)n;
|
||||
switch (window) {
|
||||
case SET_WINDOW_HANN: return 0.5 - 0.5 * cos(phase);
|
||||
case SET_WINDOW_HAMMING: return 0.54 - 0.46 * cos(phase);
|
||||
case SET_WINDOW_BLACKMAN: return 0.42 - 0.5 * cos(phase) + 0.08 * cos(2.0 * phase);
|
||||
case SET_WINDOW_FLATTOP: return 0.21557895 - 0.41663158 * cos(phase) +
|
||||
0.277263158 * cos(2.0 * phase) - 0.083578947 * cos(3.0 * phase) + 0.006947368 * cos(4.0 * phase);
|
||||
default: return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
/* RBJ biquads, https://www.w3.org/TR/audio-eq-cookbook/ (LP/HP: Q=1/sqrt(2)). */
|
||||
static void filter_block(double *data, size_t n, double fs, double cutoff, int kind)
|
||||
{
|
||||
double w = 2.0 * PI * cutoff / fs, c = cos(w);
|
||||
double alpha = sin(w) / (2.0 * (kind == SET_FILTER_NOTCH ? 30.0 : sqrt(0.5)));
|
||||
double a0 = 1.0 + alpha, a1 = -2.0 * c / a0, a2 = (1.0 - alpha) / a0;
|
||||
double b0, b1, b2;
|
||||
if (kind == SET_FILTER_LOW_PASS) {
|
||||
b0 = (1.0 - c) / (2.0 * a0); b1 = 2.0 * b0; b2 = b0;
|
||||
} else if (kind == SET_FILTER_HIGH_PASS) {
|
||||
b0 = (1.0 + c) / (2.0 * a0); b1 = -2.0 * b0; b2 = b0;
|
||||
} else { b0 = 1.0 / a0; b1 = -2.0 * c / a0; b2 = b0; }
|
||||
double x1 = data[0], x2 = x1;
|
||||
double y1 = kind == SET_FILTER_HIGH_PASS ? 0.0 : x1, y2 = y1;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
double x = data[i], y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
|
||||
data[i] = y; x2 = x1; x1 = x; y2 = y1; y1 = y;
|
||||
}
|
||||
}
|
||||
|
||||
static void fft(double *re, double *im, size_t n)
|
||||
{
|
||||
for (size_t i = 1, j = 0; i < n; ++i) {
|
||||
size_t bit = n >> 1;
|
||||
for (; j & bit; bit >>= 1) j ^= bit;
|
||||
j ^= bit;
|
||||
if (i < j) { double tmp = re[i]; re[i] = re[j]; re[j] = tmp; }
|
||||
}
|
||||
for (size_t length = 2; length <= n; length <<= 1) {
|
||||
double angle = -2.0 * PI / (double)length, wr = cos(angle), wi = sin(angle);
|
||||
for (size_t start = 0; start < n; start += length) {
|
||||
double tr = 1.0, ti = 0.0;
|
||||
for (size_t k = 0; k < length / 2; ++k) {
|
||||
size_t a = start + k, b = a + length / 2;
|
||||
double br = re[b] * tr - im[b] * ti, bi = re[b] * ti + im[b] * tr;
|
||||
re[b] = re[a] - br; im[b] = im[a] - bi; re[a] += br; im[a] += bi;
|
||||
double next = tr * wr - ti * wi;
|
||||
ti = tr * wi + ti * wr; tr = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int set_spectrum_analyze(const double *times, const double *values, size_t count,
|
||||
size_t max_size, int window, int filter, double low_hz, double high_hz, int remove_mean,
|
||||
double *amplitudes, size_t capacity, double *meta)
|
||||
{
|
||||
if (meta == NULL) return SET_SPECTRUM_INVALID;
|
||||
meta[0] = meta[1] = meta[2] = 0.0;
|
||||
if (times == NULL || values == NULL || amplitudes == NULL || max_size < 16 ||
|
||||
max_size > SET_SPECTRUM_MAX || (max_size & (max_size - 1)) != 0 ||
|
||||
window < SET_WINDOW_RECT || window > SET_WINDOW_FLATTOP ||
|
||||
filter < SET_FILTER_NONE || filter > SET_FILTER_NOTCH) return SET_SPECTRUM_INVALID;
|
||||
if (count < 16) return SET_SPECTRUM_SHORT;
|
||||
size_t n = 16;
|
||||
while (n * 2 <= count && n * 2 <= max_size) n *= 2;
|
||||
if (capacity < n / 2 + 1) return SET_SPECTRUM_INVALID;
|
||||
times += count - n; values += count - n;
|
||||
double dt = (times[n - 1] - times[0]) / (double)(n - 1);
|
||||
if (!isfinite(dt) || dt <= 0.0) return SET_SPECTRUM_TIMING;
|
||||
double fs = 1.0 / dt, jitter = 0.0;
|
||||
if (!isfinite(fs)) return SET_SPECTRUM_TIMING;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
if (!isfinite(values[i]) || !isfinite(times[i])) return SET_SPECTRUM_INVALID;
|
||||
if (i > 0) {
|
||||
double step = times[i] - times[i - 1];
|
||||
if (step <= 0.0) return SET_SPECTRUM_TIMING;
|
||||
double relative = fabs(step / dt - 1.0);
|
||||
if (relative > jitter) jitter = relative;
|
||||
}
|
||||
}
|
||||
meta[0] = (double)n; meta[1] = fs; meta[2] = jitter;
|
||||
if (jitter > 0.5) return SET_SPECTRUM_TIMING;
|
||||
int need_low = filter == SET_FILTER_HIGH_PASS || filter == SET_FILTER_BAND_PASS || filter == SET_FILTER_NOTCH;
|
||||
int need_high = filter == SET_FILTER_LOW_PASS || filter == SET_FILTER_BAND_PASS;
|
||||
if ((need_low && (!isfinite(low_hz) || low_hz <= 0.0 || low_hz >= fs / 2.0)) ||
|
||||
(need_high && (!isfinite(high_hz) || high_hz <= 0.0 || high_hz >= fs / 2.0)) ||
|
||||
(filter == SET_FILTER_BAND_PASS && low_hz >= high_hz)) return SET_SPECTRUM_CUTOFF;
|
||||
double *scratch = (double *)calloc(n * 2, sizeof(double));
|
||||
if (scratch == NULL) return SET_SPECTRUM_MEMORY;
|
||||
double *re = scratch, *im = scratch + n, mean = 0.0;
|
||||
size_t right = 1;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
double time = times[0] + (double)i * dt;
|
||||
while (right < n - 1 && times[right] < time) ++right;
|
||||
double fraction = (time - times[right - 1]) / (times[right] - times[right - 1]);
|
||||
fraction = fmax(0.0, fmin(1.0, fraction));
|
||||
re[i] = values[right - 1] * (1.0 - fraction) + values[right] * fraction;
|
||||
mean += re[i] / (double)n;
|
||||
}
|
||||
if (remove_mean) for (size_t i = 0; i < n; ++i) re[i] -= mean;
|
||||
if (filter == SET_FILTER_HIGH_PASS || filter == SET_FILTER_BAND_PASS)
|
||||
filter_block(re, n, fs, low_hz, SET_FILTER_HIGH_PASS);
|
||||
if (filter == SET_FILTER_LOW_PASS || filter == SET_FILTER_BAND_PASS)
|
||||
filter_block(re, n, fs, high_hz, SET_FILTER_LOW_PASS);
|
||||
if (filter == SET_FILTER_NOTCH) filter_block(re, n, fs, low_hz, SET_FILTER_NOTCH);
|
||||
double gain = 0.0;
|
||||
for (size_t i = 0; i < n; ++i) { double w = window_value(window, i, n); gain += w; re[i] *= w; }
|
||||
fft(re, im, n);
|
||||
int result = SET_SPECTRUM_OK;
|
||||
for (size_t i = 0; i <= n / 2; ++i) {
|
||||
amplitudes[i] = hypot(re[i], im[i]) / gain * ((i == 0 || i == n / 2) ? 1.0 : 2.0);
|
||||
if (!isfinite(amplitudes[i])) result = SET_SPECTRUM_INVALID;
|
||||
}
|
||||
free(scratch);
|
||||
return result;
|
||||
}
|
||||
74
c/set-protocol/src/set_trends.c
Normal file
74
c/set-protocol/src/set_trends.c
Normal file
@@ -0,0 +1,74 @@
|
||||
#include "set_trends.h"
|
||||
#include "pcan_id.h"
|
||||
|
||||
static uint16_t get16(const uint8_t *p)
|
||||
{
|
||||
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
|
||||
static void put16(uint8_t *p, uint16_t value)
|
||||
{
|
||||
p[0] = (uint8_t)value;
|
||||
p[1] = (uint8_t)(value >> 8);
|
||||
}
|
||||
|
||||
int32_t set_trend_word_value(uint16_t word, uint8_t is_signed)
|
||||
{
|
||||
return (is_signed != 0U && word >= 0x8000U) ? (int32_t)word - 65536 : (int32_t)word;
|
||||
}
|
||||
|
||||
int32_t set_trend_can_value(
|
||||
uint8_t source, uint32_t address, uint8_t device_type, uint8_t device,
|
||||
uint8_t byte_offset, uint8_t extended, uint8_t is_signed,
|
||||
uint32_t can_id, uint8_t flags, const uint8_t *data, size_t size)
|
||||
{
|
||||
size_t offset;
|
||||
const uint8_t ide = (uint8_t)(flags & 1U);
|
||||
if (data == NULL || size < 2U || size > 8U || (flags & 0x0EU) != 0U ||
|
||||
can_id > (ide != 0U ? 0x1FFFFFFFUL : 0x7FFUL)) return SET_TREND_NO_VALUE;
|
||||
if (source == SET_TREND_CAN_GAS) {
|
||||
pcan_id_t id;
|
||||
if (ide == 0U || (size & 1U) != 0U || address > 0xFFFFUL ||
|
||||
device_type > 7U || device > 15U) return SET_TREND_NO_VALUE;
|
||||
pcan_id_unpack(can_id, &id);
|
||||
if (id.msg_type != PCAN_MSG_GAS || id.route != PCAN_ROUTE_FROM_DEVICE ||
|
||||
id.device_type != device_type || id.device_id != device ||
|
||||
address < id.msg_body) return SET_TREND_NO_VALUE;
|
||||
offset = (size_t)(address - id.msg_body) * 2U;
|
||||
} else if (source == SET_TREND_CAN_RAW) {
|
||||
if (ide != (extended != 0U ? 1U : 0U) || can_id != address || byte_offset > 6U)
|
||||
return SET_TREND_NO_VALUE;
|
||||
offset = byte_offset;
|
||||
} else return SET_TREND_NO_VALUE;
|
||||
if (offset + 2U > size) return SET_TREND_NO_VALUE;
|
||||
return set_trend_word_value(get16(data + offset), is_signed);
|
||||
}
|
||||
|
||||
size_t set_trend_watch_request(uint16_t period_ms, const uint16_t *addresses,
|
||||
size_t count, uint8_t *output, size_t capacity)
|
||||
{
|
||||
size_t i;
|
||||
if (count > SET_TREND_WATCH_MAX || output == NULL || capacity < 4U + count * 2U ||
|
||||
(count > 0U && addresses == NULL)) return 0U;
|
||||
put16(output, period_ms);
|
||||
put16(output + 2U, (uint16_t)count);
|
||||
for (i = 0U; i < count; ++i) put16(output + 4U + i * 2U, addresses[i]);
|
||||
return 4U + count * 2U;
|
||||
}
|
||||
|
||||
int set_trend_watch_ack(const uint8_t *payload, size_t size, uint16_t period_ms, size_t count)
|
||||
{
|
||||
return payload != NULL && size == 4U && count <= SET_TREND_WATCH_MAX &&
|
||||
get16(payload) == period_ms && get16(payload + 2U) == count;
|
||||
}
|
||||
|
||||
int set_trend_watch_values(const uint8_t *payload, size_t size, uint16_t *words, size_t capacity)
|
||||
{
|
||||
size_t i, count;
|
||||
if (payload == NULL || size < 6U) return -1;
|
||||
count = get16(payload + 4U);
|
||||
if (count > SET_TREND_WATCH_MAX || size != 6U + count * 2U || count > capacity ||
|
||||
(count > 0U && words == NULL)) return -1;
|
||||
for (i = 0U; i < count; ++i) words[i] = get16(payload + 6U + i * 2U);
|
||||
return (int)count;
|
||||
}
|
||||
27
c/set-protocol/tests/fixtures/plot-v1.json
vendored
Normal file
27
c/set-protocol/tests/fixtures/plot-v1.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"version": 1,
|
||||
"cases": [
|
||||
{"name":"zoom_x_anchor","op":0,"input":[0,0,1,1,2,1,0,0,0.25,0.5],"output":[0.125,0,0.5,1]},
|
||||
{"name":"zoom_y_anchor","op":0,"input":[0,0,1,1,1,4,0,0,0.5,0.75],"output":[0,0.5625,1,0.25]},
|
||||
{"name":"pan_clamp","op":0,"input":[0.25,0.25,0.5,0.5,1,1,20,-20,0.5,0.5],"output":[0,0.5,0.5,0.5]},
|
||||
{"name":"zoom_limit","op":0,"input":[0,0,1,1,1000,1000,0,0,0.5,0.5],"output":[0.49609375,0.49609375,0.0078125,0.0078125]},
|
||||
{"name":"ignore_invalid_zoom","op":0,"input":[0.25,0.25,0.5,0.5,0,1,0,0,0.5,0.5],"output":[0.25,0.25,0.5,0.5]},
|
||||
{"name":"horizontal_spread","op":1,"input":[100,2,8],"output":[1]},
|
||||
{"name":"vertical_contract","op":1,"input":[2,-100,8],"output":[2]},
|
||||
{"name":"touch_jitter","op":1,"input":[3,-4,8],"output":[0]},
|
||||
{"name":"diagonal_tie_x","op":1,"input":[20,20,8],"output":[1]},
|
||||
{"name":"x_fraction","op":2,"input":[15,10,30,0],"output":[0.25]},
|
||||
{"name":"y_fraction","op":2,"input":[15,10,30,1],"output":[0.75]},
|
||||
{"name":"outside_view","op":2,"input":[40,10,30,0],"output":[1.5]},
|
||||
{"name":"y_coordinate","op":3,"input":[0.75,10,30,1],"output":[15]},
|
||||
{"name":"drag_x","op":4,"input":[15,50,200,10,30,0],"output":[20]},
|
||||
{"name":"drag_y","op":4,"input":[0,100,500,-10,10,1],"output":[-4]},
|
||||
{"name":"drag_clamp","op":4,"input":[15,500,200,10,30,0],"output":[30]},
|
||||
{"name":"ticks","op":5,"input":[100,800],"output":[20]},
|
||||
{"name":"negative_delta","op":6,"input":[10,0,1],"output":[-10]},
|
||||
{"name":"milliseconds","op":6,"input":[0,0.01,1000],"output":[10]},
|
||||
{"name":"zero_range","op":2,"input":[1,1,1,0],"output":null},
|
||||
{"name":"zero_pixels","op":4,"input":[0,1,0,0,1,0],"output":null},
|
||||
{"name":"bad_viewport","op":0,"input":[0,0,0,1,2,1,0,0,0.5,0.5],"output":null}
|
||||
]
|
||||
}
|
||||
19
c/set-protocol/tests/fixtures/trends-v1.json
vendored
Normal file
19
c/set-protocol/tests/fixtures/trends-v1.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"format": "setflash-trends",
|
||||
"version": 1,
|
||||
"profiles": {
|
||||
"TMS2812": [
|
||||
{"id":"tms-1","order":1,"name":"Ток фазы А","source":"TMS_MEMORY","address":"0x00000100","color":"#2F91FF","visible":true,"valueType":"INT16","deviceType":7,"device":13,"byteOffset":0,"extended":true}
|
||||
],
|
||||
"SET_V1": [
|
||||
{"id":"set-gas-1","order":1,"name":"Регистр GAS","source":"SET_GAS","address":"0xFF00","color":"#52D6A4","visible":true,"valueType":"UINT16","deviceType":7,"device":13,"byteOffset":0,"extended":true},
|
||||
{"id":"sensor-1","order":2,"name":"Температура","source":"SET_SENSOR","address":"28-01-02-03-04-05-06-07","color":"#F8798D","visible":false,"valueType":"UINT16","deviceType":7,"device":13,"byteOffset":0,"extended":true}
|
||||
],
|
||||
"CAN_BRIDGE": [
|
||||
{"id":"gas-1","order":1,"name":"GAS узла 13","source":"CAN_GAS","address":"0x1235","color":"#FFB547","visible":true,"valueType":"INT16","deviceType":7,"device":13,"byteOffset":0,"extended":true}
|
||||
],
|
||||
"BALZAM_CAN": [
|
||||
{"id":"raw-1","order":3,"name":"Слово CAN","source":"CAN_RAW","address":"0x00BA0010","color":"#B79AFF","visible":true,"valueType":"UINT16","deviceType":7,"device":13,"byteOffset":6,"extended":true}
|
||||
]
|
||||
}
|
||||
}
|
||||
31
c/set-protocol/tests/test_plot.c
Normal file
31
c/set-protocol/tests/test_plot.c
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "set_plot.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main(void) {
|
||||
double output[5] = {0, 0, 0, 0, 12345};
|
||||
const double zoom[] = {0, 0, 1, 1, 2, 1, 0, 0, .25, .5};
|
||||
const double y[] = {15, 10, 30, 1};
|
||||
const double drag[] = {0, 100, 500, -10, 10, 1};
|
||||
double back[4], bad[] = {NAN, 10, 8};
|
||||
assert(set_plot_abi_version() == 1);
|
||||
assert(set_plot_eval(SET_PLOT_TRANSFORM, zoom, 10, output, 4) == 4);
|
||||
assert(output[0] == .125 && output[1] == 0 && output[2] == .5 && output[3] == 1);
|
||||
assert(output[4] == 12345);
|
||||
assert(set_plot_eval(SET_PLOT_TRANSFORM, zoom, 10, output, 3) == 0);
|
||||
assert(set_plot_eval(SET_PLOT_TRANSFORM, zoom, 9, output, 4) == 0);
|
||||
assert(set_plot_eval(999, zoom, 10, output, 4) == 0);
|
||||
assert(set_plot_eval(SET_PLOT_FRACTION, NULL, 4, output, 4) == 0);
|
||||
assert(set_plot_eval(SET_PLOT_FRACTION, y, 4, NULL, 4) == 0);
|
||||
assert(set_plot_eval(SET_PLOT_AXIS, bad, 3, output, 4) == 0);
|
||||
assert(set_plot_eval(SET_PLOT_FRACTION, y, 4, output, 4) == 1);
|
||||
assert(output[0] == .75);
|
||||
back[0] = output[0]; back[1] = y[1]; back[2] = y[2]; back[3] = y[3];
|
||||
assert(set_plot_eval(SET_PLOT_VALUE, back, 4, output, 4) == 1);
|
||||
assert(output[0] == y[0]);
|
||||
assert(set_plot_eval(SET_PLOT_DRAG, drag, 6, output, 4) == 1);
|
||||
assert(output[0] == -4);
|
||||
puts("shared plot: OK");
|
||||
return 0;
|
||||
}
|
||||
22
c/set-protocol/tests/test_spectrum.c
Normal file
22
c/set-protocol/tests/test_spectrum.c
Normal file
@@ -0,0 +1,22 @@
|
||||
#include "set_spectrum.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
double times[32], values[32], output[18], meta[3];
|
||||
for (size_t i = 0; i < 32; ++i) { times[i] = (double)i / 32; values[i] = sin(2 * 3.14159265358979323846 * 4 * times[i]); }
|
||||
output[17] = 12345;
|
||||
assert(set_spectrum_analyze(times, values, 32, 32, SET_WINDOW_HANN, 0, 0, 0, 1, output, 17, meta) == SET_SPECTRUM_OK);
|
||||
assert(meta[0] == 32 && meta[1] == 32);
|
||||
assert(fabs(output[4] - 1.0) < 1e-12 && output[17] == 12345);
|
||||
assert(set_spectrum_analyze(times, values, 32, 32, 0, 0, 0, 0, 1, output, 16, meta) == SET_SPECTRUM_INVALID);
|
||||
assert(set_spectrum_analyze(times, values, 15, 32, 0, 0, 0, 0, 1, output, 17, meta) == SET_SPECTRUM_SHORT);
|
||||
assert(set_spectrum_analyze(times, values, 32, 32, 0, 99, 0, 0, 1, output, 17, meta) == SET_SPECTRUM_INVALID);
|
||||
assert(set_spectrum_analyze(times, values, 32, 32, 0, SET_FILTER_LOW_PASS, 0, 16, 1, output, 17, meta) == SET_SPECTRUM_CUTOFF);
|
||||
times[12] = times[11];
|
||||
assert(set_spectrum_analyze(times, values, 32, 32, 0, 0, 0, 0, 1, output, 17, meta) == SET_SPECTRUM_TIMING);
|
||||
puts("shared spectrum: OK");
|
||||
return 0;
|
||||
}
|
||||
43
c/set-protocol/tests/test_trends.c
Normal file
43
c/set-protocol/tests/test_trends.c
Normal file
@@ -0,0 +1,43 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "set_trends.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const uint8_t data[] = {1, 0, 254, 255};
|
||||
const uint32_t id = 0x1FD31234UL; /* priority 1, from device, type 7, dev 13, GAS */
|
||||
assert(set_trend_can_value(1, 0x1235, 7, 13, 0, 1, 0, id, 1, data, 4) == 65534);
|
||||
assert(set_trend_can_value(1, 0x1235, 7, 13, 0, 1, 1, id, 1, data, 4) == -2);
|
||||
assert(set_trend_can_value(1, 0x1233, 7, 13, 0, 1, 0, id, 1, data, 4) == SET_TREND_NO_VALUE);
|
||||
assert(set_trend_can_value(1, 0x1236, 7, 13, 0, 1, 0, id, 1, data, 4) == SET_TREND_NO_VALUE);
|
||||
assert(set_trend_can_value(1, 0x1235, 6, 13, 0, 1, 0, id, 1, data, 4) == SET_TREND_NO_VALUE);
|
||||
assert(set_trend_can_value(1, 0x1235, 7, 12, 0, 1, 0, id, 1, data, 4) == SET_TREND_NO_VALUE);
|
||||
assert(set_trend_can_value(1, 0x1235, 7, 13, 0, 1, 0, id ^ 0x08000000UL, 1, data, 4) == SET_TREND_NO_VALUE);
|
||||
assert(set_trend_can_value(1, 0x1235, 7, 13, 0, 1, 0, id, 1, data, 3) == SET_TREND_NO_VALUE);
|
||||
for (unsigned flag = 2; flag <= 8; flag <<= 1) {
|
||||
assert(set_trend_can_value(1, 0x1235, 7, 13, 0, 1, 0, id, (uint8_t)(1 | flag), data, 4) == SET_TREND_NO_VALUE);
|
||||
}
|
||||
assert(set_trend_can_value(2, 0x321, 0, 0, 2, 0, 1, 0x321, 0, data, 4) == -2);
|
||||
assert(set_trend_can_value(2, 0x321, 0, 0, 3, 0, 1, 0x321, 0, data, 4) == SET_TREND_NO_VALUE);
|
||||
assert(set_trend_word_value(0x8000, 1) == -32768);
|
||||
assert(set_trend_word_value(0xFFFF, 0) == 65535);
|
||||
const uint16_t addresses[] = {0x1234, 0xFFFF};
|
||||
const uint8_t expected[] = {232, 3, 2, 0, 52, 18, 255, 255};
|
||||
uint8_t output[132];
|
||||
assert(set_trend_watch_request(1000, addresses, 2, output, sizeof(output)) == 8);
|
||||
assert(memcmp(expected, output, 8) == 0);
|
||||
assert(set_trend_watch_request(1000, addresses, 65, output, sizeof(output)) == 0);
|
||||
assert(set_trend_watch_request(1000, NULL, 2, output, sizeof(output)) == 0);
|
||||
assert(set_trend_watch_request(1000, addresses, 2, output, 7) == 0);
|
||||
assert(set_trend_watch_ack(expected, 4, 1000, 2));
|
||||
assert(!set_trend_watch_ack(expected, 4, 1000, 3));
|
||||
const uint8_t packet[] = {1, 2, 3, 4, 2, 0, 52, 18, 255, 255};
|
||||
uint16_t words[64];
|
||||
assert(set_trend_watch_values(packet, sizeof(packet), words, 64) == 2);
|
||||
assert(words[0] == 0x1234 && words[1] == 0xFFFF);
|
||||
assert(set_trend_watch_values(packet, sizeof(packet) - 1, words, 64) == -1);
|
||||
assert(set_trend_watch_values(packet, sizeof(packet), words, 1) == -1);
|
||||
puts("Shared trend tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -17,9 +17,10 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
INCLUDE = ROOT / "include"
|
||||
JNI_INCLUDES: list[Path] = []
|
||||
SOURCES = [
|
||||
ROOT / "src" / name for name in (
|
||||
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c",
|
||||
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c",
|
||||
"gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
|
||||
"pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",
|
||||
"pcan_gas.c",
|
||||
@@ -51,9 +52,10 @@ def _build_msvc(output: Path, build_dir: Path) -> None:
|
||||
raise SystemExit("Visual Studio C++ tools were not found")
|
||||
quoted_sources = " ".join(f'"{source}"' for source in SOURCES)
|
||||
import_library = build_dir / "setprotocol.lib"
|
||||
jni_flags = " ".join(f'/I"{path}"' for path in JNI_INCLUDES)
|
||||
command = (
|
||||
f'call "{vcvars}" && cl /nologo /W4 /std:c11 '
|
||||
f'/DPCAN_ABI_BUILD_DLL /I"{INCLUDE}" /LD {quoted_sources} '
|
||||
f'/DPCAN_ABI_BUILD_DLL /I"{INCLUDE}" {jni_flags} /LD {quoted_sources} '
|
||||
f'/Fe"{output}" /link /IMPLIB:"{import_library}"'
|
||||
)
|
||||
# shell=True is intentional on Windows: ``call`` must run the batch file
|
||||
@@ -66,15 +68,23 @@ def _build_cc(output: Path, build_dir: Path) -> None:
|
||||
if compiler is None:
|
||||
raise SystemExit("C compiler was not found")
|
||||
command = [compiler, "-std=c99", "-Wall", "-Wextra", "-Wpedantic",
|
||||
"-fPIC", "-shared", f"-I{INCLUDE}", *map(str, SOURCES),
|
||||
"-o", str(output)]
|
||||
"-fPIC", "-shared", f"-I{INCLUDE}",
|
||||
*(f"-I{path}" for path in JNI_INCLUDES), *map(str, SOURCES),
|
||||
"-o", str(output), "-lm"]
|
||||
subprocess.run(command, cwd=build_dir, check=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--java-home", type=Path, help="Include the plot JNI adapter for host JVM tests")
|
||||
args = parser.parse_args()
|
||||
if args.java_home:
|
||||
include = args.java_home.resolve() / "include"
|
||||
if not (include / "jni.h").is_file():
|
||||
parser.error("--java-home must contain include/jni.h")
|
||||
JNI_INCLUDES.extend([include, include / {"Windows": "win32", "Darwin": "darwin"}.get(platform.system(), "linux")])
|
||||
SOURCES.append(ROOT / "ports" / "android" / "set_plot_jni.c")
|
||||
output = args.output.resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
build_dir = output.parent / ".setprotocol-build"
|
||||
|
||||
@@ -10,12 +10,16 @@
|
||||
|---|---|
|
||||
| Однокадровые SETTINGS и bxCAN STM32F1 | [`c/can-sensor`](../../c/can-sensor) |
|
||||
| Единый SET protocol v2 | [`c/set-protocol`](../../c/set-protocol) |
|
||||
| Транспорт ProtoCAN | [`c/protocan-transport`](../../c/protocan-transport) |
|
||||
| Транспорт ProtoCAN | [`c/set-protocol`](../../c/set-protocol) |
|
||||
| STM32 HAL bxCAN/RTC/TIM, API `PROTOCAN_*` | [`c/set-protocol/ports/stm32-bxcan`](../../c/set-protocol/ports/stm32-bxcan/README.md) |
|
||||
| CAN-загрузчик | [`c/protocan-boot`](../../c/protocan-boot) |
|
||||
|
||||
Файлы `Inc/protocan.h` и `Src/protocan.c`, упомянутые в историческом
|
||||
[руководстве](index.html), больше не являются подключаемым исходным кодом.
|
||||
В новых проектах используются нормализованные модули из таблицы выше.
|
||||
[руководстве](index.html), перенесены в `c/set-protocol/ports/stm32-bxcan`
|
||||
с сохранением имён и API. Общий `pcan_id` подключается из этого же `templates`.
|
||||
Отдельный репозиторий SETCAN больше не требуется. Его история до `c8eec55`
|
||||
включена в Git-историю `templates`; документация здесь сохранена без
|
||||
перезаписи старым снимком.
|
||||
|
||||
## Документы
|
||||
|
||||
|
||||
159
python/protocan/plot.py
Normal file
159
python/protocan/plot.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""Plot interaction port. All numerical operations use templates' set_plot.c.
|
||||
|
||||
This module has no Qt dependency. The application supplies its SETProtocol CDLL.
|
||||
Units, clocks, acquisition, colours and rendering belong to the application.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
from dataclasses import dataclass, replace
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Axis(IntEnum):
|
||||
X = 1
|
||||
Y = 2
|
||||
|
||||
|
||||
class Marker(IntEnum):
|
||||
A = 0
|
||||
B = 1
|
||||
C = 2
|
||||
D = 3
|
||||
E = 4
|
||||
F = 5
|
||||
G = 6
|
||||
H = 7
|
||||
|
||||
@property
|
||||
def horizontal(self) -> bool:
|
||||
return self in (Marker.E, Marker.F, Marker.G, Marker.H)
|
||||
|
||||
|
||||
class PlotMath:
|
||||
"""Typed operations over the versioned allocation-free C ABI."""
|
||||
def __init__(self, library: ctypes.CDLL) -> None:
|
||||
self.library = library
|
||||
library.set_plot_abi_version.restype = ctypes.c_uint32
|
||||
library.set_plot_abi_version.argtypes = []
|
||||
if library.set_plot_abi_version() != 1:
|
||||
raise RuntimeError("Unsupported plot ABI")
|
||||
library.set_plot_eval.argtypes = [ctypes.c_uint32, ctypes.POINTER(ctypes.c_double),
|
||||
ctypes.c_size_t, ctypes.POINTER(ctypes.c_double), ctypes.c_size_t]
|
||||
library.set_plot_eval.restype = ctypes.c_size_t
|
||||
|
||||
def call(self, operation: int, *values: float) -> tuple:
|
||||
inputs = (ctypes.c_double * len(values))(*values)
|
||||
output = (ctypes.c_double * 4)()
|
||||
count = self.library.set_plot_eval(operation, inputs, len(values), output, 4)
|
||||
if not count:
|
||||
raise ValueError("Invalid plot operation %s" % operation)
|
||||
return tuple(output[:count])
|
||||
|
||||
def pinch_axis(self, dx: float, dy: float, slop: float) -> Optional[Axis]:
|
||||
value = int(self.call(1, dx, dy, slop)[0])
|
||||
return Axis(value) if value else None
|
||||
|
||||
def tick_step(self, span: float, pixels: float) -> float:
|
||||
return self.call(5, span, pixels)[0]
|
||||
|
||||
def delta(self, a: float, b: float, multiplier: float = 1) -> float:
|
||||
return self.call(6, a, b, multiplier)[0]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Viewport:
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
width: float = 1.0
|
||||
height: float = 1.0
|
||||
|
||||
def transform(self, core: PlotMath, zoom_x: float = 1, zoom_y: float = 1,
|
||||
pan_x: float = 0, pan_y: float = 0,
|
||||
focus_x: float = 0.5, focus_y: float = 0.5) -> "Viewport":
|
||||
return Viewport(*core.call(0, self.x, self.y, self.width, self.height,
|
||||
zoom_x, zoom_y, pan_x, pan_y, focus_x, focus_y))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Bounds:
|
||||
left: float
|
||||
right: float
|
||||
bottom: float
|
||||
top: float
|
||||
|
||||
def fraction(self, core: PlotMath, value: float, horizontal: bool) -> float:
|
||||
return core.call(2, value, self.bottom if horizontal else self.left,
|
||||
self.top if horizontal else self.right, int(horizontal))[0]
|
||||
|
||||
def value(self, core: PlotMath, fraction: float, horizontal: bool) -> float:
|
||||
return core.call(3, fraction, self.bottom if horizontal else self.left,
|
||||
self.top if horizontal else self.right, int(horizontal))[0]
|
||||
|
||||
def visible(self, core: PlotMath, viewport: Viewport) -> "Bounds":
|
||||
return Bounds(self.value(core, viewport.x, False),
|
||||
self.value(core, viewport.x + viewport.width, False),
|
||||
self.value(core, viewport.y + viewport.height, True),
|
||||
self.value(core, viewport.y, True))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Markers:
|
||||
x_enabled: bool = True
|
||||
y_enabled: bool = False
|
||||
selected: Marker = Marker.A
|
||||
a: Optional[float] = None
|
||||
b: Optional[float] = None
|
||||
c: Optional[float] = None
|
||||
d: Optional[float] = None
|
||||
e: Optional[float] = None
|
||||
f: Optional[float] = None
|
||||
g: Optional[float] = None
|
||||
h: Optional[float] = None
|
||||
|
||||
def value(self, marker: Marker) -> Optional[float]:
|
||||
return getattr(self, marker.name.lower())
|
||||
|
||||
def enabled(self, marker: Marker) -> bool:
|
||||
return self.y_enabled if marker.horizontal else self.x_enabled
|
||||
|
||||
def move(self, marker: Marker, value: float) -> "Markers":
|
||||
return replace(self, **{marker.name.lower(): value})
|
||||
|
||||
def positioned(self, core: PlotMath, bounds: Bounds) -> "Markers":
|
||||
result = self
|
||||
for marker, fraction in ((Marker.A, .2), (Marker.B, .4), (Marker.C, .6), (Marker.D, .8),
|
||||
(Marker.E, .2), (Marker.F, .4), (Marker.G, .6), (Marker.H, .8)):
|
||||
if result.value(marker) is None:
|
||||
result = result.move(marker, bounds.value(core, fraction, marker.horizontal))
|
||||
return result
|
||||
|
||||
def reset(self, core: PlotMath, bounds: Bounds) -> "Markers":
|
||||
return replace(self, a=None, b=None, c=None, d=None, e=None, f=None, g=None, h=None).positioned(core, bounds)
|
||||
|
||||
def fraction(self, core: PlotMath, marker: Marker, bounds: Bounds) -> float:
|
||||
value = self.value(marker)
|
||||
if value is None:
|
||||
raise ValueError("Marker has no position")
|
||||
return bounds.fraction(core, value, marker.horizontal)
|
||||
|
||||
def drag(self, core: PlotMath, marker: Marker, delta: float, length: float, bounds: Bounds) -> "Markers":
|
||||
value = core.call(4, self.value(marker), delta, length,
|
||||
bounds.bottom if marker.horizontal else bounds.left,
|
||||
bounds.top if marker.horizontal else bounds.right, int(marker.horizontal))[0]
|
||||
return replace(self.move(marker, value), selected=marker)
|
||||
|
||||
def hit(self, core: PlotMath, x: float, y: float, width: float, height: float,
|
||||
radius: float, bounds: Bounds) -> Optional[Marker]:
|
||||
if not (0 <= x <= width and 0 <= y <= height):
|
||||
return None
|
||||
candidates = []
|
||||
for marker in Marker:
|
||||
if not self.enabled(marker) or self.value(marker) is None:
|
||||
continue
|
||||
fraction = self.fraction(core, marker, bounds)
|
||||
distance = abs(y - fraction * height if marker.horizontal else x - fraction * width)
|
||||
if 0 <= fraction <= 1 and distance <= radius:
|
||||
candidates.append((distance, marker != self.selected, marker))
|
||||
return min(candidates)[2] if candidates else None
|
||||
@@ -66,6 +66,7 @@ class SettingsResult(IntEnum):
|
||||
EEPROM_ERROR = 0x05
|
||||
INVALID_LOCATION = 0x06
|
||||
BUSY = 0x07
|
||||
INVALID_VALUE = 0x08
|
||||
|
||||
|
||||
class BroadcastType(IntEnum):
|
||||
@@ -145,6 +146,7 @@ SETTINGS_RESULT_RU = {
|
||||
SettingsResult.EEPROM_ERROR: "ошибка EEPROM",
|
||||
SettingsResult.INVALID_LOCATION: "неверная локация",
|
||||
SettingsResult.BUSY: "устройство занято",
|
||||
SettingsResult.INVALID_VALUE: "значение настройки вне диапазона",
|
||||
}
|
||||
|
||||
BROADCAST_RU = {
|
||||
@@ -680,6 +682,30 @@ def _decode_settings(res: Decoded) -> None:
|
||||
(" SETTINGS.Position Y", "%d (0x%02X)" % (position, position)),
|
||||
]
|
||||
|
||||
# Зарезервированная локация прибора KONOR: runtime-настройки не являются
|
||||
# ROM датчика, хотя используют тот же однокадровый SETTINGS-транспорт.
|
||||
if assembly == 0xFF and position == 0x01:
|
||||
if res.dlc == 0 and res.id.pm == Route.FROM_PM:
|
||||
res.summary = "SETTINGS RUNTIME GET"
|
||||
return
|
||||
if res.dlc == 8:
|
||||
pulse_seconds = res.data[0]
|
||||
led_hz = int.from_bytes(res.data[1:3], "little")
|
||||
res.fields += [
|
||||
(" SETTINGS.Pulse period",
|
||||
"%d s" % pulse_seconds if pulse_seconds else "выключен"),
|
||||
(" SETTINGS.LED frequency", "%d Hz" % led_hz),
|
||||
]
|
||||
if led_hz > 500:
|
||||
res.warnings.append("Частота LED должна быть 0..500 Гц")
|
||||
if any(res.data[3:]):
|
||||
res.warnings.append("Резерв SETTINGS Data[3:8] должен быть нулевым")
|
||||
direction = "WRITE" if res.id.pm == Route.FROM_PM else "RESPONSE"
|
||||
period_text = "%d s" % pulse_seconds if pulse_seconds else "выключен"
|
||||
res.summary = ("SETTINGS RUNTIME %s: PULSE %s, LED %d Hz"
|
||||
% (direction, period_text, led_hz))
|
||||
return
|
||||
|
||||
if res.id.pm == Route.FROM_PM:
|
||||
if res.dlc == 0:
|
||||
operation = "GET"
|
||||
|
||||
71
python/protocan/spectrum.py
Normal file
71
python/protocan/spectrum.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Shared FFT adapter for desktop GUIs; no numpy/Qt dependency or duplicated DSP.
|
||||
|
||||
Pass NativeProtocol.lib (rebuilt with set_spectrum.c). Timestamp inputs are seconds.
|
||||
See c/set-protocol/include/set_spectrum.h for filtering and normalization semantics.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import ctypes
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class Window(IntEnum):
|
||||
RECT = 0
|
||||
HANN = 1
|
||||
HAMMING = 2
|
||||
BLACKMAN = 3
|
||||
FLATTOP = 4
|
||||
|
||||
|
||||
class Filter(IntEnum):
|
||||
NONE = 0
|
||||
LOW_PASS = 1
|
||||
HIGH_PASS = 2
|
||||
BAND_PASS = 3
|
||||
NOTCH = 4
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Spectrum:
|
||||
size: int
|
||||
sample_rate: float
|
||||
jitter: float
|
||||
amplitudes: tuple[float, ...]
|
||||
|
||||
@property
|
||||
def frequencies(self) -> tuple[float, ...]:
|
||||
return tuple(i * self.sample_rate / self.size for i in range(len(self.amplitudes)))
|
||||
|
||||
|
||||
class NativeSpectrum:
|
||||
def __init__(self, library: ctypes.CDLL):
|
||||
self.lib = library
|
||||
self._analyze = library.set_spectrum_analyze
|
||||
pointer = ctypes.POINTER(ctypes.c_double)
|
||||
self._analyze.argtypes = [pointer, pointer, ctypes.c_size_t, ctypes.c_size_t,
|
||||
ctypes.c_int, ctypes.c_int, ctypes.c_double, ctypes.c_double, ctypes.c_int,
|
||||
pointer, ctypes.c_size_t, pointer]
|
||||
self._analyze.restype = ctypes.c_int
|
||||
|
||||
def analyze(self, times, values, *, max_size=4096, window=Window.HANN, filter=Filter.NONE,
|
||||
low_hz=10.0, high_hz=100.0, remove_mean=True) -> Spectrum:
|
||||
if type(max_size) is not int or not 16 <= max_size <= 16384 or max_size & (max_size - 1):
|
||||
raise ValueError("FFT size must be a power of two in 16..16384")
|
||||
if len(times) != len(values):
|
||||
raise ValueError("Timestamp and value counts differ")
|
||||
count = min(len(times), max_size)
|
||||
origin = times[len(times) - count] if count else 0.0
|
||||
t = (ctypes.c_double * count)(*(value - origin for value in times[-count:])) if count else (ctypes.c_double * 0)()
|
||||
v = (ctypes.c_double * count)(*values[-count:]) if count else (ctypes.c_double * 0)()
|
||||
capacity = max_size // 2 + 1
|
||||
output, meta = (ctypes.c_double * capacity)(), (ctypes.c_double * 3)()
|
||||
status = self._analyze(t, v, count, max_size, int(window), int(filter),
|
||||
low_hz, high_hz, remove_mean, output, capacity, meta)
|
||||
if status:
|
||||
message = {1: "At least 16 samples are required", 2: "Invalid spectrum input",
|
||||
3: "Gaps, duplicate or irregular timestamps (>50% interval deviation)",
|
||||
4: f"Filter frequencies must be between 0 and Fs/2 ({meta[1] / 2:g} Hz)",
|
||||
5: "Cannot allocate FFT workspace"}
|
||||
raise ValueError(message.get(status, "FFT failed"))
|
||||
n = int(meta[0])
|
||||
return Spectrum(n, meta[1], meta[2], tuple(output[:n // 2 + 1]))
|
||||
192
python/protocan/trends.py
Normal file
192
python/protocan/trends.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""Portable trend configuration shared by SETGUI and Android (no Qt/Android).
|
||||
|
||||
Numeric CAN decoding uses set_trends.c through NativeTrends. JSON adapters
|
||||
on Python/Kotlin implement the same documented version-1 interchange contract.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import uuid
|
||||
from collections import deque
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Mapping
|
||||
|
||||
MAX_SIGNALS = 64
|
||||
MAX_POINTS = 600
|
||||
MAX_FILE_BYTES = 1024 * 1024
|
||||
PROFILE_SOURCES = {
|
||||
"TMS2812": ("TMS_MEMORY",),
|
||||
"SET_V1": ("SET_GAS", "SET_SENSOR"),
|
||||
"BALZAM_CAN": ("CAN_RAW",),
|
||||
**{name: ("CAN_GAS", "CAN_RAW") for name in
|
||||
("CAN_BRIDGE", "GS_USB_CAN", "SLCAN", "CANGAROO_SLCAN")},
|
||||
}
|
||||
|
||||
|
||||
def parse_address(value: str, maximum: int) -> int:
|
||||
text = value.strip()
|
||||
if not re.fullmatch(r"(?:0[xX][0-9a-fA-F]+|[0-9]+)", text):
|
||||
raise ValueError("Address must be decimal or explicit 0x hexadecimal")
|
||||
number = int(text[2:], 16) if text.lower().startswith("0x") else int(text, 10)
|
||||
if not 0 <= number <= maximum:
|
||||
raise ValueError("Address out of range")
|
||||
return number
|
||||
|
||||
|
||||
def normalize_rom(value: str) -> str:
|
||||
return value.strip().replace("-", "").replace(" ", "").upper()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TrendSignal:
|
||||
# Field spellings intentionally match the JSON/Kotlin contract.
|
||||
id: str
|
||||
order: int = 1
|
||||
name: str = "Тренд 1"
|
||||
source: str = "TMS_MEMORY"
|
||||
address: str = "0x00000100"
|
||||
color: str = "#2F91FF"
|
||||
visible: bool = True
|
||||
valueType: str = "UINT16"
|
||||
deviceType: int = 7
|
||||
device: int = 13
|
||||
byteOffset: int = 0
|
||||
extended: bool = True
|
||||
|
||||
@classmethod
|
||||
def new(cls, profile: str, existing: tuple = ()) -> "TrendSignal":
|
||||
order = next(number for number in range(1, 10000)
|
||||
if all(item.order != number for item in existing))
|
||||
source = PROFILE_SOURCES[profile][0]
|
||||
address = {"TMS_MEMORY": "0x00000100", "SET_GAS": "0x0000",
|
||||
"CAN_GAS": "0x0000", "CAN_RAW": "0x00BA0010"}[source]
|
||||
colors = ("#2F91FF", "#FFB547", "#52D6A4", "#F8798D",
|
||||
"#B79AFF", "#4AD9E8", "#E8DA68", "#E7ECF3")
|
||||
return cls(str(uuid.uuid4()), order, f"Тренд {order}", source,
|
||||
address, colors[(order - 1) % len(colors)])
|
||||
|
||||
def validate(self, profile: str) -> None:
|
||||
for field in ("id", "name", "source", "address", "color", "valueType"):
|
||||
if type(getattr(self, field)) is not str:
|
||||
raise ValueError(f"{field} must be a string")
|
||||
for field in ("order", "deviceType", "device", "byteOffset"):
|
||||
if type(getattr(self, field)) is not int:
|
||||
raise ValueError(f"{field} must be an integer")
|
||||
if type(self.visible) is not bool or type(self.extended) is not bool:
|
||||
raise ValueError("Visibility and CAN format must be boolean")
|
||||
if not self.id.strip() or len(self.id) > 80 or not 1 <= self.order <= 9999:
|
||||
raise ValueError("Invalid ID/order")
|
||||
if not self.name.strip() or len(self.name) > 100 or len(self.address) > 64:
|
||||
raise ValueError("Invalid name/address length")
|
||||
if self.source not in PROFILE_SOURCES.get(profile, ()):
|
||||
raise ValueError("Source does not match connection protocol")
|
||||
if not re.fullmatch(r"#[0-9a-fA-F]{6}", self.color):
|
||||
raise ValueError("Color must be #RRGGBB")
|
||||
if self.valueType not in ("UINT16", "INT16"):
|
||||
raise ValueError("Unsupported word type")
|
||||
if self.source == "SET_SENSOR":
|
||||
if not re.fullmatch(r"[0-9A-F]{16}", normalize_rom(self.address)):
|
||||
raise ValueError("ROM must contain 16 hexadecimal digits")
|
||||
else:
|
||||
maximum = {"TMS_MEMORY": 0xFFFFFFFF, "CAN_GAS": 0xFFFF,
|
||||
"SET_GAS": 0xFFFF, "CAN_RAW": 0x1FFFFFFF if self.extended else 0x7FF}[self.source]
|
||||
parse_address(self.address, maximum)
|
||||
if self.source == "CAN_GAS" and not (0 <= self.deviceType <= 7 and 0 <= self.device <= 15):
|
||||
raise ValueError("Invalid ProtoCAN device")
|
||||
if self.source == "CAN_RAW" and not 0 <= self.byteOffset <= 6:
|
||||
raise ValueError("Word offset must be 0..6")
|
||||
|
||||
def word_value(self, word: int) -> float:
|
||||
if not 0 <= word <= 65535:
|
||||
raise ValueError("Not a 16-bit word")
|
||||
return float(word - 65536 if self.valueType == "INT16" and word >= 32768 else word)
|
||||
|
||||
|
||||
def validate_settings(settings: Mapping[str, list[TrendSignal]]) -> None:
|
||||
ids = set()
|
||||
for profile, signals in settings.items():
|
||||
if profile not in PROFILE_SOURCES or len(signals) > MAX_SIGNALS:
|
||||
raise ValueError("Unknown profile or too many signals")
|
||||
orders = set()
|
||||
for signal in signals:
|
||||
signal.validate(profile)
|
||||
if signal.id in ids or signal.order in orders:
|
||||
raise ValueError("Duplicate trend ID or order")
|
||||
ids.add(signal.id)
|
||||
orders.add(signal.order)
|
||||
|
||||
|
||||
def encode_settings(settings: Mapping[str, list[TrendSignal]]) -> str:
|
||||
validate_settings(settings)
|
||||
return json.dumps({"format": "setflash-trends", "version": 1, "profiles": {
|
||||
profile: [asdict(signal) for signal in sorted(signals, key=lambda item: item.order)]
|
||||
for profile, signals in settings.items()}}, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def decode_settings(text: str) -> dict[str, list[TrendSignal]]:
|
||||
if len(text.encode("utf-8")) > MAX_FILE_BYTES:
|
||||
raise ValueError("Trend configuration exceeds 1 MiB")
|
||||
root = json.loads(text.removeprefix("\ufeff"))
|
||||
if not isinstance(root, dict) or root.get("format") != "setflash-trends" or type(root.get("version")) is not int or root["version"] != 1:
|
||||
raise ValueError("Unknown trend configuration format/version")
|
||||
profiles = root.get("profiles")
|
||||
if not isinstance(profiles, dict):
|
||||
raise ValueError("profiles must be an object")
|
||||
result = {}
|
||||
fields = set(TrendSignal.__dataclass_fields__)
|
||||
for profile, items in profiles.items():
|
||||
if not isinstance(items, list) or len(items) > MAX_SIGNALS:
|
||||
raise ValueError("Expected up to 64 signals")
|
||||
signals = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict) or not fields <= item.keys():
|
||||
raise ValueError("Missing signal fields")
|
||||
signals.append(TrendSignal(**{key: item[key] for key in fields}))
|
||||
result[profile] = signals
|
||||
validate_settings(result)
|
||||
return result
|
||||
|
||||
|
||||
class TrendHistory:
|
||||
"""Bounded per-signal monotonic-time history; visibility belongs to the renderer."""
|
||||
def __init__(self) -> None:
|
||||
self.series: dict[str, deque] = {}
|
||||
|
||||
def append(self, values: Mapping[str, float], timestamp_ms: int) -> None:
|
||||
for key, value in values.items():
|
||||
if math.isfinite(value):
|
||||
self.series.setdefault(key, deque(maxlen=MAX_POINTS)).append((timestamp_ms, value))
|
||||
|
||||
|
||||
class NativeTrends:
|
||||
"""Thin ctypes port: accepts NativeProtocol.lib or any loaded SETProtocol CDLL.
|
||||
|
||||
Existing older DLLs keep working for other protocol functions. This optional
|
||||
feature reports a clear error until the shared library is rebuilt.
|
||||
"""
|
||||
def __init__(self, library: ctypes.CDLL) -> None:
|
||||
self.lib = library
|
||||
try:
|
||||
self.decode = library.set_trend_can_value
|
||||
except AttributeError as error:
|
||||
raise RuntimeError("Rebuild SETProtocol with set_trends.c to use trends") from error
|
||||
self.decode.argtypes = [ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint8,
|
||||
ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8,
|
||||
ctypes.c_uint32, ctypes.c_uint8, ctypes.c_void_p, ctypes.c_size_t]
|
||||
self.decode.restype = ctypes.c_int32
|
||||
|
||||
def can_value(self, signal: TrendSignal, can_id: int, flags: int, data: bytes) -> float | None:
|
||||
if signal.source not in ("CAN_GAS", "CAN_RAW"):
|
||||
return None
|
||||
signal.validate("CAN_BRIDGE")
|
||||
if not 0 <= can_id <= 0x1FFFFFFF or not 0 <= flags <= 255 or len(data) > 8:
|
||||
return None
|
||||
payload = (ctypes.c_uint8 * len(data)).from_buffer_copy(data)
|
||||
value = self.decode(1 if signal.source == "CAN_GAS" else 2,
|
||||
parse_address(signal.address, 0x1FFFFFFF), signal.deviceType, signal.device,
|
||||
signal.byteOffset, signal.extended, signal.valueType == "INT16", can_id, flags,
|
||||
payload, len(data))
|
||||
return None if value == -2147483648 else float(value)
|
||||
50
python/tests/test_plot.py
Normal file
50
python/tests/test_plot.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Cross-port contract: Python/ctypes and Kotlin/JNI consume the same fixtures."""
|
||||
import ctypes
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from protocan.plot import Bounds, Marker, Markers, PlotMath, Viewport
|
||||
|
||||
|
||||
class PlotTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.core = PlotMath(ctypes.CDLL(os.environ["SETPROTOCOL_LIBRARY"]))
|
||||
|
||||
def test_shared_numeric_fixtures(self):
|
||||
path = Path(__file__).resolve().parents[2] / "c/set-protocol/tests/fixtures/plot-v1.json"
|
||||
for case in json.loads(path.read_text())["cases"]:
|
||||
with self.subTest(case=case["name"]):
|
||||
if case["output"] is None:
|
||||
with self.assertRaises(ValueError):
|
||||
self.core.call(case["op"], *case["input"])
|
||||
else:
|
||||
actual = self.core.call(case["op"], *case["input"])
|
||||
self.assertEqual(len(actual), len(case["output"]))
|
||||
for a, b in zip(actual, case["output"]):
|
||||
self.assertAlmostEqual(a, b, places=10)
|
||||
|
||||
def test_markers_stay_in_data_coordinates_and_cross(self):
|
||||
bounds = Bounds(1000, 2000, -10, 10)
|
||||
markers = Markers().positioned(self.core, bounds)
|
||||
zoomed = bounds.visible(self.core, Viewport(.25, .25, .5, .5))
|
||||
self.assertEqual(markers, markers.positioned(self.core, zoomed))
|
||||
moved = markers.drag(self.core, Marker.A, 50, 500, zoomed)
|
||||
self.assertAlmostEqual(moved.a, markers.a + 50)
|
||||
self.assertEqual(markers.b, moved.b)
|
||||
self.assertEqual(Marker.A, moved.hit(self.core, 0, 50, 500, 100, 20, zoomed))
|
||||
crossed = markers.move(Marker.A, 1900).move(Marker.B, 1100)
|
||||
self.assertEqual(-800, self.core.delta(crossed.a, crossed.b))
|
||||
|
||||
def test_invalid_numeric_inputs_do_not_escape_to_painter(self):
|
||||
for value in (math.nan, math.inf, -math.inf):
|
||||
self.assertEqual(Viewport(), Viewport().transform(self.core, zoom_x=value))
|
||||
with self.assertRaises(ValueError):
|
||||
self.core.pinch_axis(value, 10, 8)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
117
python/tests/test_spectrum.py
Normal file
117
python/tests/test_spectrum.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import cmath
|
||||
import ctypes
|
||||
import math
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from protocan.spectrum import Filter, NativeSpectrum, Window
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get("SETPROTOCOL_LIBRARY"), "Set SETPROTOCOL_LIBRARY to the built C library")
|
||||
class SpectrumTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.core = NativeSpectrum(ctypes.CDLL(os.environ["SETPROTOCOL_LIBRARY"]))
|
||||
|
||||
def sample(self, n=4096, fs=1024, frequencies=(64,), amplitude=1):
|
||||
times = [i / fs for i in range(n)]
|
||||
return times, [amplitude * sum(math.sin(2 * math.pi * f * t) for f in frequencies) for t in times]
|
||||
|
||||
def test_fft_matches_independent_direct_dft(self):
|
||||
times, _ = self.sample(32)
|
||||
values = [math.sin(i * 1.37) + 0.1 * i for i in range(32)]
|
||||
result = self.core.analyze(times, values, window=Window.RECT, remove_mean=False)
|
||||
for k, amplitude in enumerate(result.amplitudes):
|
||||
direct = abs(sum(v * cmath.exp(-2j * math.pi * k * i / 32) for i, v in enumerate(values))) / 32
|
||||
if k not in (0, 16):
|
||||
direct *= 2
|
||||
self.assertAlmostEqual(direct, amplitude, places=11)
|
||||
|
||||
def test_all_windows_preserve_bin_centered_peak_amplitude(self):
|
||||
times, values = self.sample(amplitude=3.25)
|
||||
for window in Window:
|
||||
with self.subTest(window=window):
|
||||
result = self.core.analyze(times, values, window=window)
|
||||
peak = max(range(len(result.amplitudes)), key=result.amplitudes.__getitem__)
|
||||
self.assertEqual(64, result.frequencies[peak])
|
||||
self.assertAlmostEqual(3.25, result.amplitudes[peak], places=9)
|
||||
|
||||
def test_dc_and_nyquist_are_not_doubled(self):
|
||||
times, _ = self.sample()
|
||||
result = self.core.analyze(times, [2.5] * len(times), remove_mean=False, window=Window.RECT)
|
||||
self.assertAlmostEqual(2.5, result.amplitudes[0], places=10)
|
||||
result = self.core.analyze(times, [3 * (-1) ** i for i in range(len(times))], window=Window.RECT)
|
||||
self.assertAlmostEqual(3, result.amplitudes[-1], places=10)
|
||||
result = self.core.analyze(times, [2.5] * len(times))
|
||||
self.assertLess(max(result.amplitudes), 1e-12)
|
||||
|
||||
def test_windows_suppress_far_leakage_and_flattop_recovers_off_bin_amplitude(self):
|
||||
times, values = self.sample(frequencies=(64.13,))
|
||||
rect = self.core.analyze(times, values, window=Window.RECT)
|
||||
hann = self.core.analyze(times, values, window=Window.HANN)
|
||||
flat = self.core.analyze(times, values, window=Window.FLATTOP)
|
||||
self.assertLess(hann.amplitudes[400], rect.amplitudes[400] / 100)
|
||||
self.assertAlmostEqual(1, max(flat.amplitudes), delta=0.002)
|
||||
|
||||
def test_filters_attenuate_expected_bands(self):
|
||||
times, values = self.sample(frequencies=(16, 64, 256))
|
||||
low = self.core.analyze(times, values, filter=Filter.LOW_PASS, high_hz=64)
|
||||
high = self.core.analyze(times, values, filter=Filter.HIGH_PASS, low_hz=64)
|
||||
band = self.core.analyze(times, values, filter=Filter.BAND_PASS, low_hz=32, high_hz=128)
|
||||
notch = self.core.analyze(times, values, filter=Filter.NOTCH, low_hz=64)
|
||||
at = lambda result, hz: result.amplitudes[int(hz / (result.sample_rate / result.size))]
|
||||
self.assertGreater(at(low, 16), 0.99)
|
||||
self.assertLess(at(low, 256), 0.05)
|
||||
self.assertAlmostEqual(1 / math.sqrt(2), at(low, 64), delta=0.001)
|
||||
self.assertLess(at(high, 16), 0.07)
|
||||
self.assertGreater(at(high, 256), 0.99)
|
||||
self.assertGreater(at(band, 64), 0.93)
|
||||
self.assertLess(at(band, 16), 0.25)
|
||||
self.assertLess(at(band, 256), 0.2)
|
||||
self.assertLess(at(notch, 64), 0.02)
|
||||
self.assertGreater(at(notch, 16), 0.99)
|
||||
|
||||
def test_timestamp_rate_not_requested_rate_and_jitter_interpolation(self):
|
||||
n, fs = 1024, 200
|
||||
times = [(i + (0.05 if i % 2 else 0)) / fs for i in range(n)]
|
||||
values = [2 * math.sin(2 * math.pi * (16 * fs / n) * t) for t in times]
|
||||
result = self.core.analyze(times, values)
|
||||
self.assertAlmostEqual((n - 1) / (times[-1] - times[0]), result.sample_rate)
|
||||
self.assertGreater(result.jitter, 0.04)
|
||||
self.assertAlmostEqual(2, result.amplitudes[16], delta=0.005)
|
||||
|
||||
def test_rejects_gaps_duplicates_bad_values_and_cutoffs(self):
|
||||
times, values = self.sample(128)
|
||||
for broken in ([0.0] * 128, times[:64] + [t + 1 for t in times[64:]], list(reversed(times))):
|
||||
with self.assertRaisesRegex(ValueError, "timestamps"):
|
||||
self.core.analyze(broken, values)
|
||||
for value in (float("nan"), float("inf")):
|
||||
with self.assertRaises(ValueError):
|
||||
self.core.analyze(times, values[:-1] + [value])
|
||||
for cutoff in (0, -1, 512, 1000, float("nan")):
|
||||
with self.assertRaisesRegex(ValueError, "Filter frequencies"):
|
||||
self.core.analyze(times, values, filter=Filter.LOW_PASS, high_hz=cutoff)
|
||||
with self.assertRaises(ValueError):
|
||||
self.core.analyze(times, values, filter=Filter.BAND_PASS, low_hz=100, high_hz=50)
|
||||
|
||||
def test_size_limits_tail_selection_and_inputs_unchanged(self):
|
||||
times, values = self.sample(1000)
|
||||
original = values[:]
|
||||
result = self.core.analyze(times, values)
|
||||
self.assertEqual(512, result.size)
|
||||
self.assertEqual(original, values)
|
||||
self.assertEqual(256, self.core.analyze(times, values, max_size=256).size)
|
||||
for n in (0, 1, 15):
|
||||
with self.assertRaisesRegex(ValueError, "16 samples"):
|
||||
self.core.analyze(times[:n], values[:n])
|
||||
for size in (0, 15, 1000, 32768):
|
||||
with self.assertRaises(ValueError):
|
||||
self.core.analyze(times, values, max_size=size)
|
||||
with self.assertRaises(ValueError):
|
||||
self.core.analyze(times[:-1], values)
|
||||
times, values = self.sample(20000)
|
||||
self.assertEqual(16384, self.core.analyze(times, values, max_size=16384).size)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user