Compare commits
5 Commits
codex/tren
...
codex/stm3
| Author | SHA1 | Date | |
|---|---|---|---|
| 20dbc2c721 | |||
| e95338d781 | |||
| daa3466a41 | |||
| c9cf797132 | |||
| de5dc18a43 |
@@ -13,7 +13,6 @@
|
||||
templates/
|
||||
c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты
|
||||
python/ модули на чистом Python 3.9+, только stdlib
|
||||
tools/ общие инструменты сборки и выпуска прошивок
|
||||
```
|
||||
|
||||
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
|
||||
@@ -35,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/stm32f407-devboard-v1`](c/set-protocol/ports/stm32f407-devboard-v1) | доступ SETGUI к Modbus-регистрам F407 через CAN485 DevBoard_V1 | `pcan_modbus_server`, STM32 HAL CAN | bxCAN FIFO0 и callbacks карты регистров |
|
||||
| [`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 в комплекте** |
|
||||
@@ -49,12 +49,6 @@ 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 |
|
||||
|
||||
### Инструменты
|
||||
|
||||
| Инструмент | Что делает |
|
||||
|---|---|
|
||||
| [`tools/firmware-publish`](tools/firmware-publish) | единый BAT и конфигурации для проверки и публикации `.hex` Keil / `.bin` CCS 12 в каталоге SETGUI |
|
||||
|
||||
Общие тренды для Android GUI и SETGUI: [формат JSON, C99-ядро и адаптеры](c/set-protocol/docs/GUI_TRENDS.md).
|
||||
Общие масштабирование и маркеры: [C99, JNI и Python/Qt](c/set-protocol/docs/GUI_PLOT.md).
|
||||
|
||||
|
||||
80
RULES.md
80
RULES.md
@@ -1,80 +0,0 @@
|
||||
# Правила общего кроссплатформенного кода
|
||||
|
||||
Этот репозиторий — единственный источник общих алгоритмов для `GUI_Android`,
|
||||
`SETGUI`, прошивок и будущих GUI. Копирование одной реализации между Kotlin,
|
||||
Python, C# или другим языком запрещено.
|
||||
|
||||
## 1. Граница C-ядра и порта
|
||||
|
||||
В `c/set-protocol` на C99 обязательно размещаются:
|
||||
|
||||
- форматы кадров и идентификаторов, CRC/checksum, endian-преобразования;
|
||||
- построение команд, разбор и проверка ответов;
|
||||
- автоматы обмена, сегментация, повтор, таймаутные состояния без системных часов;
|
||||
- общие вычисления, таблицы и каталоги, влияющие на поведение протокола;
|
||||
- проверка образов прошивки и других бинарных форматов.
|
||||
|
||||
Порт на языке GUI содержит только:
|
||||
|
||||
- вызовы C через стабильный ABI (`ctypes`, JNI, P/Invoke, Swift FFI и т. п.);
|
||||
- преобразование C-структур в модели языка без повторения алгоритма;
|
||||
- работу с USB, COM, Bluetooth, SocketCAN и API операционной системы;
|
||||
- жизненный цикл, потоки, разрешения, хранение настроек и UI;
|
||||
- локализованный текст и чисто визуальные преобразования.
|
||||
|
||||
Порт не вычисляет CRC, не собирает wire-пакет и не разбирает его поля заново.
|
||||
Если для функции C-ядро недоступно, приложение сообщает об ошибке сборки или
|
||||
загрузки. Алгоритмический fallback на языке GUI запрещён: он снова создаёт две
|
||||
версии протокола.
|
||||
|
||||
## 2. Разделение контроллеров
|
||||
|
||||
Профили контроллеров нельзя сливать по совпадению названия транспорта:
|
||||
|
||||
- **ПМ67 / TMS320F2812** — основной контроллер, собственные RS и CAN;
|
||||
- **ПМ35 / TMS320F28335 periph** — отдельный контроллер и отдельный CAN для
|
||||
настроечного терминала, а также собственный прямой RS232/485-протокол.
|
||||
|
||||
Выбор профиля выполняется в GUI, но выбранный профиль вызывает свой отдельный
|
||||
модуль C-ядра. Наличие одной CAN-линии не даёт права удалить или подменить
|
||||
другую.
|
||||
|
||||
## 3. Порядок изменения протокола
|
||||
|
||||
1. Добавить или изменить публичный заголовок и реализацию в
|
||||
`c/set-protocol/include` и `c/set-protocol/src`.
|
||||
2. Зафиксировать эталонные байты и ошибочные случаи в C-тесте.
|
||||
3. При необходимости расширить `pcan_abi.h`, сохраняя бинарную совместимость.
|
||||
4. Добавить тонкие порты в `ports/<platform>` и `python/`; в них не должно быть
|
||||
второго кодека.
|
||||
5. Одними и теми же векторами проверить C, Python и Android/JVM.
|
||||
6. Собрать SETGUI и Android с одним commit submodule `templates`.
|
||||
|
||||
Изменение только в одном GUI считается незавершённым. Сначала меняется
|
||||
`templates`, затем оба потребителя обновляют ссылку submodule на проверенный
|
||||
commit.
|
||||
|
||||
## 4. Требования к C-ядру
|
||||
|
||||
- C99, без зависимости от GUI и конкретной ОС.
|
||||
- Буферы и их размеры передаются явно; владение памятью остаётся у вызывающего.
|
||||
- Для MCU основная логика не требует heap, исключений или файловой системы.
|
||||
- Endian и размеры целых задаются через `stdint.h`, структуры wire-формата не
|
||||
передаются через ABI без явного стабильного представления.
|
||||
- Экспорт shared library идёт через `PCAN_ABI_API`; существующие символы не
|
||||
меняют смысл и сигнатуру.
|
||||
- Ошибки возвращаются детерминированным кодом и тестируются наряду с успехом.
|
||||
|
||||
## 5. Проверка на ревью
|
||||
|
||||
Изменение нельзя принимать, если ответ «да» хотя бы на один вопрос:
|
||||
|
||||
- появился одинаковый CRC/parser/builder в двух языках;
|
||||
- UI знает byte offset, endian или служебный байт wire-протокола;
|
||||
- Python и Kotlin содержат одинаковую таблицу команд, влияющую на обмен;
|
||||
- добавлен тихий fallback, поведение которого отличается от C;
|
||||
- обновлён один GUI без обновления и теста `templates`;
|
||||
- ПМ67 и ПМ35 сведены к одному соединению или одному состоянию контроллера.
|
||||
|
||||
Текущее состояние и очередь переноса перечислены в
|
||||
[`doc/CROSS_PLATFORM_AUDIT.md`](doc/CROSS_PLATFORM_AUDIT.md).
|
||||
@@ -18,7 +18,6 @@ set(SETPROTOCOL_V2_SOURCES
|
||||
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
|
||||
set(SETPROTOCOL_LEGACY_SOURCES
|
||||
src/balsam_can.c
|
||||
src/periph28335.c
|
||||
src/gui_catalog.c
|
||||
src/gui_frame.c
|
||||
src/pcan_abi.c
|
||||
@@ -27,6 +26,7 @@ set(SETPROTOCOL_LEGACY_SOURCES
|
||||
src/pcan_gas.c
|
||||
src/pcan_id.c
|
||||
src/pcan_link.c
|
||||
src/pcan_modbus_server.c
|
||||
src/pcan_ring.c
|
||||
)
|
||||
|
||||
@@ -89,6 +89,10 @@ if(SETP_BUILD_TESTS)
|
||||
target_link_libraries(test_transport PRIVATE setprotocol_static)
|
||||
add_test(NAME legacy_transport COMMAND test_transport)
|
||||
|
||||
add_executable(test_modbus_server tests/test_modbus_server.c)
|
||||
target_link_libraries(test_modbus_server PRIVATE setprotocol_static)
|
||||
add_test(NAME protocan_modbus_server COMMAND test_modbus_server)
|
||||
|
||||
add_executable(test_gui tests/test_gui.c)
|
||||
target_link_libraries(test_gui PRIVATE setprotocol_static)
|
||||
add_test(NAME legacy_gui COMMAND test_gui)
|
||||
@@ -99,9 +103,6 @@ if(SETP_BUILD_TESTS)
|
||||
add_executable(test_balsam_can tests/test_balsam_can.c)
|
||||
target_link_libraries(test_balsam_can PRIVATE setprotocol_static)
|
||||
add_test(NAME legacy_balsam_can COMMAND test_balsam_can)
|
||||
add_executable(test_periph28335 tests/test_periph28335.c)
|
||||
target_link_libraries(test_periph28335 PRIVATE setprotocol_static)
|
||||
add_test(NAME shared_periph28335 COMMAND test_periph28335)
|
||||
add_executable(test_trends tests/test_trends.c)
|
||||
target_link_libraries(test_trends PRIVATE setprotocol_static)
|
||||
add_test(NAME shared_trends COMMAND test_trends)
|
||||
|
||||
@@ -21,12 +21,6 @@ TCP, UART/DMA и аппаратный CAN подключаются портам
|
||||
Масштабирование, координаты и измерительные маркеры Android/SETGUI используют
|
||||
общий `set_plot.c`: [границы модулей, ABI и проверки](docs/GUI_PLOT.md).
|
||||
|
||||
Прямой терминал ПМ35/TMS320F28335 использует единый wire contract
|
||||
`Set_Terminal_28335`: функции MODBUS RTU `03/06`, 128 регистров и CRC16.
|
||||
Эталонные адаптеры находятся в
|
||||
[`python/protocan/periph28335.py`](../../python/protocan/periph28335.py) и
|
||||
[`ports/android/kotlin/.../periph28335`](ports/android/kotlin/ru/setcorp/setprotocol/periph28335).
|
||||
|
||||
## Структура
|
||||
|
||||
| Каталог | Назначение |
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
Численная логика находится в `include/set_plot.h` и `src/set_plot.c`.
|
||||
Это модуль C99 без Qt, Android, транспорта, динамической памяти и глобального
|
||||
состояния. Он собирается в существующую библиотеку SETProtocol; отдельная DLL
|
||||
для графиков не требуется. Версия ABI графиков — `set_plot_abi_version() == 1`;
|
||||
новые операции добавляются без изменения существующих значений и сигнатур.
|
||||
для графиков не требуется. Версия ABI графиков — `set_plot_abi_version() == 1`.
|
||||
|
||||
| Общее в templates | Адаптер приложения |
|
||||
|---|---|
|
||||
@@ -14,7 +13,6 @@
|
||||
| Перевод значения в долю экрана и обратно, инверсия Y | Canvas/QPainter и оформление шкал |
|
||||
| Перемещение маркера от начальной координаты, ограничение видимой областью | Захват линии пальцем/мышью, редактор положения |
|
||||
| Разность B−A, D−C и множитель единиц, шаги шкалы 1/2/5 | Подписи, цвета, миллисекунды/герцы/единицы сигнала |
|
||||
| Проверка абсолютных границ X/Y для фиксации осей | Диалог ввода и хранение отдельных границ времени/FFT |
|
||||
| Модели маркеров и их размещение в Kotlin/Python-портах | Жизненный цикл экрана, очистка и выбор источника |
|
||||
|
||||
Модель маркеров: A/B — координаты X и вертикальные линии во всю высоту поля;
|
||||
@@ -61,9 +59,8 @@ SETGUI: `ui/plot_interaction.py` адаптирует общий модуль к
|
||||
используются исходные метки приёма. Дискретные дорожки имеют общую X-шкалу,
|
||||
а маркеры уровня Y относятся к аналоговому полю.
|
||||
|
||||
Расчёт FFT не является частью этого модуля. Android и SETGUI вычисляют спектр через
|
||||
`set_spectrum.c`; там же находится общий поиск доминирующего узкополосного пика,
|
||||
а трёхсекундный таймер его отображения остаётся состоянием GUI. Вкладка спектра SETGUI получает готовые уровни в дБмВ от
|
||||
Расчёт FFT не является частью этого модуля. Android вычисляет спектр через
|
||||
`set_spectrum.c`; вкладка спектра SETGUI получает готовые уровни в дБмВ от
|
||||
прибора. Общая интерактивная часть не меняет эти данные или единицы.
|
||||
|
||||
## Проверка и сборка
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
| `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` (word/CAN/GAS request/ack/data); Python 3.9+, stdlib, без Qt |
|
||||
| `python/protocan/trends.py` | Модель, JSON, история и `NativeTrends`; Python 3.9+, stdlib, без Qt |
|
||||
| `tests/fixtures/trends-v1.json` | Один образец для тестов обоих GUI и обмена настройками |
|
||||
|
||||
## Формат файла
|
||||
@@ -44,8 +44,6 @@ UTF-8 JSON: `format = "setflash-trends"`, `version = 1`, `profiles` — слов
|
||||
|
||||
CAN — пассивный приём, без записи GAS и автоматической отправки запросов.
|
||||
GAS принимает только входящие FROM_DEVICE; TX/RTR/ошибки исключены.
|
||||
Полный `GAS_WATCH_DATA`, включая 32-битную метку прибора, разбирается функцией
|
||||
`set_trend_watch_decode`; UI не знает смещений полей и endian.
|
||||
SET GUI оформляет подписку в порядке адресов. ACK должен подтвердить весь
|
||||
список: при частичном принятии нельзя определить пропущенные адреса, поэтому
|
||||
строить график по смещённым индексам запрещено. При паузе порт приложения
|
||||
|
||||
@@ -73,34 +73,6 @@ PCAN_ABI_API const char *pcan_abi_balsam_device_name(uint8_t device);
|
||||
PCAN_ABI_API size_t pcan_abi_balsam_register_name(
|
||||
uint8_t device, uint16_t address, char *output, size_t output_size);
|
||||
|
||||
/* PM35/TMS320F28335 direct RS232/485 register terminal. */
|
||||
PCAN_ABI_API uint16_t pcan_abi_periph28335_crc16(
|
||||
const uint8_t *data, size_t size);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_append_crc(
|
||||
const uint8_t *payload, size_t payload_size,
|
||||
uint8_t *output, size_t output_size);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_build_read(
|
||||
uint8_t controller, uint16_t start, uint16_t count,
|
||||
uint8_t *output, size_t output_size);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_build_write(
|
||||
uint8_t controller, uint16_t address, uint16_t value,
|
||||
uint8_t *output, size_t output_size);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_build_command(
|
||||
uint8_t controller, uint8_t command_index,
|
||||
uint8_t *output, size_t output_size);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_expected_read_size(uint16_t count);
|
||||
PCAN_ABI_API int pcan_abi_periph28335_decode_read(
|
||||
const uint8_t *data, size_t size, uint8_t controller, uint16_t count,
|
||||
uint16_t *output, size_t output_count);
|
||||
PCAN_ABI_API int pcan_abi_periph28335_validate_write(
|
||||
const uint8_t *response, size_t response_size,
|
||||
const uint8_t *request, size_t request_size);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_project_count(void);
|
||||
PCAN_ABI_API const char *pcan_abi_periph28335_project_name(
|
||||
size_t project_index);
|
||||
PCAN_ABI_API const char *pcan_abi_periph28335_command_name(
|
||||
size_t project_index, size_t command_index);
|
||||
|
||||
PCAN_ABI_API size_t pcan_abi_frame_encode(uint8_t sequence, uint8_t flags,
|
||||
uint32_t can_id,
|
||||
const uint8_t *data, uint8_t dlc,
|
||||
|
||||
60
c/set-protocol/include/pcan_modbus_server.h
Normal file
60
c/set-protocol/include/pcan_modbus_server.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @file pcan_modbus_server.h
|
||||
* @brief Modbus register windows transported in one classic ProtoCAN frame.
|
||||
*/
|
||||
#ifndef PCAN_MODBUS_SERVER_H
|
||||
#define PCAN_MODBUS_SERVER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "pcan_frame.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
PCAN_MODBUS_COIL = 0x4U,
|
||||
PCAN_MODBUS_DISCRETE = 0x5U,
|
||||
PCAN_MODBUS_HOLDING = 0x6U,
|
||||
PCAN_MODBUS_INPUT = 0x7U
|
||||
} pcan_modbus_bank_t;
|
||||
|
||||
typedef bool (*pcan_modbus_read_fn)(void *user, pcan_modbus_bank_t bank,
|
||||
uint16_t address, uint16_t *value);
|
||||
typedef bool (*pcan_modbus_write_fn)(void *user, pcan_modbus_bank_t bank,
|
||||
uint16_t address, uint16_t value);
|
||||
|
||||
typedef struct {
|
||||
uint8_t device_type;
|
||||
uint8_t device_id;
|
||||
pcan_modbus_read_fn read;
|
||||
pcan_modbus_write_fn write;
|
||||
void *user;
|
||||
uint32_t requests;
|
||||
uint32_t responses;
|
||||
uint32_t rejected;
|
||||
} pcan_modbus_server_t;
|
||||
|
||||
typedef enum {
|
||||
PCAN_MODBUS_NOT_FOR_US = 0,
|
||||
PCAN_MODBUS_HANDLED_NO_RESPONSE,
|
||||
PCAN_MODBUS_RESPONSE
|
||||
} pcan_modbus_result_t;
|
||||
|
||||
bool pcan_modbus_server_init(pcan_modbus_server_t *server,
|
||||
uint8_t device_type, uint8_t device_id,
|
||||
pcan_modbus_read_fn read,
|
||||
pcan_modbus_write_fn write, void *user);
|
||||
|
||||
/** Zero DLC reads; data writes COIL/HOLDING. Register words are little-endian. */
|
||||
pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
|
||||
const pcan_frame_t *request,
|
||||
pcan_frame_t *response);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PCAN_MODBUS_SERVER_H */
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* @file periph28335.h
|
||||
* @brief Shared PM35/TMS320F28335 register protocol.
|
||||
*
|
||||
* The protocol is independent from PM67/TMS320F2812 CAN and RS channels.
|
||||
* It implements the direct RS232/485 register terminal used by PM35.
|
||||
*/
|
||||
#ifndef PERIPH28335_H
|
||||
#define PERIPH28335_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PERIPH28335_REGISTER_COUNT 128U
|
||||
#define PERIPH28335_REQUEST_SIZE 8U
|
||||
#define PERIPH28335_COMMAND_COUNT 17U
|
||||
|
||||
typedef enum {
|
||||
PERIPH28335_OK = 0,
|
||||
PERIPH28335_ERROR_ARGUMENT = -1,
|
||||
PERIPH28335_ERROR_RANGE = -2,
|
||||
PERIPH28335_ERROR_LENGTH = -3,
|
||||
PERIPH28335_ERROR_CRC = -4,
|
||||
PERIPH28335_ERROR_HEADER = -5,
|
||||
PERIPH28335_ERROR_CAPACITY = -6
|
||||
} periph28335_status_t;
|
||||
|
||||
uint16_t periph28335_crc16_modbus(const uint8_t *data, size_t size);
|
||||
|
||||
size_t periph28335_append_crc(const uint8_t *payload, size_t payload_size,
|
||||
uint8_t *output, size_t output_size);
|
||||
|
||||
size_t periph28335_build_read_registers(
|
||||
uint8_t controller, uint16_t start, uint16_t count,
|
||||
uint8_t *output, size_t output_size);
|
||||
|
||||
size_t periph28335_build_write_register(
|
||||
uint8_t controller, uint16_t address, uint16_t value,
|
||||
uint8_t *output, size_t output_size);
|
||||
|
||||
size_t periph28335_build_command(
|
||||
uint8_t controller, uint8_t command_index,
|
||||
uint8_t *output, size_t output_size);
|
||||
|
||||
size_t periph28335_expected_read_response_size(uint16_t count);
|
||||
|
||||
int periph28335_decode_read_response(
|
||||
const uint8_t *data, size_t size, uint8_t controller, uint16_t count,
|
||||
uint16_t *output, size_t output_count);
|
||||
|
||||
int periph28335_validate_write_response(
|
||||
const uint8_t *response, size_t response_size,
|
||||
const uint8_t *request, size_t request_size);
|
||||
|
||||
size_t periph28335_project_count(void);
|
||||
const char *periph28335_project_name(size_t project_index);
|
||||
const char *periph28335_command_name(size_t project_index,
|
||||
size_t command_index);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PERIPH28335_H */
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "pcan_gas.h"
|
||||
#include "pcan_id.h"
|
||||
#include "pcan_link.h"
|
||||
#include "pcan_modbus_server.h"
|
||||
#include "pcan_ring.h"
|
||||
|
||||
#endif /* PROTOCAN_TRANSPORT_H */
|
||||
|
||||
@@ -17,11 +17,10 @@ enum set_plot_operation {
|
||||
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 */
|
||||
SET_PLOT_DB_DELTA = 7, /* A,B -> 20*log10(abs(B/A)); zero is invalid */
|
||||
SET_PLOT_LIMITS = 8 /* xMin,xMax,yMin,yMax -> validated unchanged limits */
|
||||
SET_PLOT_DB_DELTA = 7 /* A,B -> 20*log10(abs(B/A)); zero is invalid */
|
||||
};
|
||||
|
||||
/** Version of this additive plot ABI, independently of the transport ABI. */
|
||||
/** 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
|
||||
|
||||
@@ -28,15 +28,6 @@ PCAN_ABI_API int set_spectrum_analyze(const double *times, const double *values,
|
||||
size_t max_size, int window, int filter, double low_hz, double high_hz, int remove_mean,
|
||||
double *amplitudes, size_t capacity, double *meta);
|
||||
|
||||
/** Find the strongest non-DC local maximum above both the absolute floor and
|
||||
* relative_threshold * median(non-DC amplitudes). The caller supplies scratch
|
||||
* storage of at least count-1 doubles. peak = {frequency_hz, amplitude}.
|
||||
* Returns 1 when found, 0 when no narrow-band peak exists, -1 on invalid input.
|
||||
*/
|
||||
PCAN_ABI_API int set_spectrum_dominant_peak(const double *amplitudes, size_t count,
|
||||
double bin_hz, double relative_threshold, double absolute_floor,
|
||||
double *scratch, size_t scratch_capacity, double *peak, size_t peak_capacity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -48,13 +48,6 @@ PCAN_ABI_API int set_trend_watch_ack(
|
||||
PCAN_ABI_API int set_trend_watch_values(
|
||||
const uint8_t *payload, size_t size, uint16_t *words, size_t capacity);
|
||||
|
||||
/** Decode the complete GAS_WATCH_DATA payload including its device timestamp.
|
||||
* This is the preferred ABI for GUI ports; the older values-only symbol remains
|
||||
* available for binary compatibility.
|
||||
*/
|
||||
PCAN_ABI_API int set_trend_watch_decode(const uint8_t *payload, size_t size,
|
||||
uint32_t *timestamp_ms, uint16_t *words, size_t capacity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#define SETPROTOCOL_H
|
||||
|
||||
#include "balsam_can.h"
|
||||
#include "periph28335.h"
|
||||
|
||||
/* Основной SET protocol v2. */
|
||||
#include "set_protocol.h"
|
||||
|
||||
@@ -13,7 +13,6 @@ LOCAL_SRC_FILES := \
|
||||
../../src/set_trends.c \
|
||||
../../src/set_spectrum.c \
|
||||
../../src/balsam_can.c \
|
||||
../../src/periph28335.c \
|
||||
../../src/gui_catalog.c \
|
||||
../../src/gui_frame.c \
|
||||
../../src/pcan_abi.c \
|
||||
|
||||
@@ -175,9 +175,7 @@ object CanBridgeProtocol {
|
||||
stats = Stats(
|
||||
frames = values[0],
|
||||
crcErrors = values[1],
|
||||
// The former Kotlin parser counted one resync step
|
||||
// for a rejected CRC frame in addition to stray bytes.
|
||||
resyncBytes = values[1] + values[2] + values[3],
|
||||
resyncBytes = values[2] + values[3],
|
||||
sequenceLost = values[4],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@ package ru.setcorp.setprotocol
|
||||
object NativeSetProtocol {
|
||||
val available: Boolean by lazy {
|
||||
runCatching {
|
||||
val hostLibrary = System.getProperty("setprotocol.library")
|
||||
?: System.getProperty("setplot.library")
|
||||
if (hostLibrary != null) System.load(hostLibrary)
|
||||
else System.loadLibrary("setprotocol")
|
||||
System.loadLibrary("setprotocol")
|
||||
nativeAbiVersion() == 1
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
@@ -16,18 +13,12 @@ object NativeSetProtocol {
|
||||
/** {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?
|
||||
/** {frequencyHz, amplitude}, empty when no narrow-band peak is present. */
|
||||
external fun nativeSpectrumPeak(amplitudes: DoubleArray, binHz: Double,
|
||||
relativeThreshold: Double, absoluteFloor: Double): 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 nativeTrendWatchAck(input: ByteArray, period: Int, count: Int): Boolean
|
||||
external fun nativeTrendWatchValues(input: ByteArray): IntArray?
|
||||
/** {unsigned timestampMs, word0, ...}. */
|
||||
external fun nativeTrendWatchDecode(input: ByteArray): LongArray?
|
||||
external fun nativePackId(
|
||||
priority: Int,
|
||||
route: Int,
|
||||
@@ -41,17 +32,6 @@ object NativeSetProtocol {
|
||||
external fun nativeBalsamDecode(canId: Long, input: ByteArray): IntArray?
|
||||
external fun nativeBalsamDeviceName(device: Int): String
|
||||
external fun nativeBalsamRegisterName(device: Int, address: Int): String
|
||||
external fun nativePeriph28335Crc16(input: ByteArray): Int
|
||||
external fun nativePeriph28335AppendCrc(input: ByteArray): ByteArray?
|
||||
external fun nativePeriph28335BuildRead(controller: Int, start: Int, count: Int): ByteArray?
|
||||
external fun nativePeriph28335BuildWrite(controller: Int, address: Int, value: Int): ByteArray?
|
||||
external fun nativePeriph28335BuildCommand(controller: Int, commandIndex: Int): ByteArray?
|
||||
external fun nativePeriph28335ExpectedReadSize(count: Int): Int
|
||||
external fun nativePeriph28335DecodeRead(input: ByteArray, controller: Int, count: Int): IntArray?
|
||||
external fun nativePeriph28335ValidateWrite(response: ByteArray, request: ByteArray): Int
|
||||
external fun nativePeriph28335ProjectCount(): Int
|
||||
external fun nativePeriph28335ProjectName(projectIndex: Int): String?
|
||||
external fun nativePeriph28335CommandName(projectIndex: Int, commandIndex: Int): String?
|
||||
external fun nativeEncodeFrame(
|
||||
sequence: Int,
|
||||
flags: Int,
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package ru.setcorp.setprotocol.periph28335
|
||||
|
||||
import ru.setcorp.setprotocol.NativeSetProtocol
|
||||
|
||||
/** Kotlin UI adapter; all PM35 wire logic and the command catalog live in C99. */
|
||||
object Periph28335Protocol {
|
||||
const val REGISTER_COUNT = 128
|
||||
const val DEFAULT_CONTROLLER = 16
|
||||
const val DEFAULT_BAUD_RATE = 115_200
|
||||
|
||||
val projectCommands: Map<String, List<String>> by lazy {
|
||||
requireNative()
|
||||
buildMap {
|
||||
repeat(NativeSetProtocol.nativePeriph28335ProjectCount()) { project ->
|
||||
val name = requireNotNull(
|
||||
NativeSetProtocol.nativePeriph28335ProjectName(project),
|
||||
) { "Повреждён каталог проектов ПМ35" }
|
||||
put(name, List(17) { command ->
|
||||
requireNotNull(
|
||||
NativeSetProtocol.nativePeriph28335CommandName(project, command),
|
||||
) { "Повреждён каталог команд ПМ35" }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun crc16Modbus(data: ByteArray, initial: Int = 0xFFFF): Int {
|
||||
require(initial == 0xFFFF) {
|
||||
"Произвольное начальное значение CRC не входит в протокол ПМ35"
|
||||
}
|
||||
requireNative()
|
||||
return NativeSetProtocol.nativePeriph28335Crc16(data)
|
||||
}
|
||||
|
||||
fun withCrc(payload: ByteArray): ByteArray {
|
||||
requireNative()
|
||||
return requireNotNull(NativeSetProtocol.nativePeriph28335AppendCrc(payload)) {
|
||||
"SETProtocol отклонил данные ПМ35"
|
||||
}
|
||||
}
|
||||
|
||||
fun buildReadRegisters(controller: Int, start: Int, count: Int): ByteArray {
|
||||
requireRange("Адрес контроллера", controller, 0xFF)
|
||||
requireRange("Начальный регистр", start, 0xFFFF)
|
||||
require(count in 1..REGISTER_COUNT && start + count <= REGISTER_COUNT) {
|
||||
"Диапазон регистров должен находиться в 0..127"
|
||||
}
|
||||
requireNative()
|
||||
return requireNotNull(
|
||||
NativeSetProtocol.nativePeriph28335BuildRead(controller, start, count),
|
||||
) { "SETProtocol отклонил запрос чтения ПМ35" }
|
||||
}
|
||||
|
||||
fun buildWriteRegister(controller: Int, address: Int, value: Int): ByteArray {
|
||||
requireRange("Адрес контроллера", controller, 0xFF)
|
||||
requireRange("Адрес регистра", address, REGISTER_COUNT - 1)
|
||||
requireRange("Значение", value, 0xFFFF)
|
||||
requireNative()
|
||||
return requireNotNull(
|
||||
NativeSetProtocol.nativePeriph28335BuildWrite(controller, address, value),
|
||||
) { "SETProtocol отклонил запрос записи ПМ35" }
|
||||
}
|
||||
|
||||
fun buildCommand(controller: Int, commandIndex: Int): ByteArray {
|
||||
require(commandIndex in 0..16) { "Номер команды должен быть в диапазоне 0..16" }
|
||||
requireNative()
|
||||
return requireNotNull(
|
||||
NativeSetProtocol.nativePeriph28335BuildCommand(controller, commandIndex),
|
||||
) { "SETProtocol отклонил команду ПМ35" }
|
||||
}
|
||||
|
||||
fun expectedReadResponseSize(count: Int): Int {
|
||||
require(count in 1..REGISTER_COUNT)
|
||||
requireNative()
|
||||
return NativeSetProtocol.nativePeriph28335ExpectedReadSize(count)
|
||||
}
|
||||
|
||||
fun decodeReadResponse(data: ByteArray, controller: Int, count: Int): List<Int> {
|
||||
val expected = expectedReadResponseSize(count)
|
||||
require(data.size == expected) { "Ожидалось $expected байт, получено ${data.size}" }
|
||||
requireNative()
|
||||
return requireNotNull(
|
||||
NativeSetProtocol.nativePeriph28335DecodeRead(data, controller, count),
|
||||
) { "Повреждён ответ ПМ35: заголовок, длина или CRC" }.toList()
|
||||
}
|
||||
|
||||
fun validateWriteResponse(data: ByteArray, request: ByteArray): Boolean {
|
||||
requireNative()
|
||||
return NativeSetProtocol.nativePeriph28335ValidateWrite(data, request) == 0
|
||||
}
|
||||
|
||||
// Presentation-only conversions stay in the GUI port; they do not define wire bytes.
|
||||
fun bitsLsbFirst(value: Int): List<Boolean> {
|
||||
requireRange("Значение", value, 0xFFFF)
|
||||
return (0 until 16).map { bit -> value and (1 shl bit) != 0 }
|
||||
}
|
||||
|
||||
fun wordFromBits(bits: List<Boolean>): Int {
|
||||
require(bits.size == 16) { "Должно быть ровно 16 бит" }
|
||||
return bits.foldIndexed(0) { bit, value, checked ->
|
||||
if (checked) value or (1 shl bit) else value
|
||||
}
|
||||
}
|
||||
|
||||
fun signedWord(value: Int): Int {
|
||||
requireRange("Значение", value, 0xFFFF)
|
||||
return if (value < 0x8000) value else value - 0x10000
|
||||
}
|
||||
|
||||
private fun requireNative() {
|
||||
check(NativeSetProtocol.available) { "Нативное ядро SETProtocol недоступно" }
|
||||
}
|
||||
|
||||
private fun requireRange(name: String, value: Int, maximum: Int) {
|
||||
require(value in 0..maximum) { "$name вне диапазона 0..$maximum" }
|
||||
}
|
||||
}
|
||||
@@ -10,28 +10,37 @@ object GuiGasWatch {
|
||||
|
||||
fun request(periodMs: Int, addresses: List<Int>): ByteArray {
|
||||
require(periodMs in 0..65535 && addresses.size <= 64 && addresses.all { it in 0..65535 })
|
||||
check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" }
|
||||
return requireNotNull(NativeSetProtocol.nativeTrendWatchRequest(periodMs, addresses.toIntArray()))
|
||||
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(NativeSetProtocol.available && NativeSetProtocol.nativeTrendWatchAck(payload, periodMs, count)) {
|
||||
require(payload.size == 4 && read16(payload, 0) == periodMs && read16(payload, 2) == count) {
|
||||
"Прибор принял не все адреса GAS. Проверьте карту регистров; отображение по неполной подписке невозможно"
|
||||
}
|
||||
}
|
||||
|
||||
fun values(payload: ByteArray, expectedCount: Int): List<Int> {
|
||||
check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" }
|
||||
val values = requireNotNull(NativeSetProtocol.nativeTrendWatchValues(payload)) { "Повреждён GAS_WATCH_DATA" }.toList()
|
||||
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
|
||||
}
|
||||
|
||||
data class Sample(val timestampMs: Long, val values: List<Int>)
|
||||
fun decode(payload: ByteArray, expectedCount: Int): Sample {
|
||||
check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" }
|
||||
val decoded = requireNotNull(NativeSetProtocol.nativeTrendWatchDecode(payload)) { "Повреждён GAS_WATCH_DATA" }
|
||||
require(decoded.size == expectedCount + 1) { "Число значений GAS не соответствует подписке" }
|
||||
return Sample(decoded[0], decoded.drop(1).map(Long::toInt))
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,3 @@ fun plotTickStep(range: Double, pixels: Double): Double = NativePlot.call(5, ran
|
||||
fun plotDelta(a: Double, b: Double, multiplier: Double = 1.0): Double = NativePlot.call(6, a, b, multiplier)[0]
|
||||
fun plotDbDelta(a: Double, b: Double): Double? =
|
||||
runCatching { NativePlot.call(7, a, b)[0] }.getOrNull()
|
||||
|
||||
data class PlotLimits(val xMin: Double, val xMax: Double, val yMin: Double, val yMax: Double) {
|
||||
fun validated(): PlotLimits {
|
||||
val values = NativePlot.call(8, xMin, xMax, yMin, yMax)
|
||||
return PlotLimits(values[0], values[1], values[2], values[3])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ 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"), TMS28335("TMS320F28335 / ПМ35"), CAN_BRIDGE("CAN ↔ RS485"),
|
||||
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"),
|
||||
}
|
||||
@@ -29,7 +29,7 @@ enum class TrendValueType(val title: String) {
|
||||
}
|
||||
|
||||
fun TrendProfile.trendSources(): List<TrendSource> = when (this) {
|
||||
TrendProfile.TMS2812, TrendProfile.TMS28335 -> listOf(TrendSource.TMS_MEMORY)
|
||||
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)
|
||||
|
||||
@@ -34,8 +34,6 @@ data class TrendSpectrum(
|
||||
val binHz: Double get() = if (size > 0) sampleRate / size else 0.0
|
||||
}
|
||||
|
||||
data class SpectrumPeak(val frequencyHz: Double, val amplitude: Double)
|
||||
|
||||
/** Math is implemented once in C and used unchanged by JNI and ctypes. */
|
||||
object SpectrumAnalyzer {
|
||||
fun analyze(points: List<TrendPoint>, options: SpectrumOptions): TrendSpectrum {
|
||||
@@ -66,12 +64,4 @@ object SpectrumAnalyzer {
|
||||
return TrendSpectrum(output[1].toInt(), output[2], output[3],
|
||||
if (error == null) output.drop(4) else emptyList(), error)
|
||||
}
|
||||
|
||||
fun dominantPeak(spectrum: TrendSpectrum, relativeThreshold: Double = 3.0,
|
||||
absoluteFloor: Double = 1e-6): SpectrumPeak? {
|
||||
if (spectrum.error != null || spectrum.size <= 0 || spectrum.amplitudes.size < 3) return null
|
||||
val result = NativeSetProtocol.nativeSpectrumPeak(spectrum.amplitudes.toDoubleArray(),
|
||||
spectrum.binHz, relativeThreshold, absoluteFloor) ?: return null
|
||||
return result.takeIf { it.size == 2 }?.let { SpectrumPeak(it[0], it[1]) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,182 +7,6 @@
|
||||
#include "set_trends.h"
|
||||
#include "set_spectrum.h"
|
||||
#include "balsam_can.h"
|
||||
#include "periph28335.h"
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335Crc16(
|
||||
JNIEnv *env, jobject self, jbyteArray input)
|
||||
{
|
||||
(void)self;
|
||||
if (input == NULL) return 0;
|
||||
jsize size = (*env)->GetArrayLength(env, input);
|
||||
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
|
||||
if ((data == NULL) && (size != 0)) return 0;
|
||||
uint16_t crc = periph28335_crc16_modbus((const uint8_t *)data, (size_t)size);
|
||||
if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
|
||||
return (jint)crc;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335AppendCrc(
|
||||
JNIEnv *env, jobject self, jbyteArray input)
|
||||
{
|
||||
(void)self;
|
||||
if (input == NULL) return NULL;
|
||||
jsize size = (*env)->GetArrayLength(env, input);
|
||||
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
|
||||
if ((data == NULL) && (size != 0)) return NULL;
|
||||
uint8_t *output = (uint8_t *)malloc((size_t)size + 2U);
|
||||
if (output == NULL) {
|
||||
if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
|
||||
return NULL;
|
||||
}
|
||||
size_t written = periph28335_append_crc(
|
||||
(const uint8_t *)data, (size_t)size, output, (size_t)size + 2U);
|
||||
if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
|
||||
if (written == 0U) { free(output); return NULL; }
|
||||
jbyteArray result = (*env)->NewByteArray(env, (jsize)written);
|
||||
if (result != NULL) (*env)->SetByteArrayRegion(
|
||||
env, result, 0, (jsize)written, (const jbyte *)output);
|
||||
free(output);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335BuildRead(
|
||||
JNIEnv *env, jobject self, jint controller, jint start, jint count)
|
||||
{
|
||||
(void)self;
|
||||
uint8_t output[PERIPH28335_REQUEST_SIZE];
|
||||
if (controller < 0 || controller > 255 || start < 0 || start > 65535 ||
|
||||
count < 0 || count > 65535) return NULL;
|
||||
size_t written = periph28335_build_read_registers(
|
||||
(uint8_t)controller, (uint16_t)start, (uint16_t)count,
|
||||
output, sizeof output);
|
||||
if (written == 0U) return NULL;
|
||||
jbyteArray result = (*env)->NewByteArray(env, (jsize)written);
|
||||
if (result != NULL) (*env)->SetByteArrayRegion(
|
||||
env, result, 0, (jsize)written, (const jbyte *)output);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335BuildWrite(
|
||||
JNIEnv *env, jobject self, jint controller, jint address, jint value)
|
||||
{
|
||||
(void)self;
|
||||
uint8_t output[PERIPH28335_REQUEST_SIZE];
|
||||
if (controller < 0 || controller > 255 || address < 0 || address > 65535 ||
|
||||
value < 0 || value > 65535) return NULL;
|
||||
size_t written = periph28335_build_write_register(
|
||||
(uint8_t)controller, (uint16_t)address, (uint16_t)value,
|
||||
output, sizeof output);
|
||||
if (written == 0U) return NULL;
|
||||
jbyteArray result = (*env)->NewByteArray(env, (jsize)written);
|
||||
if (result != NULL) (*env)->SetByteArrayRegion(
|
||||
env, result, 0, (jsize)written, (const jbyte *)output);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335BuildCommand(
|
||||
JNIEnv *env, jobject self, jint controller, jint command_index)
|
||||
{
|
||||
(void)self;
|
||||
uint8_t output[PERIPH28335_REQUEST_SIZE];
|
||||
if (controller < 0 || controller > 255 || command_index < 0 || command_index > 255) return NULL;
|
||||
size_t written = periph28335_build_command(
|
||||
(uint8_t)controller, (uint8_t)command_index, output, sizeof output);
|
||||
if (written == 0U) return NULL;
|
||||
jbyteArray result = (*env)->NewByteArray(env, (jsize)written);
|
||||
if (result != NULL) (*env)->SetByteArrayRegion(
|
||||
env, result, 0, (jsize)written, (const jbyte *)output);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335ExpectedReadSize(
|
||||
JNIEnv *env, jobject self, jint count)
|
||||
{
|
||||
(void)env; (void)self;
|
||||
if (count < 0 || count > 65535) return 0;
|
||||
return (jint)periph28335_expected_read_response_size((uint16_t)count);
|
||||
}
|
||||
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335DecodeRead(
|
||||
JNIEnv *env, jobject self, jbyteArray input, jint controller, jint count)
|
||||
{
|
||||
(void)self;
|
||||
if (input == NULL || controller < 0 || controller > 255 ||
|
||||
count < 1 || count > (jint)PERIPH28335_REGISTER_COUNT) return NULL;
|
||||
jsize size = (*env)->GetArrayLength(env, input);
|
||||
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
|
||||
if ((data == NULL) && (size != 0)) return NULL;
|
||||
uint16_t words[PERIPH28335_REGISTER_COUNT];
|
||||
int status = periph28335_decode_read_response(
|
||||
(const uint8_t *)data, (size_t)size, (uint8_t)controller,
|
||||
(uint16_t)count, words, PERIPH28335_REGISTER_COUNT);
|
||||
if (data != NULL) (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
|
||||
if (status != PERIPH28335_OK) return NULL;
|
||||
jint values[PERIPH28335_REGISTER_COUNT];
|
||||
for (jint index = 0; index < count; ++index) values[index] = words[index];
|
||||
jintArray result = (*env)->NewIntArray(env, count);
|
||||
if (result != NULL) (*env)->SetIntArrayRegion(env, result, 0, count, values);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335ValidateWrite(
|
||||
JNIEnv *env, jobject self, jbyteArray response, jbyteArray request)
|
||||
{
|
||||
(void)self;
|
||||
if (response == NULL || request == NULL) return PERIPH28335_ERROR_ARGUMENT;
|
||||
jsize response_size = (*env)->GetArrayLength(env, response);
|
||||
jsize request_size = (*env)->GetArrayLength(env, request);
|
||||
jbyte *response_data = (*env)->GetByteArrayElements(env, response, NULL);
|
||||
jbyte *request_data = (*env)->GetByteArrayElements(env, request, NULL);
|
||||
if (response_data == NULL || request_data == NULL) {
|
||||
if (response_data != NULL) (*env)->ReleaseByteArrayElements(env, response, response_data, JNI_ABORT);
|
||||
if (request_data != NULL) (*env)->ReleaseByteArrayElements(env, request, request_data, JNI_ABORT);
|
||||
return PERIPH28335_ERROR_ARGUMENT;
|
||||
}
|
||||
int status = periph28335_validate_write_response(
|
||||
(const uint8_t *)response_data, (size_t)response_size,
|
||||
(const uint8_t *)request_data, (size_t)request_size);
|
||||
(*env)->ReleaseByteArrayElements(env, response, response_data, JNI_ABORT);
|
||||
(*env)->ReleaseByteArrayElements(env, request, request_data, JNI_ABORT);
|
||||
return status;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335ProjectCount(
|
||||
JNIEnv *env, jobject self)
|
||||
{
|
||||
(void)env; (void)self;
|
||||
return (jint)periph28335_project_count();
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335ProjectName(
|
||||
JNIEnv *env, jobject self, jint project_index)
|
||||
{
|
||||
(void)self;
|
||||
const char *name = project_index >= 0
|
||||
? periph28335_project_name((size_t)project_index) : NULL;
|
||||
return name != NULL ? (*env)->NewStringUTF(env, name) : NULL;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativePeriph28335CommandName(
|
||||
JNIEnv *env, jobject self, jint project_index, jint command_index)
|
||||
{
|
||||
(void)self;
|
||||
const char *name = project_index >= 0 && command_index >= 0
|
||||
? periph28335_command_name((size_t)project_index, (size_t)command_index)
|
||||
: NULL;
|
||||
return name != NULL ? (*env)->NewStringUTF(env, name) : NULL;
|
||||
}
|
||||
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeBalsamDecode(
|
||||
@@ -276,30 +100,6 @@ Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendCanValue(
|
||||
(uint8_t)flags, (const uint8_t *)data, (size_t)size);
|
||||
}
|
||||
|
||||
JNIEXPORT jdoubleArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeSpectrumPeak(
|
||||
JNIEnv *env, jobject self, jdoubleArray input, jdouble bin_hz,
|
||||
jdouble relative_threshold, jdouble absolute_floor)
|
||||
{
|
||||
(void)self;
|
||||
jsize count = input == NULL ? 0 : (*env)->GetArrayLength(env, input);
|
||||
if (count < 3 || count > (jsize)(SET_SPECTRUM_MAX / 2U + 1U)) return NULL;
|
||||
double *buffer = (double *)malloc(sizeof(double) * (size_t)(count * 2 - 1));
|
||||
if (buffer == NULL) return NULL;
|
||||
double *amplitudes = buffer, *scratch = buffer + count, peak[2];
|
||||
(*env)->GetDoubleArrayRegion(env, input, 0, count, amplitudes);
|
||||
int status = (*env)->ExceptionCheck(env) ? -1 : set_spectrum_dominant_peak(
|
||||
amplitudes, (size_t)count, bin_hz, relative_threshold, absolute_floor,
|
||||
scratch, (size_t)count - 1U, peak, 2U);
|
||||
jdoubleArray result = NULL;
|
||||
if (status >= 0) {
|
||||
result = (*env)->NewDoubleArray(env, status == 1 ? 2 : 0);
|
||||
if (result != NULL && status == 1) (*env)->SetDoubleArrayRegion(env, result, 0, 2, peak);
|
||||
}
|
||||
free(buffer);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchRequest(
|
||||
JNIEnv *env, jobject self, jint period, jintArray input)
|
||||
@@ -322,20 +122,6 @@ Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchRequest(
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchAck(
|
||||
JNIEnv *env, jobject self, jbyteArray input, jint period, jint count)
|
||||
{
|
||||
(void)self;
|
||||
if (input == NULL || period < 0 || period > 65535 || count < 0) return JNI_FALSE;
|
||||
jsize size = (*env)->GetArrayLength(env, input);
|
||||
if (size != 4) return JNI_FALSE;
|
||||
jbyte payload[4];
|
||||
(*env)->GetByteArrayRegion(env, input, 0, size, payload);
|
||||
return set_trend_watch_ack((const uint8_t *)payload, (size_t)size,
|
||||
(uint16_t)period, (size_t)count) ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchValues(
|
||||
JNIEnv *env, jobject self, jbyteArray input)
|
||||
@@ -355,28 +141,6 @@ Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchValues(
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jlongArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTrendWatchDecode(
|
||||
JNIEnv *env, jobject self, jbyteArray input)
|
||||
{
|
||||
(void)self;
|
||||
jsize size = input == NULL ? 0 : (*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];
|
||||
uint32_t timestamp = 0U;
|
||||
jlong values[1U + SET_TREND_WATCH_MAX];
|
||||
(*env)->GetByteArrayRegion(env, input, 0, size, payload);
|
||||
int count = set_trend_watch_decode((const uint8_t *)payload, (size_t)size,
|
||||
×tamp, words, SET_TREND_WATCH_MAX);
|
||||
if (count < 0) return NULL;
|
||||
values[0] = (jlong)timestamp;
|
||||
for (int i = 0; i < count; ++i) values[i + 1] = (jlong)words[i];
|
||||
jlongArray result = (*env)->NewLongArray(env, count + 1);
|
||||
if (result != NULL) (*env)->SetLongArrayRegion(env, result, 0, count + 1, values);
|
||||
return result;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint8_t *storage;
|
||||
size_t storage_size;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package ru.setcorp.setprotocol.periph28335
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class Periph28335ProtocolTest {
|
||||
@Test fun readRequestMatchesDelphiByteOrder() {
|
||||
val request = Periph28335Protocol.buildReadRegisters(16, 24, 64)
|
||||
assertTrue(request.copyOf(6).contentEquals(byteArrayOf(0x10, 0x03, 0x00, 0x18, 0x00, 0x40)))
|
||||
assertEquals(
|
||||
Periph28335Protocol.crc16Modbus(request.copyOf(6)),
|
||||
(request[6].toInt() and 0xFF) or ((request[7].toInt() and 0xFF) shl 8),
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun writeAndCommandUseRegister127() {
|
||||
assertTrue(
|
||||
Periph28335Protocol.buildWriteRegister(16, 7, 0x1234).copyOf(6)
|
||||
.contentEquals(byteArrayOf(0x10, 0x06, 0x00, 0x07, 0x12, 0x34)),
|
||||
)
|
||||
assertTrue(
|
||||
Periph28335Protocol.buildCommand(16, 15).copyOf(6)
|
||||
.contentEquals(byteArrayOf(0x10, 0x06, 0x00, 0x7F, 0x80.toByte(), 0x00)),
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun responseAndBitsRoundTrip() {
|
||||
val response = Periph28335Protocol.withCrc(byteArrayOf(0x10, 0x03, 0x04, 0x80.toByte(), 0x05, 0x12, 0x34))
|
||||
assertEquals(listOf(0x8005, 0x1234), Periph28335Protocol.decodeReadResponse(response, 16, 2))
|
||||
val bits = Periph28335Protocol.bitsLsbFirst(0x8005)
|
||||
assertTrue(bits[0] && bits[2] && bits[15])
|
||||
assertEquals(0x8005, Periph28335Protocol.wordFromBits(bits))
|
||||
}
|
||||
}
|
||||
@@ -36,12 +36,4 @@ class PlotViewportTest {
|
||||
}
|
||||
SpectrumOptions().validate()
|
||||
}
|
||||
@Test fun absoluteLimitsAndDominantPeakUseNativeCore() {
|
||||
assertEquals(PlotLimits(0.0, 500.0, -1.0, 1.0),
|
||||
PlotLimits(0.0, 500.0, -1.0, 1.0).validated())
|
||||
assertTrue(runCatching { PlotLimits(1.0, 1.0, -1.0, 1.0).validated() }.isFailure)
|
||||
val spectrum = TrendSpectrum(12, 60.0, 0.0,
|
||||
listOf(10.0, .01, .02, .8, .03, .4, .02))
|
||||
assertEquals(15.0, SpectrumAnalyzer.dominantPeak(spectrum)!!.frequencyHz, 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
22
c/set-protocol/ports/stm32f407-devboard-v1/README.md
Normal file
22
c/set-protocol/ports/stm32f407-devboard-v1/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# STM32F407 + CAN485 DevBoard_V1
|
||||
|
||||
Порт даёт SETGUI доступ к существующим Modbus-регистрам STM32F407 через
|
||||
классический CAN. ПК подключён к USB-UART платы CAN485 DevBoard_V1, команда
|
||||
`T,E` платы отправляет ProtoCAN-кадр, а этот порт отвечает тем же типом кадра.
|
||||
При маршруте `Q3` плата одновременно ретранслирует поток по RS-485.
|
||||
|
||||
Порт не настраивает GPIO, bitrate, фильтры и не запускает CAN: это остаётся за
|
||||
CubeMX-проектом. Необработанные кадры передаются callback-у приложения, поэтому
|
||||
загрузчик ProtoCAN и регистровый сервис могут делить FIFO0.
|
||||
|
||||
Слой приложения предоставляет callback-и чтения и записи. Нулевой DLC означает
|
||||
чтение; данные в кадре `HOLDING`/`COIL` — запись. За кадр передаются до четырёх
|
||||
16-битных регистров или до пятнадцати coils (предел 4-битного `RegCount`).
|
||||
|
||||
```c
|
||||
static set_devboard_v1_stm32f407_t gui_can;
|
||||
set_devboard_v1_stm32f407_init(&gui_can, &hcan, 7, 3,
|
||||
app_read_register, app_write_register, NULL,
|
||||
app_handle_boot_frame, NULL);
|
||||
for (;;) set_devboard_v1_stm32f407_poll(&gui_can);
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "set_devboard_v1_stm32f407.h"
|
||||
|
||||
static bool transmit(set_devboard_v1_stm32f407_t *port,
|
||||
const pcan_frame_t *frame)
|
||||
{
|
||||
CAN_TxHeaderTypeDef header = {0};
|
||||
uint32_t mailbox = 0U;
|
||||
uint8_t data[8] = {0U};
|
||||
|
||||
header.ExtId = frame->id & 0x1FFFFFFFUL;
|
||||
header.IDE = CAN_ID_EXT;
|
||||
header.RTR = ((frame->flags & PCAN_FLAG_RTR) != 0U) ?
|
||||
CAN_RTR_REMOTE : CAN_RTR_DATA;
|
||||
header.DLC = frame->dlc;
|
||||
header.TransmitGlobalTime = DISABLE;
|
||||
for (uint8_t index = 0U; index < frame->dlc; ++index) {
|
||||
data[index] = frame->data[index];
|
||||
}
|
||||
if (HAL_CAN_AddTxMessage(port->can, &header, data, &mailbox) != HAL_OK) {
|
||||
port->tx_errors++;
|
||||
return false;
|
||||
}
|
||||
port->tx_frames++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set_devboard_v1_stm32f407_init(
|
||||
set_devboard_v1_stm32f407_t *port, CAN_HandleTypeDef *can,
|
||||
uint8_t device_type, uint8_t device_id,
|
||||
pcan_modbus_read_fn read, pcan_modbus_write_fn write, void *register_user,
|
||||
set_devboard_v1_unhandled_fn unhandled, void *unhandled_user)
|
||||
{
|
||||
if ((port == NULL) || (can == NULL)) {
|
||||
return false;
|
||||
}
|
||||
port->can = can;
|
||||
port->unhandled = unhandled;
|
||||
port->unhandled_user = unhandled_user;
|
||||
port->rx_frames = 0U;
|
||||
port->tx_frames = 0U;
|
||||
port->tx_errors = 0U;
|
||||
return pcan_modbus_server_init(&port->server, device_type, device_id,
|
||||
read, write, register_user);
|
||||
}
|
||||
|
||||
size_t set_devboard_v1_stm32f407_poll(set_devboard_v1_stm32f407_t *port)
|
||||
{
|
||||
size_t handled = 0U;
|
||||
if ((port == NULL) || (port->can == NULL)) {
|
||||
return 0U;
|
||||
}
|
||||
while (HAL_CAN_GetRxFifoFillLevel(port->can, CAN_RX_FIFO0) != 0U) {
|
||||
CAN_RxHeaderTypeDef header;
|
||||
uint8_t data[8] = {0U};
|
||||
pcan_frame_t request = {0};
|
||||
pcan_frame_t response;
|
||||
pcan_modbus_result_t result;
|
||||
|
||||
if (HAL_CAN_GetRxMessage(port->can, CAN_RX_FIFO0, &header, data) != HAL_OK) {
|
||||
break;
|
||||
}
|
||||
port->rx_frames++;
|
||||
request.id = (header.IDE == CAN_ID_EXT) ? header.ExtId : header.StdId;
|
||||
request.flags = (header.IDE == CAN_ID_EXT) ? PCAN_FLAG_IDE : 0U;
|
||||
if (header.RTR == CAN_RTR_REMOTE) {
|
||||
request.flags |= PCAN_FLAG_RTR;
|
||||
}
|
||||
request.dlc = (header.DLC > 8U) ? 8U : (uint8_t)header.DLC;
|
||||
for (uint8_t index = 0U; index < request.dlc; ++index) {
|
||||
request.data[index] = data[index];
|
||||
}
|
||||
|
||||
result = pcan_modbus_server_handle(&port->server, &request, &response);
|
||||
if (result == PCAN_MODBUS_RESPONSE) {
|
||||
(void)transmit(port, &response);
|
||||
handled++;
|
||||
} else if ((result == PCAN_MODBUS_NOT_FOR_US) &&
|
||||
(port->unhandled != NULL)) {
|
||||
port->unhandled(port->unhandled_user, &header, data);
|
||||
}
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/** STM32F407 bxCAN port for SETGUI through CAN485 DevBoard_V1. */
|
||||
#ifndef SET_DEVBOARD_V1_STM32F407_H
|
||||
#define SET_DEVBOARD_V1_STM32F407_H
|
||||
|
||||
#include "stm32f4xx_hal.h"
|
||||
#include "pcan_modbus_server.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*set_devboard_v1_unhandled_fn)(void *user,
|
||||
const CAN_RxHeaderTypeDef *header,
|
||||
const uint8_t data[8]);
|
||||
|
||||
typedef struct {
|
||||
CAN_HandleTypeDef *can;
|
||||
pcan_modbus_server_t server;
|
||||
set_devboard_v1_unhandled_fn unhandled;
|
||||
void *unhandled_user;
|
||||
uint32_t rx_frames;
|
||||
uint32_t tx_frames;
|
||||
uint32_t tx_errors;
|
||||
} set_devboard_v1_stm32f407_t;
|
||||
|
||||
bool set_devboard_v1_stm32f407_init(
|
||||
set_devboard_v1_stm32f407_t *port, CAN_HandleTypeDef *can,
|
||||
uint8_t device_type, uint8_t device_id,
|
||||
pcan_modbus_read_fn read, pcan_modbus_write_fn write, void *register_user,
|
||||
set_devboard_v1_unhandled_fn unhandled, void *unhandled_user);
|
||||
|
||||
size_t set_devboard_v1_stm32f407_poll(set_devboard_v1_stm32f407_t *port);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
#include "pcan_crc.h"
|
||||
#include "balsam_can.h"
|
||||
#include "periph28335.h"
|
||||
#include "pcan_frame.h"
|
||||
#include "pcan_id.h"
|
||||
#include "gui_frame.h"
|
||||
@@ -89,79 +88,6 @@ size_t pcan_abi_balsam_register_name(uint8_t device, uint16_t address,
|
||||
return balsam_can_register_name(device, address, output, output_size);
|
||||
}
|
||||
|
||||
uint16_t pcan_abi_periph28335_crc16(const uint8_t *data, size_t size)
|
||||
{
|
||||
return periph28335_crc16_modbus(data, size);
|
||||
}
|
||||
|
||||
size_t pcan_abi_periph28335_append_crc(
|
||||
const uint8_t *payload, size_t payload_size,
|
||||
uint8_t *output, size_t output_size)
|
||||
{
|
||||
return periph28335_append_crc(payload, payload_size, output, output_size);
|
||||
}
|
||||
|
||||
size_t pcan_abi_periph28335_build_read(
|
||||
uint8_t controller, uint16_t start, uint16_t count,
|
||||
uint8_t *output, size_t output_size)
|
||||
{
|
||||
return periph28335_build_read_registers(
|
||||
controller, start, count, output, output_size);
|
||||
}
|
||||
|
||||
size_t pcan_abi_periph28335_build_write(
|
||||
uint8_t controller, uint16_t address, uint16_t value,
|
||||
uint8_t *output, size_t output_size)
|
||||
{
|
||||
return periph28335_build_write_register(
|
||||
controller, address, value, output, output_size);
|
||||
}
|
||||
|
||||
size_t pcan_abi_periph28335_build_command(
|
||||
uint8_t controller, uint8_t command_index,
|
||||
uint8_t *output, size_t output_size)
|
||||
{
|
||||
return periph28335_build_command(
|
||||
controller, command_index, output, output_size);
|
||||
}
|
||||
|
||||
size_t pcan_abi_periph28335_expected_read_size(uint16_t count)
|
||||
{
|
||||
return periph28335_expected_read_response_size(count);
|
||||
}
|
||||
|
||||
int pcan_abi_periph28335_decode_read(
|
||||
const uint8_t *data, size_t size, uint8_t controller, uint16_t count,
|
||||
uint16_t *output, size_t output_count)
|
||||
{
|
||||
return periph28335_decode_read_response(
|
||||
data, size, controller, count, output, output_count);
|
||||
}
|
||||
|
||||
int pcan_abi_periph28335_validate_write(
|
||||
const uint8_t *response, size_t response_size,
|
||||
const uint8_t *request, size_t request_size)
|
||||
{
|
||||
return periph28335_validate_write_response(
|
||||
response, response_size, request, request_size);
|
||||
}
|
||||
|
||||
size_t pcan_abi_periph28335_project_count(void)
|
||||
{
|
||||
return periph28335_project_count();
|
||||
}
|
||||
|
||||
const char *pcan_abi_periph28335_project_name(size_t project_index)
|
||||
{
|
||||
return periph28335_project_name(project_index);
|
||||
}
|
||||
|
||||
const char *pcan_abi_periph28335_command_name(
|
||||
size_t project_index, size_t command_index)
|
||||
{
|
||||
return periph28335_command_name(project_index, command_index);
|
||||
}
|
||||
|
||||
size_t pcan_abi_frame_encode(uint8_t sequence, uint8_t flags,
|
||||
uint32_t can_id, const uint8_t *data, uint8_t dlc,
|
||||
uint8_t *output, size_t output_size)
|
||||
|
||||
144
c/set-protocol/src/pcan_modbus_server.c
Normal file
144
c/set-protocol/src/pcan_modbus_server.c
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "pcan_modbus_server.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "pcan_id.h"
|
||||
|
||||
static bool is_bank(uint8_t value)
|
||||
{
|
||||
return (value >= (uint8_t)PCAN_MODBUS_COIL) &&
|
||||
(value <= (uint8_t)PCAN_MODBUS_INPUT);
|
||||
}
|
||||
|
||||
bool pcan_modbus_server_init(pcan_modbus_server_t *server,
|
||||
uint8_t device_type, uint8_t device_id,
|
||||
pcan_modbus_read_fn read,
|
||||
pcan_modbus_write_fn write, void *user)
|
||||
{
|
||||
if ((server == NULL) || (read == NULL) || (device_type > 7U) ||
|
||||
(device_id > 15U)) {
|
||||
return false;
|
||||
}
|
||||
server->device_type = device_type;
|
||||
server->device_id = device_id;
|
||||
server->read = read;
|
||||
server->write = write;
|
||||
server->user = user;
|
||||
server->requests = 0U;
|
||||
server->responses = 0U;
|
||||
server->rejected = 0U;
|
||||
return true;
|
||||
}
|
||||
|
||||
static pcan_modbus_result_t reject(pcan_modbus_server_t *server)
|
||||
{
|
||||
server->rejected++;
|
||||
return PCAN_MODBUS_HANDLED_NO_RESPONSE;
|
||||
}
|
||||
|
||||
pcan_modbus_result_t pcan_modbus_server_handle(pcan_modbus_server_t *server,
|
||||
const pcan_frame_t *request,
|
||||
pcan_frame_t *response)
|
||||
{
|
||||
pcan_id_t id;
|
||||
uint16_t address;
|
||||
uint8_t count;
|
||||
pcan_modbus_bank_t bank;
|
||||
|
||||
if ((server == NULL) || (request == NULL) || (response == NULL) ||
|
||||
((request->flags & PCAN_FLAG_IDE) == 0U)) {
|
||||
return PCAN_MODBUS_NOT_FOR_US;
|
||||
}
|
||||
pcan_id_unpack(request->id, &id);
|
||||
if ((id.route != PCAN_ROUTE_FROM_PM) ||
|
||||
(id.device_type != server->device_type) ||
|
||||
(id.device_id != server->device_id) || !is_bank(id.msg_type)) {
|
||||
return PCAN_MODBUS_NOT_FOR_US;
|
||||
}
|
||||
|
||||
server->requests++;
|
||||
address = (uint16_t)(id.msg_body >> 4U);
|
||||
count = (uint8_t)(id.msg_body & 0x0FU);
|
||||
bank = (pcan_modbus_bank_t)id.msg_type;
|
||||
if (count == 0U) {
|
||||
return reject(server);
|
||||
}
|
||||
|
||||
*response = *request;
|
||||
id.route = PCAN_ROUTE_FROM_DEVICE;
|
||||
response->id = pcan_id_pack(&id);
|
||||
response->flags = PCAN_FLAG_IDE;
|
||||
response->seq = 0U;
|
||||
|
||||
if (request->dlc == 0U) {
|
||||
uint16_t packed = 0U;
|
||||
if ((bank == PCAN_MODBUS_COIL) || (bank == PCAN_MODBUS_DISCRETE)) {
|
||||
if (count > 16U) {
|
||||
return reject(server);
|
||||
}
|
||||
for (uint8_t index = 0U; index < count; ++index) {
|
||||
uint16_t value = 0U;
|
||||
if (!server->read(server->user, bank,
|
||||
(uint16_t)(address + index), &value)) {
|
||||
return reject(server);
|
||||
}
|
||||
if (value != 0U) {
|
||||
packed |= (uint16_t)(1U << index);
|
||||
}
|
||||
}
|
||||
response->dlc = 2U;
|
||||
response->data[0] = (uint8_t)packed;
|
||||
response->data[1] = (uint8_t)(packed >> 8U);
|
||||
} else {
|
||||
if (count > 4U) {
|
||||
return reject(server);
|
||||
}
|
||||
response->dlc = (uint8_t)(count * 2U);
|
||||
for (uint8_t index = 0U; index < count; ++index) {
|
||||
uint16_t value = 0U;
|
||||
if (!server->read(server->user, bank,
|
||||
(uint16_t)(address + index), &value)) {
|
||||
return reject(server);
|
||||
}
|
||||
response->data[index * 2U] = (uint8_t)value;
|
||||
response->data[(index * 2U) + 1U] = (uint8_t)(value >> 8U);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((bank == PCAN_MODBUS_INPUT) || (bank == PCAN_MODBUS_DISCRETE) ||
|
||||
(server->write == NULL)) {
|
||||
return reject(server);
|
||||
}
|
||||
if (bank == PCAN_MODBUS_COIL) {
|
||||
uint16_t bits;
|
||||
if ((count > 16U) || (request->dlc != 2U)) {
|
||||
return reject(server);
|
||||
}
|
||||
bits = (uint16_t)((uint16_t)request->data[0] |
|
||||
((uint16_t)request->data[1] << 8U));
|
||||
for (uint8_t index = 0U; index < count; ++index) {
|
||||
if (!server->write(server->user, bank,
|
||||
(uint16_t)(address + index),
|
||||
(uint16_t)((bits >> index) & 1U))) {
|
||||
return reject(server);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((count > 4U) || (request->dlc != (uint8_t)(count * 2U))) {
|
||||
return reject(server);
|
||||
}
|
||||
for (uint8_t index = 0U; index < count; ++index) {
|
||||
uint16_t value = (uint16_t)(request->data[index * 2U] |
|
||||
((uint16_t)request->data[(index * 2U) + 1U] << 8U));
|
||||
if (!server->write(server->user, bank,
|
||||
(uint16_t)(address + index), value)) {
|
||||
return reject(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
response->dlc = request->dlc;
|
||||
}
|
||||
|
||||
server->responses++;
|
||||
return PCAN_MODBUS_RESPONSE;
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
#include "periph28335.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
const char *project;
|
||||
const char *commands[PERIPH28335_COMMAND_COUNT];
|
||||
} periph28335_project_t;
|
||||
|
||||
static const periph28335_project_t projects[] = {
|
||||
{ "По умолчанию", { "Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Light", "Raw", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } },
|
||||
{ "23470", { "Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "-", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } },
|
||||
{ "23550", { "Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "Send", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } },
|
||||
{ "23550.2", { "Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Send", "Raw", "Beep", "", "", "", "", "Log", "Reset", "Nothing at all" } },
|
||||
{ "ICE 22220.1-3", { "Test", "Zero", "Save", "Def", "Calibr", "Read", "ExtLamp", "ExtLite", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } },
|
||||
{ "ICE 22220.4-5", { "Test", "Def", "Save", "Load", "Raw", "Read", "ExtLamp", "ExtLite", "No log", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } },
|
||||
{ "Бальзам 161", { "Test", "Zero", "Save", "Def", "Calibr", "Clbr 400", "Stop", "Start", "Init", "Secret", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } },
|
||||
{ "Бальзам 162", { "Test", "Def", "Save", "Load", "Calibr", "Secret", "Stop", "Start", "Init", "Tune", "-", "-", "-", "-", "-", "Reset", "Nothing at all" } },
|
||||
{ "Бальзам 163", { "Test", "Def", "Save", "Load", "Calibr", "Calcul", "Stop", "Start", "Init", "Tune", "Secret", "-", "-", "-", "-", "Reset", "Nothing at all" } }
|
||||
};
|
||||
|
||||
static void put_be16(uint8_t *output, uint16_t value)
|
||||
{
|
||||
output[0] = (uint8_t)(value >> 8);
|
||||
output[1] = (uint8_t)value;
|
||||
}
|
||||
|
||||
static uint16_t get_be16(const uint8_t *input)
|
||||
{
|
||||
return (uint16_t)(((uint16_t)input[0] << 8) | input[1]);
|
||||
}
|
||||
|
||||
uint16_t periph28335_crc16_modbus(const uint8_t *data, size_t size)
|
||||
{
|
||||
uint16_t crc = 0xFFFFU;
|
||||
size_t index;
|
||||
uint8_t bit;
|
||||
if ((data == NULL) && (size != 0U)) return 0U;
|
||||
for (index = 0U; index < size; ++index) {
|
||||
crc ^= data[index];
|
||||
for (bit = 0U; bit < 8U; ++bit) {
|
||||
crc = (crc & 1U) != 0U
|
||||
? (uint16_t)((crc >> 1) ^ 0xA001U)
|
||||
: (uint16_t)(crc >> 1);
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
size_t periph28335_append_crc(const uint8_t *payload, size_t payload_size,
|
||||
uint8_t *output, size_t output_size)
|
||||
{
|
||||
uint16_t crc;
|
||||
if ((output == NULL) || ((payload == NULL) && (payload_size != 0U)) ||
|
||||
(payload_size > output_size) || (output_size - payload_size < 2U)) {
|
||||
return 0U;
|
||||
}
|
||||
if (payload_size != 0U) memcpy(output, payload, payload_size);
|
||||
crc = periph28335_crc16_modbus(payload, payload_size);
|
||||
output[payload_size] = (uint8_t)crc;
|
||||
output[payload_size + 1U] = (uint8_t)(crc >> 8);
|
||||
return payload_size + 2U;
|
||||
}
|
||||
|
||||
size_t periph28335_build_read_registers(
|
||||
uint8_t controller, uint16_t start, uint16_t count,
|
||||
uint8_t *output, size_t output_size)
|
||||
{
|
||||
uint8_t payload[6];
|
||||
if ((count == 0U) || (count > PERIPH28335_REGISTER_COUNT) ||
|
||||
(start >= PERIPH28335_REGISTER_COUNT) ||
|
||||
((uint32_t)start + count > PERIPH28335_REGISTER_COUNT)) return 0U;
|
||||
payload[0] = controller;
|
||||
payload[1] = 3U;
|
||||
put_be16(&payload[2], start);
|
||||
put_be16(&payload[4], count);
|
||||
return periph28335_append_crc(payload, sizeof payload, output, output_size);
|
||||
}
|
||||
|
||||
size_t periph28335_build_write_register(
|
||||
uint8_t controller, uint16_t address, uint16_t value,
|
||||
uint8_t *output, size_t output_size)
|
||||
{
|
||||
uint8_t payload[6];
|
||||
if (address >= PERIPH28335_REGISTER_COUNT) return 0U;
|
||||
payload[0] = controller;
|
||||
payload[1] = 6U;
|
||||
put_be16(&payload[2], address);
|
||||
put_be16(&payload[4], value);
|
||||
return periph28335_append_crc(payload, sizeof payload, output, output_size);
|
||||
}
|
||||
|
||||
size_t periph28335_build_command(
|
||||
uint8_t controller, uint8_t command_index,
|
||||
uint8_t *output, size_t output_size)
|
||||
{
|
||||
uint16_t value;
|
||||
if (command_index >= PERIPH28335_COMMAND_COUNT) return 0U;
|
||||
value = command_index < 16U ? (uint16_t)(1UL << command_index) : 0U;
|
||||
return periph28335_build_write_register(
|
||||
controller, PERIPH28335_REGISTER_COUNT - 1U, value,
|
||||
output, output_size);
|
||||
}
|
||||
|
||||
size_t periph28335_expected_read_response_size(uint16_t count)
|
||||
{
|
||||
return (count >= 1U && count <= PERIPH28335_REGISTER_COUNT)
|
||||
? (size_t)count * 2U + 5U : 0U;
|
||||
}
|
||||
|
||||
int periph28335_decode_read_response(
|
||||
const uint8_t *data, size_t size, uint8_t controller, uint16_t count,
|
||||
uint16_t *output, size_t output_count)
|
||||
{
|
||||
size_t expected = periph28335_expected_read_response_size(count);
|
||||
uint16_t crc;
|
||||
size_t index;
|
||||
if ((data == NULL) || (output == NULL)) return PERIPH28335_ERROR_ARGUMENT;
|
||||
if (expected == 0U) return PERIPH28335_ERROR_RANGE;
|
||||
if (size != expected) return PERIPH28335_ERROR_LENGTH;
|
||||
if (output_count < count) return PERIPH28335_ERROR_CAPACITY;
|
||||
crc = periph28335_crc16_modbus(data, size - 2U);
|
||||
if ((data[size - 2U] != (uint8_t)crc) ||
|
||||
(data[size - 1U] != (uint8_t)(crc >> 8))) {
|
||||
return PERIPH28335_ERROR_CRC;
|
||||
}
|
||||
if ((data[0] != controller) || (data[1] != 3U) ||
|
||||
(data[2] != (uint8_t)(count * 2U))) {
|
||||
return PERIPH28335_ERROR_HEADER;
|
||||
}
|
||||
for (index = 0U; index < count; ++index) {
|
||||
output[index] = get_be16(&data[3U + index * 2U]);
|
||||
}
|
||||
return PERIPH28335_OK;
|
||||
}
|
||||
|
||||
int periph28335_validate_write_response(
|
||||
const uint8_t *response, size_t response_size,
|
||||
const uint8_t *request, size_t request_size)
|
||||
{
|
||||
uint16_t crc;
|
||||
if ((response == NULL) || (request == NULL)) return PERIPH28335_ERROR_ARGUMENT;
|
||||
if ((response_size != PERIPH28335_REQUEST_SIZE) ||
|
||||
(request_size != PERIPH28335_REQUEST_SIZE)) return PERIPH28335_ERROR_LENGTH;
|
||||
crc = periph28335_crc16_modbus(response, response_size - 2U);
|
||||
if ((response[response_size - 2U] != (uint8_t)crc) ||
|
||||
(response[response_size - 1U] != (uint8_t)(crc >> 8))) {
|
||||
return PERIPH28335_ERROR_CRC;
|
||||
}
|
||||
return memcmp(response, request, PERIPH28335_REQUEST_SIZE) == 0
|
||||
? PERIPH28335_OK : PERIPH28335_ERROR_HEADER;
|
||||
}
|
||||
|
||||
size_t periph28335_project_count(void)
|
||||
{
|
||||
return sizeof projects / sizeof projects[0];
|
||||
}
|
||||
|
||||
const char *periph28335_project_name(size_t project_index)
|
||||
{
|
||||
return project_index < periph28335_project_count()
|
||||
? projects[project_index].project : NULL;
|
||||
}
|
||||
|
||||
const char *periph28335_command_name(size_t project_index,
|
||||
size_t command_index)
|
||||
{
|
||||
return project_index < periph28335_project_count() &&
|
||||
command_index < PERIPH28335_COMMAND_COUNT
|
||||
? projects[project_index].commands[command_index] : NULL;
|
||||
}
|
||||
@@ -11,10 +11,10 @@ static int finite_values(const double *v, size_t n) {
|
||||
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, 2, 4};
|
||||
static const size_t sizes[] = {10, 3, 4, 4, 6, 2, 3, 2};
|
||||
double span, fraction;
|
||||
if (op > SET_PLOT_LIMITS || !v || !out || n != sizes[op] ||
|
||||
cap < (op == SET_PLOT_TRANSFORM || op == SET_PLOT_LIMITS ? 4U : 1U)) return 0;
|
||||
if (op > SET_PLOT_DB_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 ||
|
||||
@@ -69,10 +69,6 @@ size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t
|
||||
if (v[0] == 0 || v[1] == 0) return 0;
|
||||
out[0] = 20 * log10(fabs(v[1] / v[0]));
|
||||
break;
|
||||
case SET_PLOT_LIMITS:
|
||||
if (v[0] >= v[1] || v[2] >= v[3]) return 0;
|
||||
out[0] = v[0]; out[1] = v[1]; out[2] = v[2]; out[3] = v[3];
|
||||
return 4;
|
||||
default: return 0;
|
||||
}
|
||||
return isfinite(out[0]) ? 1 : 0;
|
||||
|
||||
@@ -4,13 +4,6 @@
|
||||
|
||||
#define PI 3.14159265358979323846
|
||||
|
||||
static int compare_double(const void *left, const void *right)
|
||||
{
|
||||
const double a = *(const double *)left;
|
||||
const double b = *(const double *)right;
|
||||
return (a > b) - (a < b);
|
||||
}
|
||||
|
||||
static double window_value(int window, size_t index, size_t n)
|
||||
{
|
||||
double phase = 2.0 * PI * (double)index / (double)n;
|
||||
@@ -131,35 +124,3 @@ int set_spectrum_analyze(const double *times, const double *values, size_t count
|
||||
free(scratch);
|
||||
return result;
|
||||
}
|
||||
|
||||
int set_spectrum_dominant_peak(const double *amplitudes, size_t count,
|
||||
double bin_hz, double relative_threshold, double absolute_floor,
|
||||
double *scratch, size_t scratch_capacity, double *peak, size_t peak_capacity)
|
||||
{
|
||||
size_t i, usable = 0U, peak_bin = 0U;
|
||||
double peak_amplitude = -1.0, median, threshold;
|
||||
if (amplitudes == NULL || scratch == NULL || peak == NULL || count < 3U ||
|
||||
scratch_capacity < count - 1U || peak_capacity < 2U || !isfinite(bin_hz) ||
|
||||
bin_hz <= 0.0 || !isfinite(relative_threshold) || relative_threshold <= 0.0 ||
|
||||
!isfinite(absolute_floor) || absolute_floor < 0.0) return -1;
|
||||
for (i = 1U; i < count; ++i) {
|
||||
if (isfinite(amplitudes[i]) && amplitudes[i] >= 0.0)
|
||||
scratch[usable++] = amplitudes[i];
|
||||
}
|
||||
if (usable < 2U) return 0;
|
||||
qsort(scratch, usable, sizeof(double), compare_double);
|
||||
median = scratch[usable / 2U];
|
||||
for (i = 1U; i + 1U < count; ++i) {
|
||||
const double value = amplitudes[i];
|
||||
if (isfinite(value) && isfinite(amplitudes[i - 1U]) && isfinite(amplitudes[i + 1U]) &&
|
||||
value >= amplitudes[i - 1U] && value > amplitudes[i + 1U] && value > peak_amplitude) {
|
||||
peak_bin = i;
|
||||
peak_amplitude = value;
|
||||
}
|
||||
}
|
||||
threshold = fmax(absolute_floor, median * relative_threshold);
|
||||
if (peak_bin == 0U || peak_amplitude < threshold) return 0;
|
||||
peak[0] = (double)peak_bin * bin_hz;
|
||||
peak[1] = peak_amplitude;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,6 @@ static uint16_t get16(const uint8_t *p)
|
||||
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
|
||||
static uint32_t get32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static void put16(uint8_t *p, uint16_t value)
|
||||
{
|
||||
p[0] = (uint8_t)value;
|
||||
@@ -69,12 +63,6 @@ int set_trend_watch_ack(const uint8_t *payload, size_t size, uint16_t period_ms,
|
||||
}
|
||||
|
||||
int set_trend_watch_values(const uint8_t *payload, size_t size, uint16_t *words, size_t capacity)
|
||||
{
|
||||
return set_trend_watch_decode(payload, size, NULL, words, capacity);
|
||||
}
|
||||
|
||||
int set_trend_watch_decode(const uint8_t *payload, size_t size, uint32_t *timestamp_ms,
|
||||
uint16_t *words, size_t capacity)
|
||||
{
|
||||
size_t i, count;
|
||||
if (payload == NULL || size < 6U) return -1;
|
||||
@@ -82,6 +70,5 @@ int set_trend_watch_decode(const uint8_t *payload, size_t size, uint32_t *timest
|
||||
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);
|
||||
if (timestamp_ms != NULL) *timestamp_ms = get32(payload);
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
2
c/set-protocol/tests/fixtures/plot-v1.json
vendored
2
c/set-protocol/tests/fixtures/plot-v1.json
vendored
@@ -23,8 +23,6 @@
|
||||
{"name":"db_gain","op":7,"input":[1,10],"output":[20]},
|
||||
{"name":"db_attenuation","op":7,"input":[10,1],"output":[-20]},
|
||||
{"name":"db_zero_reference","op":7,"input":[0,1],"output":null},
|
||||
{"name":"absolute_limits","op":8,"input":[0,500,-2,2],"output":[0,500,-2,2]},
|
||||
{"name":"reversed_x_limits","op":8,"input":[1,1,-2,2],"output":null},
|
||||
{"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}
|
||||
|
||||
90
c/set-protocol/tests/test_modbus_server.c
Normal file
90
c/set-protocol/tests/test_modbus_server.c
Normal file
@@ -0,0 +1,90 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "pcan_id.h"
|
||||
#include "pcan_modbus_server.h"
|
||||
|
||||
static uint16_t holding[32];
|
||||
static uint16_t input[32];
|
||||
static uint16_t coils;
|
||||
|
||||
static bool read_word(void *user, pcan_modbus_bank_t bank,
|
||||
uint16_t address, uint16_t *value)
|
||||
{
|
||||
(void)user;
|
||||
if (address >= 32U) return false;
|
||||
if (bank == PCAN_MODBUS_HOLDING) *value = holding[address];
|
||||
else if (bank == PCAN_MODBUS_INPUT) *value = input[address];
|
||||
else if (bank == PCAN_MODBUS_COIL) *value = (uint16_t)((coils >> address) & 1U);
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool write_word(void *user, pcan_modbus_bank_t bank,
|
||||
uint16_t address, uint16_t value)
|
||||
{
|
||||
(void)user;
|
||||
if (address >= 32U) return false;
|
||||
if (bank == PCAN_MODBUS_HOLDING) holding[address] = value;
|
||||
else if (bank == PCAN_MODBUS_COIL && address < 16U) {
|
||||
if (value) coils |= (uint16_t)(1U << address);
|
||||
else coils &= (uint16_t)~(1U << address);
|
||||
} else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static pcan_frame_t request(uint8_t type, uint16_t address, uint8_t count)
|
||||
{
|
||||
pcan_id_t id = {0};
|
||||
pcan_frame_t frame = {0};
|
||||
id.priority = PCAN_PRIORITY_STANDARD;
|
||||
id.route = PCAN_ROUTE_FROM_PM;
|
||||
id.device_type = 7U;
|
||||
id.device_id = 3U;
|
||||
id.msg_type = type;
|
||||
id.msg_body = pcan_body_modbus(address, count);
|
||||
frame.id = pcan_id_pack(&id);
|
||||
frame.flags = PCAN_FLAG_IDE;
|
||||
return frame;
|
||||
}
|
||||
|
||||
#define CHECK(x) do { if (!(x)) { \
|
||||
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); return 1; \
|
||||
} } while (0)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
pcan_modbus_server_t server;
|
||||
pcan_frame_t req;
|
||||
pcan_frame_t rsp;
|
||||
pcan_id_t id;
|
||||
memset(holding, 0, sizeof(holding));
|
||||
memset(input, 0, sizeof(input));
|
||||
input[10] = 0x1234U;
|
||||
input[11] = 0xABCDU;
|
||||
|
||||
CHECK(pcan_modbus_server_init(&server, 7U, 3U, read_word, write_word, NULL));
|
||||
req = request(PCAN_MODBUS_INPUT, 10U, 2U);
|
||||
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
|
||||
CHECK(rsp.dlc == 4U && rsp.data[0] == 0x34U && rsp.data[1] == 0x12U);
|
||||
CHECK(rsp.data[2] == 0xCDU && rsp.data[3] == 0xABU);
|
||||
pcan_id_unpack(rsp.id, &id);
|
||||
CHECK(id.route == PCAN_ROUTE_FROM_DEVICE);
|
||||
|
||||
req = request(PCAN_MODBUS_HOLDING, 3U, 2U);
|
||||
req.dlc = 4U;
|
||||
req.data[0] = 0x22U; req.data[1] = 0x11U;
|
||||
req.data[2] = 0x44U; req.data[3] = 0x33U;
|
||||
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
|
||||
CHECK(holding[3] == 0x1122U && holding[4] == 0x3344U);
|
||||
|
||||
req = request(PCAN_MODBUS_COIL, 2U, 3U);
|
||||
req.dlc = 2U; req.data[0] = 0x05U; req.data[1] = 0U;
|
||||
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_RESPONSE);
|
||||
CHECK(coils == 0x0014U);
|
||||
|
||||
req = request(PCAN_MODBUS_INPUT, 0U, 5U);
|
||||
CHECK(pcan_modbus_server_handle(&server, &req, &rsp) == PCAN_MODBUS_HANDLED_NO_RESPONSE);
|
||||
CHECK(server.requests == 4U && server.responses == 3U && server.rejected == 1U);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "periph28335.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t request[PERIPH28335_REQUEST_SIZE];
|
||||
static const uint8_t read_prefix[] = { 0x10U, 0x03U, 0x00U, 0x18U, 0x00U, 0x40U };
|
||||
static const uint8_t response_payload[] = { 0x10U, 0x03U, 0x04U, 0x80U, 0x05U, 0x12U, 0x34U };
|
||||
uint8_t response[sizeof response_payload + 2U];
|
||||
uint16_t words[2];
|
||||
|
||||
if (periph28335_build_read_registers(16U, 24U, 64U, request, sizeof request)
|
||||
!= sizeof request || memcmp(request, read_prefix, sizeof read_prefix) != 0) return 1;
|
||||
if (periph28335_build_write_register(16U, 7U, 0x1234U, request, sizeof request)
|
||||
!= sizeof request || memcmp(request, "\x10\x06\x00\x07\x12\x34", 6U) != 0) return 2;
|
||||
if (periph28335_build_command(16U, 15U, request, sizeof request)
|
||||
!= sizeof request || memcmp(request, "\x10\x06\x00\x7F\x80\x00", 6U) != 0) return 3;
|
||||
if (periph28335_append_crc(response_payload, sizeof response_payload,
|
||||
response, sizeof response) != sizeof response) return 4;
|
||||
if (periph28335_decode_read_response(response, sizeof response, 16U, 2U,
|
||||
words, 2U) != PERIPH28335_OK || words[0] != 0x8005U || words[1] != 0x1234U) return 5;
|
||||
response[0] = 17U;
|
||||
if (periph28335_decode_read_response(response, sizeof response, 16U, 2U,
|
||||
words, 2U) != PERIPH28335_ERROR_CRC) return 6;
|
||||
if (periph28335_project_count() != 9U ||
|
||||
strcmp(periph28335_project_name(2U), "23550") != 0 ||
|
||||
strcmp(periph28335_command_name(2U, 7U), "Send") != 0) return 7;
|
||||
puts("PM35/TMS320F28335 protocol tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -26,13 +26,6 @@ int main(void) {
|
||||
assert(output[0] == y[0]);
|
||||
assert(set_plot_eval(SET_PLOT_DRAG, drag, 6, output, 4) == 1);
|
||||
assert(output[0] == -4);
|
||||
{
|
||||
const double limits[] = {0, 500, -2, 2};
|
||||
const double reversed[] = {1, 1, -2, 2};
|
||||
assert(set_plot_eval(SET_PLOT_LIMITS, limits, 4, output, 4) == 4);
|
||||
assert(output[0] == 0 && output[1] == 500 && output[2] == -2 && output[3] == 2);
|
||||
assert(set_plot_eval(SET_PLOT_LIMITS, reversed, 4, output, 4) == 0);
|
||||
}
|
||||
puts("shared plot: OK");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -17,15 +17,6 @@ int main(void)
|
||||
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);
|
||||
{
|
||||
const double amplitudes[] = {10.0, .01, .02, .8, .03, .4, .02};
|
||||
const double noise[] = {0, .10, .12, .11, .09, .10};
|
||||
double scratch[6], peak[2];
|
||||
assert(set_spectrum_dominant_peak(amplitudes, 7, 5, 3, 1e-6, scratch, 6, peak, 2) == 1);
|
||||
assert(peak[0] == 15 && peak[1] == .8);
|
||||
assert(set_spectrum_dominant_peak(noise, 6, 1, 3, 1e-6, scratch, 6, peak, 2) == 0);
|
||||
assert(set_spectrum_dominant_peak(amplitudes, 7, 0, 3, 1e-6, scratch, 6, peak, 2) == -1);
|
||||
}
|
||||
puts("shared spectrum: OK");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -34,11 +34,8 @@ int main(void)
|
||||
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];
|
||||
uint32_t timestamp = 0;
|
||||
assert(set_trend_watch_values(packet, sizeof(packet), words, 64) == 2);
|
||||
assert(words[0] == 0x1234 && words[1] == 0xFFFF);
|
||||
assert(set_trend_watch_decode(packet, sizeof(packet), ×tamp, words, 64) == 2);
|
||||
assert(timestamp == 0x04030201UL);
|
||||
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");
|
||||
|
||||
@@ -21,9 +21,9 @@ JNI_INCLUDES: list[Path] = []
|
||||
SOURCES = [
|
||||
ROOT / "src" / name for name in (
|
||||
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c",
|
||||
"balsam_can.c", "periph28335.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
|
||||
"balsam_can.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
|
||||
"pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",
|
||||
"pcan_gas.c",
|
||||
"pcan_gas.c", "pcan_modbus_server.c",
|
||||
)
|
||||
]
|
||||
|
||||
@@ -84,10 +84,7 @@ def main() -> int:
|
||||
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.extend([
|
||||
ROOT / "ports" / "android" / "set_plot_jni.c",
|
||||
ROOT / "ports" / "android" / "setprotocol_jni.c",
|
||||
])
|
||||
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"
|
||||
|
||||
379
doc/CAN_FRAME_PARSE_V1_V2.md
Normal file
379
doc/CAN_FRAME_PARSE_V1_V2.md
Normal file
@@ -0,0 +1,379 @@
|
||||
# Разбор CAN-кадров ProtoCAN Boot v1 и SETProtocol v2
|
||||
|
||||
Документ описывает wire-форматы двух протоколов обновления прошивки:
|
||||
|
||||
- **v1** — `templates/c/protocan-boot`, одна команда или 8 байт образа в одном
|
||||
Extended CAN-кадре;
|
||||
- **v2** — `templates/c/set-protocol`, полный кадр SETProtocol разбивается на
|
||||
несколько Extended CAN-кадров.
|
||||
|
||||
Все многобайтные поля payload передаются **little-endian**. CAN ID — 29-битный.
|
||||
Для рабочего кода нужно использовать канонические реализации из `templates`,
|
||||
а приведённый ниже Python-парсер удобен для анализатора, логов и отладки.
|
||||
|
||||
## 1. ProtoCAN Boot v1
|
||||
|
||||
### 1.1. Разметка Extended CAN ID
|
||||
|
||||
```text
|
||||
bits size field
|
||||
28 1 Priority
|
||||
27 1 Route: 0 = host -> device, 1 = device -> host
|
||||
26..24 3 Device Type
|
||||
23..20 4 Device ID
|
||||
19..16 4 Message Type
|
||||
15..0 16 Message Body
|
||||
```
|
||||
|
||||
Формула:
|
||||
|
||||
```text
|
||||
ID = Priority << 28 |
|
||||
Route << 27 |
|
||||
DeviceType << 24 |
|
||||
DeviceID << 20 |
|
||||
MessageType << 16 |
|
||||
MessageBody
|
||||
```
|
||||
|
||||
Типы загрузочных сообщений:
|
||||
|
||||
| Message Type | Имя | Message Body | CAN payload |
|
||||
|---:|---|---|---|
|
||||
| `0x9` | `BOOT_CONTROL` | `SessionID << 8 \| Command` | параметры команды |
|
||||
| `0xA` | `BOOT_DATA_A` | индекс блока | 8 байт слота A |
|
||||
| `0xB` | `BOOT_DATA_B` | индекс блока | 8 байт слота B |
|
||||
| `0xC` | `BOOT_STATUS` | `SessionID << 8 \| Command` | статус и прогресс |
|
||||
| `0xD` | `BOOT_DISCOVERY` | подтип | информация об устройстве |
|
||||
|
||||
Команды `BOOT_CONTROL`:
|
||||
|
||||
| Код | Команда | Payload |
|
||||
|---:|---|---|
|
||||
| `0x01` | `IDENTIFY` | пустой |
|
||||
| `0x02` | `ENTER_BOOT` | пустой |
|
||||
| `0x03` | `BEGIN_IMAGE` | `image_size u32`, `image_crc32 u32` |
|
||||
| `0x04` | `BEGIN_COMPAT` | `product u16`, `hw_min u8`, `hw_max u8`, `version u32` |
|
||||
| `0x05` | `ERASE` | пустой |
|
||||
| `0x06` | `VERIFY` | пустой |
|
||||
| `0x07` | `COMMIT` | пустой |
|
||||
| `0x08` | `CONFIRM` | пустой |
|
||||
| `0x09` | `REBOOT` | пустой |
|
||||
| `0x0A` | `ABORT` | пустой |
|
||||
| `0x0B` | `QUERY_PROGRESS` | пустой |
|
||||
|
||||
`BOOT_STATUS` всегда содержит 8 байт:
|
||||
|
||||
```text
|
||||
offset size field
|
||||
0 1 status
|
||||
1 1 target_slot
|
||||
2 2 next_block u16 LE
|
||||
4 4 running_crc32 u32 LE
|
||||
```
|
||||
|
||||
`BOOT_DISCOVERY` с body `1` содержит:
|
||||
|
||||
```text
|
||||
offset size field
|
||||
0 2 product_type u16 LE
|
||||
2 1 hardware_revision
|
||||
3 1 protocol_version = 1
|
||||
4 4 firmware_version u32 LE
|
||||
```
|
||||
|
||||
Пример запроса `IDENTIFY` для `DeviceType=7`, `DeviceID=13`:
|
||||
|
||||
```text
|
||||
CAN ID: 17D90001
|
||||
DLC: 0
|
||||
```
|
||||
|
||||
### 1.2. Python-парсер v1
|
||||
|
||||
```python
|
||||
def parse_v1(can_id: int, data: bytes) -> dict:
|
||||
if not 0 <= can_id <= 0x1FFFFFFF:
|
||||
raise ValueError("неверный Extended CAN ID")
|
||||
if len(data) > 8:
|
||||
raise ValueError("DLC больше 8")
|
||||
|
||||
result = {
|
||||
"version": 1,
|
||||
"priority": (can_id >> 28) & 0x01,
|
||||
"route": (can_id >> 27) & 0x01,
|
||||
"device_type": (can_id >> 24) & 0x07,
|
||||
"device_id": (can_id >> 20) & 0x0F,
|
||||
"message_type": (can_id >> 16) & 0x0F,
|
||||
"message_body": can_id & 0xFFFF,
|
||||
"data": bytes(data),
|
||||
}
|
||||
|
||||
msg_type = result["message_type"]
|
||||
body = result["message_body"]
|
||||
if msg_type in (0x9, 0xC):
|
||||
result["session_id"] = (body >> 8) & 0xFF
|
||||
result["command"] = body & 0xFF
|
||||
elif msg_type in (0xA, 0xB):
|
||||
result["slot"] = msg_type - 0xA
|
||||
result["block_index"] = body
|
||||
|
||||
if msg_type == 0xC:
|
||||
if len(data) != 8:
|
||||
raise ValueError("BOOT_STATUS должен содержать 8 байт")
|
||||
result.update({
|
||||
"status": data[0],
|
||||
"target_slot": data[1],
|
||||
"next_block": int.from_bytes(data[2:4], "little"),
|
||||
"running_crc32": int.from_bytes(data[4:8], "little"),
|
||||
})
|
||||
elif msg_type == 0xD and body == 1:
|
||||
if len(data) != 8:
|
||||
raise ValueError("BOOT_DISCOVERY должен содержать 8 байт")
|
||||
result.update({
|
||||
"product_type": int.from_bytes(data[0:2], "little"),
|
||||
"hardware_revision": data[2],
|
||||
"protocol_version": data[3],
|
||||
"firmware_version": int.from_bytes(data[4:8], "little"),
|
||||
})
|
||||
return result
|
||||
```
|
||||
|
||||
## 2. SETProtocol v2 поверх classic CAN
|
||||
|
||||
В v2 CAN-кадр является только транспортным сегментом. Сначала нужно собрать
|
||||
полный SETP-пакет, и только затем разбирать его заголовок, payload и CRC32.
|
||||
|
||||
### 2.1. Разметка Extended CAN ID
|
||||
|
||||
```text
|
||||
bits size field
|
||||
28..24 5 Prefix = 0x12
|
||||
23..16 8 Destination node
|
||||
15..8 8 Source node
|
||||
7 1 Priority
|
||||
6..0 7 Channel
|
||||
```
|
||||
|
||||
Формула:
|
||||
|
||||
```text
|
||||
ID = 0x12 << 24 |
|
||||
Destination << 16 |
|
||||
Source << 8 |
|
||||
Priority << 7 |
|
||||
Channel
|
||||
```
|
||||
|
||||
### 2.2. CAN-сегменты
|
||||
|
||||
Первый байт CAN payload — PCI:
|
||||
|
||||
| PCI | Назначение | Формат CAN payload |
|
||||
|---:|---|---|
|
||||
| `0x10` | первый сегмент | `10`, `total_length u16 LE`, первые 5 байт SETP |
|
||||
| `0x20..0x2F` | продолжение | `2N`, следующие 1–7 байт SETP |
|
||||
| `0x30..0x32` | flow control | `3S`, `block_size`, `st_min_ms` |
|
||||
|
||||
`N` — циклический номер сегмента `1..15,0..`; следующий сегмент обязан иметь
|
||||
ожидаемый номер, тот же CAN ID и прийти до тайм-аута сборки 500 мс.
|
||||
|
||||
### 2.3. Внутренний кадр SETProtocol v2
|
||||
|
||||
```text
|
||||
offset size field
|
||||
0 2 SOF = A5 5A
|
||||
2 1 version = 02
|
||||
3 1 flags
|
||||
4 2 message_type u16 LE
|
||||
6 2 source u16 LE
|
||||
8 2 destination u16 LE
|
||||
10 2 sequence u16 LE
|
||||
12 2 payload_length u16 LE
|
||||
14 N payload
|
||||
14+N 4 CRC32 IEEE u32 LE
|
||||
```
|
||||
|
||||
CRC32 считается по байтам от `version` на offset 2 до конца payload. Поля
|
||||
`source`, `destination` и `priority` внутреннего заголовка должны совпадать с
|
||||
CAN ID.
|
||||
|
||||
Флаги:
|
||||
|
||||
| Бит | Значение |
|
||||
|---:|---|
|
||||
| `0x01` | RESPONSE |
|
||||
| `0x02` | EVENT |
|
||||
| `0x04` | ERROR |
|
||||
| `0x08` | ACK_REQUIRED |
|
||||
| `0x10` | MORE |
|
||||
| `0x20` | PRIORITY |
|
||||
|
||||
Каждый response начинается с `status u16 LE`. Основные firmware message types:
|
||||
`FW_BEGIN=0x0100`, `FW_DATA=0x0101`, `FW_END=0x0102`, `FW_ABORT=0x0103`,
|
||||
`FW_STATUS=0x0104`, `FW_ACTIVATE=0x0105`.
|
||||
|
||||
Пример `PING` к BALZAM node `13`, source `0`, sequence `1`, priority `1`,
|
||||
channel `1`:
|
||||
|
||||
```text
|
||||
Полный SETP:
|
||||
A5 5A 02 28 01 00 00 00 0D 00 01 00 00 00 E7 29 51 40
|
||||
|
||||
CAN ID 120D0081, сегменты:
|
||||
10 12 00 A5 5A 02 28 01
|
||||
21 00 00 00 0D 00 01 00
|
||||
22 00 00 E7 29 51 40
|
||||
```
|
||||
|
||||
### 2.4. Python-парсер и сборщик v2
|
||||
|
||||
```python
|
||||
import binascii
|
||||
|
||||
|
||||
def parse_v2_can_id(can_id: int) -> dict:
|
||||
if not 0 <= can_id <= 0x1FFFFFFF:
|
||||
raise ValueError("неверный Extended CAN ID")
|
||||
if (can_id >> 24) & 0x1F != 0x12:
|
||||
raise ValueError("не SETProtocol v2 CAN ID")
|
||||
return {
|
||||
"destination": (can_id >> 16) & 0xFF,
|
||||
"source": (can_id >> 8) & 0xFF,
|
||||
"priority": (can_id >> 7) & 0x01,
|
||||
"channel": can_id & 0x7F,
|
||||
}
|
||||
|
||||
|
||||
def parse_setp(packet: bytes, can_id: int) -> dict:
|
||||
if len(packet) < 18 or packet[:2] != b"\xA5\x5A":
|
||||
raise ValueError("нет полного SETP-кадра")
|
||||
if packet[2] != 2:
|
||||
raise ValueError("неподдерживаемая версия SETP")
|
||||
flags = packet[3]
|
||||
if flags & 0xC0:
|
||||
raise ValueError("установлены зарезервированные флаги")
|
||||
payload_length = int.from_bytes(packet[12:14], "little")
|
||||
if len(packet) != 14 + payload_length + 4:
|
||||
raise ValueError("не совпадает payload_length")
|
||||
expected_crc = int.from_bytes(packet[-4:], "little")
|
||||
actual_crc = binascii.crc32(packet[2:-4]) & 0xFFFFFFFF
|
||||
if actual_crc != expected_crc:
|
||||
raise ValueError("ошибка CRC32 SETP")
|
||||
|
||||
address = parse_v2_can_id(can_id)
|
||||
source = int.from_bytes(packet[6:8], "little")
|
||||
destination = int.from_bytes(packet[8:10], "little")
|
||||
priority = int(bool(flags & 0x20))
|
||||
if (source, destination, priority) != (
|
||||
address["source"], address["destination"], address["priority"]
|
||||
):
|
||||
raise ValueError("SETP header не совпадает с CAN ID")
|
||||
|
||||
payload = packet[14:-4]
|
||||
result = {
|
||||
"version": 2,
|
||||
"flags": flags,
|
||||
"message_type": int.from_bytes(packet[4:6], "little"),
|
||||
"source": source,
|
||||
"destination": destination,
|
||||
"sequence": int.from_bytes(packet[10:12], "little"),
|
||||
"payload": payload,
|
||||
"can": address,
|
||||
}
|
||||
if flags & 0x01:
|
||||
if len(payload) < 2:
|
||||
raise ValueError("response не содержит status")
|
||||
result["status"] = int.from_bytes(payload[:2], "little")
|
||||
result["body"] = payload[2:]
|
||||
return result
|
||||
|
||||
|
||||
class V2CanReassembler:
|
||||
def __init__(self, timeout_ms: int = 500):
|
||||
self.timeout_ms = timeout_ms
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.can_id = None
|
||||
self.total = 0
|
||||
self.data = bytearray()
|
||||
self.next_sequence = 1
|
||||
self.deadline_ms = 0
|
||||
|
||||
def feed(self, can_id: int, data: bytes, now_ms: int):
|
||||
parse_v2_can_id(can_id)
|
||||
if not 1 <= len(data) <= 8:
|
||||
raise ValueError("DLC вне диапазона 1..8")
|
||||
if self.can_id is not None and now_ms >= self.deadline_ms:
|
||||
self.reset()
|
||||
raise ValueError("тайм-аут сборки SETP")
|
||||
|
||||
pci_type = data[0] & 0xF0
|
||||
if pci_type == 0x10:
|
||||
if len(data) != 8:
|
||||
raise ValueError("первый сегмент должен иметь DLC 8")
|
||||
total = int.from_bytes(data[1:3], "little")
|
||||
if not 18 <= total <= 530:
|
||||
raise ValueError("неверный размер SETP")
|
||||
self.can_id = can_id
|
||||
self.total = total
|
||||
self.data = bytearray(data[3:])
|
||||
self.next_sequence = 1
|
||||
self.deadline_ms = now_ms + self.timeout_ms
|
||||
return None
|
||||
|
||||
if pci_type == 0x20:
|
||||
sequence = data[0] & 0x0F
|
||||
if (
|
||||
self.can_id is None
|
||||
or can_id != self.can_id
|
||||
or sequence != self.next_sequence
|
||||
or len(data) < 2
|
||||
):
|
||||
self.reset()
|
||||
raise ValueError("ошибка последовательности CAN-сегментов")
|
||||
if len(data) - 1 > self.total - len(self.data):
|
||||
self.reset()
|
||||
raise ValueError("лишние байты CAN-сегмента")
|
||||
self.data.extend(data[1:])
|
||||
self.next_sequence = (self.next_sequence + 1) & 0x0F
|
||||
self.deadline_ms = now_ms + self.timeout_ms
|
||||
if len(self.data) == self.total:
|
||||
packet = bytes(self.data)
|
||||
packet_can_id = self.can_id
|
||||
self.reset()
|
||||
return parse_setp(packet, packet_can_id)
|
||||
return None
|
||||
|
||||
if pci_type == 0x30:
|
||||
return {"flow_control": data[0] & 0x0F, "data": data[1:]}
|
||||
raise ValueError("неизвестный PCI")
|
||||
```
|
||||
|
||||
В SETGUI эти операции уже реализованы в
|
||||
`third_party/templates/python/setprotocol/can.py`; собственный parser нужен
|
||||
только внешнему анализатору или диагностическому скрипту.
|
||||
|
||||
## 3. Как отличать v1 от v2
|
||||
|
||||
Для используемых сейчас адресов достаточно следующих признаков:
|
||||
|
||||
- v2: верхние пять бит CAN ID равны `0x12`, PCI начинается с `0x10`, `0x2N`
|
||||
или `0x3S`, после reassembly присутствует `A5 5A 02`;
|
||||
- v1: `MessageType` в битах `19..16` равен `0x9..0xD`, каждый кадр разбирается
|
||||
самостоятельно.
|
||||
|
||||
Однако универсальное автоопределение только по одному CAN ID невозможно:
|
||||
комбинация `Priority/Route/DeviceType` v1 теоретически тоже может дать верхнее
|
||||
поле `0x12`, а первый байт firmware data v1 может случайно совпасть с PCI.
|
||||
Надёжный анализатор должен учитывать настроенный режим узла либо подтвердить v2
|
||||
только после сборки кадра с корректными `A5 5A 02`, длиной и CRC32.
|
||||
|
||||
## 4. Канонические исходники
|
||||
|
||||
- v1 ID и state machine: `third_party/templates/c/protocan-boot/src/pcan_boot.c`;
|
||||
- v2 CAN transport: `third_party/templates/c/set-protocol/src/set_can.c`;
|
||||
- v2 frame/CRC: `third_party/templates/c/set-protocol/src/set_protocol.c`;
|
||||
- v2 firmware payload: `third_party/templates/c/set-protocol/src/set_firmware.c`;
|
||||
- Python v2 CAN: `third_party/templates/python/setprotocol/can.py`.
|
||||
@@ -1,46 +0,0 @@
|
||||
# Аудит общего кода GUI_Android и SETGUI
|
||||
|
||||
Дата проверки: 2026-09-04.
|
||||
|
||||
## Уже является общим C-ядром
|
||||
|
||||
| Область | Каноническая реализация |
|
||||
|---|---|
|
||||
| SETProtocol v2, CAN segmentation, firmware flow, telemetry | `c/set-protocol/src/set_*.c` |
|
||||
| ProtoCAN ID, кадр, parser, link, CRC-CCITT | `c/set-protocol/src/pcan_*.c` |
|
||||
| GUI frame и каталог | `c/set-protocol/src/gui_*.c` |
|
||||
| CAN Бальзам | `c/set-protocol/src/balsam_can.c` |
|
||||
| График, тренды и спектр | `c/set-protocol/src/set_plot.c`, `set_trends.c`, `set_spectrum.c` |
|
||||
| ПМ35 / TMS320F28335: MODBUS-подобные запросы, CRC16/Modbus, ответы и каталог команд | `c/set-protocol/src/periph28335.c` |
|
||||
|
||||
Python вызывает это ядро через `python/protocan/native.py`, Android — через
|
||||
`ports/android/setprotocol_jni.c`. Файлы на Python и Kotlin являются портами и
|
||||
не должны содержать wire-алгоритм. В старых портах ещё встречаются fallback-
|
||||
реализации; они отмечены ниже и удаляются по мере включения обязательной
|
||||
загрузки C-ядра.
|
||||
|
||||
## Найденное общее, которое ещё нужно перенести
|
||||
|
||||
| Приоритет | Android | SETGUI | Что вынести в C |
|
||||
|---|---|---|---|
|
||||
| 0 | `ports/android/.../CanBridgeProtocol.kt`, `GuiProtocol.kt` | Python transport wrappers | удалить оставшиеся алгоритмические fallback, так как codec/parser уже есть в C |
|
||||
| 1 | `core/Tms2812Protocol.kt` | `core/tms_flash.py`, `core/tms_terminal.py` | весь RS-протокол ПМ67/2812, CRC, команды и validators |
|
||||
| 2 | `core/Ds18b20Protocol.kt` | `core/ds18b20.py` | кодеки списков/данных/EEPROM, CRC8 и значения датчиков |
|
||||
| 3 | `core/CanFirmwareProtocol.kt` | `core/can_firmware.py` | автомат CAN boot, статусы, окна блоков |
|
||||
| 4 | `core/SlCanProtocol.kt` | `adapters/slcan_adapter.py` и CAN transport | ASCII codec/parser SLCAN; доступ к COM остаётся в портах |
|
||||
| 5 | `core/FirmwareImage.kt` | `core/firmware.py` | Intel HEX parser, CRC32/SHA и правила диапазонов |
|
||||
|
||||
Это очередь миграции, а не разрешение поддерживать две реализации. При первом
|
||||
изменении любой строки из таблицы сначала создаётся соответствующий модуль C и
|
||||
общие тестовые векторы.
|
||||
|
||||
## Что должно остаться платформенным
|
||||
|
||||
- Compose и PySide widgets, навигация и внешний вид;
|
||||
- Android USB host, разрешения, foreground lifecycle;
|
||||
- Windows COM/MOXA/драйверы и выбор последовательного порта;
|
||||
- хранилище настроек, диалоги файлов и уведомления;
|
||||
- привязка моделей C к Kotlin/Python и локализованные сообщения UI.
|
||||
|
||||
Таким образом, язык GUI можно менять без повторного написания протокола: новый
|
||||
клиент реализует только FFI и свой транспортный/UI-порт.
|
||||
@@ -4,9 +4,22 @@ param()
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$outputPath = Join-Path $PSScriptRoot 'setprotocol.html'
|
||||
$sourcePath = Join-Path $PSScriptRoot '..\c\set-protocol\docs\SETPROTOCOL.md'
|
||||
$canFrameSourcePath = Join-Path $PSScriptRoot 'CAN_FRAME_PARSE_V1_V2.md'
|
||||
|
||||
$markdown = Get-Content -Raw -LiteralPath $sourcePath -Encoding UTF8
|
||||
$html = (ConvertFrom-Markdown -InputObject $markdown).Html
|
||||
function Convert-DocumentationMarkdown {
|
||||
param([string]$Path)
|
||||
|
||||
$markdown = Get-Content -Raw -LiteralPath $Path -Encoding UTF8
|
||||
$html = (ConvertFrom-Markdown -InputObject $markdown).Html
|
||||
|
||||
# Wide protocol tables must scroll horizontally instead of squeezing their
|
||||
# contents into unreadable one-character columns on a narrow viewport.
|
||||
$html = $html -replace '<table>', '<div class="table-wrap"><table>'
|
||||
$html = $html -replace '</table>', '</table></div>'
|
||||
return $html
|
||||
}
|
||||
|
||||
$html = Convert-DocumentationMarkdown -Path $sourcePath
|
||||
$generatedBlock = @"
|
||||
<!-- SETPROTOCOL:START -->
|
||||
<article class="card full-doc" data-source="c/set-protocol/docs/SETPROTOCOL.md">
|
||||
@@ -15,6 +28,15 @@ $html
|
||||
<!-- SETPROTOCOL:END -->
|
||||
"@
|
||||
|
||||
$canFrameHtml = Convert-DocumentationMarkdown -Path $canFrameSourcePath
|
||||
$canFrameBlock = @"
|
||||
<!-- CAN-FRAME-PARSE:START -->
|
||||
<article class="card full-doc" data-source="doc/CAN_FRAME_PARSE_V1_V2.md">
|
||||
$canFrameHtml
|
||||
</article>
|
||||
<!-- CAN-FRAME-PARSE:END -->
|
||||
"@
|
||||
|
||||
$page = Get-Content -Raw -LiteralPath $outputPath -Encoding UTF8
|
||||
$pattern = '(?s)<!-- SETPROTOCOL:START -->.*?<!-- SETPROTOCOL:END -->'
|
||||
if ($page -notmatch $pattern) {
|
||||
@@ -26,6 +48,16 @@ $page = [regex]::Replace($page, $pattern, [System.Text.RegularExpressions.MatchE
|
||||
$generatedBlock
|
||||
}, 1)
|
||||
|
||||
$canFramePattern = '(?s)<!-- CAN-FRAME-PARSE:START -->.*?<!-- CAN-FRAME-PARSE:END -->'
|
||||
if ($page -notmatch $canFramePattern) {
|
||||
throw 'Не найдены маркеры CAN-FRAME-PARSE:START/END в doc/setprotocol.html.'
|
||||
}
|
||||
|
||||
$page = [regex]::Replace($page, $canFramePattern, [System.Text.RegularExpressions.MatchEvaluator]{
|
||||
param($match)
|
||||
$canFrameBlock
|
||||
}, 1)
|
||||
|
||||
$page = $page.TrimEnd("`r", "`n") + [Environment]::NewLine
|
||||
$utf8WithoutBom = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($outputPath, $page, $utf8WithoutBom)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
60
python/protocan/devboard_v1.py
Normal file
60
python/protocan/devboard_v1.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""CAN485 DevBoard_V1 USB command and frame helpers (no GUI/serial dependency)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
FRAME_RE = re.compile(
|
||||
r"^\[\s*(\d+)\.(\d{3})\]\s+(?:(RS485)\s+)?"
|
||||
r"(EXT|STD)\s+0x([0-9A-Fa-f]+)\s+"
|
||||
r"(RTR\s+)?DLC=(\d)(?:\s+DATA=([0-9A-Fa-f ]*))?"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BoardFrame:
|
||||
timestamp: float
|
||||
extended: bool
|
||||
can_id: int
|
||||
data: bytes
|
||||
rtr: bool = False
|
||||
source: str = "CAN"
|
||||
|
||||
|
||||
def parse_frame_line(line: str) -> BoardFrame | None:
|
||||
match = FRAME_RE.match(line.strip())
|
||||
if match is None:
|
||||
return None
|
||||
seconds, millis, source, frame_type, raw_id, raw_rtr, raw_dlc, raw_data = match.groups()
|
||||
dlc = int(raw_dlc)
|
||||
data = bytes.fromhex(raw_data or "")
|
||||
rtr = bool(raw_rtr)
|
||||
if (not rtr and len(data) != dlc) or (rtr and data):
|
||||
return None
|
||||
extended = frame_type == "EXT"
|
||||
can_id = int(raw_id, 16)
|
||||
if can_id > (0x1FFFFFFF if extended else 0x7FF):
|
||||
return None
|
||||
return BoardFrame(int(seconds) + int(millis) / 1000.0, extended, can_id,
|
||||
data, rtr, source or "CAN")
|
||||
|
||||
|
||||
def command_transmit(frame: BoardFrame) -> bytes:
|
||||
if len(frame.data) > 8:
|
||||
raise ValueError("DLC cannot exceed 8")
|
||||
kind = "E" if frame.extended else "S"
|
||||
return f"T,{kind},{frame.can_id:X},{len(frame.data)},{frame.data.hex().upper()}\n".encode("ascii")
|
||||
|
||||
|
||||
def command_setup(*, can_bitrate: int = 500, rs485_baud: int = 512000,
|
||||
route: int = 3) -> tuple[bytes, ...]:
|
||||
if can_bitrate not in (25, 50, 100, 125, 250, 500, 800, 1000):
|
||||
raise ValueError("unsupported CAN bitrate")
|
||||
if not 1200 <= rs485_baud <= 4_000_000:
|
||||
raise ValueError("unsupported RS485 baud")
|
||||
if route not in range(4):
|
||||
raise ValueError("route must be 0..3")
|
||||
return (f"S{can_bitrate}\n".encode("ascii"), b"M0\n",
|
||||
f"B{rs485_baud}\n".encode("ascii"), f"Q{route}\n".encode("ascii"))
|
||||
@@ -139,44 +139,6 @@ class NativeProtocol:
|
||||
ctypes.c_uint8, ctypes.c_uint16, ctypes.c_void_p, ctypes.c_size_t,
|
||||
]
|
||||
lib.pcan_abi_balsam_register_name.restype = ctypes.c_size_t
|
||||
lib.pcan_abi_periph28335_crc16.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
||||
lib.pcan_abi_periph28335_crc16.restype = ctypes.c_uint16
|
||||
lib.pcan_abi_periph28335_append_crc.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t,
|
||||
]
|
||||
lib.pcan_abi_periph28335_append_crc.restype = ctypes.c_size_t
|
||||
lib.pcan_abi_periph28335_build_read.argtypes = [
|
||||
ctypes.c_uint8, ctypes.c_uint16, ctypes.c_uint16,
|
||||
ctypes.c_void_p, ctypes.c_size_t,
|
||||
]
|
||||
lib.pcan_abi_periph28335_build_read.restype = ctypes.c_size_t
|
||||
lib.pcan_abi_periph28335_build_write.argtypes = [
|
||||
ctypes.c_uint8, ctypes.c_uint16, ctypes.c_uint16,
|
||||
ctypes.c_void_p, ctypes.c_size_t,
|
||||
]
|
||||
lib.pcan_abi_periph28335_build_write.restype = ctypes.c_size_t
|
||||
lib.pcan_abi_periph28335_build_command.argtypes = [
|
||||
ctypes.c_uint8, ctypes.c_uint8, ctypes.c_void_p, ctypes.c_size_t,
|
||||
]
|
||||
lib.pcan_abi_periph28335_build_command.restype = ctypes.c_size_t
|
||||
lib.pcan_abi_periph28335_expected_read_size.argtypes = [ctypes.c_uint16]
|
||||
lib.pcan_abi_periph28335_expected_read_size.restype = ctypes.c_size_t
|
||||
lib.pcan_abi_periph28335_decode_read.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint8, ctypes.c_uint16,
|
||||
ctypes.POINTER(ctypes.c_uint16), ctypes.c_size_t,
|
||||
]
|
||||
lib.pcan_abi_periph28335_decode_read.restype = ctypes.c_int
|
||||
lib.pcan_abi_periph28335_validate_write.argtypes = [
|
||||
ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t,
|
||||
]
|
||||
lib.pcan_abi_periph28335_validate_write.restype = ctypes.c_int
|
||||
lib.pcan_abi_periph28335_project_count.restype = ctypes.c_size_t
|
||||
lib.pcan_abi_periph28335_project_name.argtypes = [ctypes.c_size_t]
|
||||
lib.pcan_abi_periph28335_project_name.restype = ctypes.c_char_p
|
||||
lib.pcan_abi_periph28335_command_name.argtypes = [
|
||||
ctypes.c_size_t, ctypes.c_size_t,
|
||||
]
|
||||
lib.pcan_abi_periph28335_command_name.restype = ctypes.c_char_p
|
||||
lib.pcan_abi_frame_encode.argtypes = [
|
||||
ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint32,
|
||||
ctypes.c_void_p, ctypes.c_uint8, ctypes.c_void_p, ctypes.c_size_t,
|
||||
@@ -258,83 +220,6 @@ class NativeProtocol:
|
||||
device, address, output, len(output))
|
||||
return output.value.decode("utf-8")
|
||||
|
||||
@staticmethod
|
||||
def _bytes_buffer(data: bytes):
|
||||
return (ctypes.c_uint8 * len(data)).from_buffer_copy(data) if data else None
|
||||
|
||||
def periph28335_crc16(self, data: bytes) -> int:
|
||||
return int(self.lib.pcan_abi_periph28335_crc16(
|
||||
self._bytes_buffer(data), len(data)))
|
||||
|
||||
def periph28335_append_crc(self, payload: bytes) -> bytes:
|
||||
source = self._bytes_buffer(payload)
|
||||
output = (ctypes.c_uint8 * (len(payload) + 2))()
|
||||
size = int(self.lib.pcan_abi_periph28335_append_crc(
|
||||
source, len(payload), output, len(output)))
|
||||
if size == 0:
|
||||
raise ValueError("SETProtocol rejected PM35 payload")
|
||||
return bytes(output[:size])
|
||||
|
||||
def periph28335_build_read(self, controller: int, start: int,
|
||||
count: int) -> bytes:
|
||||
output = (ctypes.c_uint8 * 8)()
|
||||
size = int(self.lib.pcan_abi_periph28335_build_read(
|
||||
controller, start, count, output, len(output)))
|
||||
if size == 0:
|
||||
raise ValueError("SETProtocol rejected PM35 read request")
|
||||
return bytes(output[:size])
|
||||
|
||||
def periph28335_build_write(self, controller: int, address: int,
|
||||
value: int) -> bytes:
|
||||
output = (ctypes.c_uint8 * 8)()
|
||||
size = int(self.lib.pcan_abi_periph28335_build_write(
|
||||
controller, address, value, output, len(output)))
|
||||
if size == 0:
|
||||
raise ValueError("SETProtocol rejected PM35 write request")
|
||||
return bytes(output[:size])
|
||||
|
||||
def periph28335_build_command(self, controller: int,
|
||||
command_index: int) -> bytes:
|
||||
output = (ctypes.c_uint8 * 8)()
|
||||
size = int(self.lib.pcan_abi_periph28335_build_command(
|
||||
controller, command_index, output, len(output)))
|
||||
if size == 0:
|
||||
raise ValueError("SETProtocol rejected PM35 command")
|
||||
return bytes(output[:size])
|
||||
|
||||
def periph28335_expected_read_size(self, count: int) -> int:
|
||||
return int(self.lib.pcan_abi_periph28335_expected_read_size(count))
|
||||
|
||||
def periph28335_decode_read(self, data: bytes, controller: int,
|
||||
count: int) -> tuple[int, tuple[int, ...]]:
|
||||
source = self._bytes_buffer(data)
|
||||
output = (ctypes.c_uint16 * count)()
|
||||
status = int(self.lib.pcan_abi_periph28335_decode_read(
|
||||
source, len(data), controller, count, output, count))
|
||||
return status, tuple(int(value) for value in output) if status == 0 else ()
|
||||
|
||||
def periph28335_validate_write(self, response: bytes,
|
||||
request: bytes) -> int:
|
||||
return int(self.lib.pcan_abi_periph28335_validate_write(
|
||||
self._bytes_buffer(response), len(response),
|
||||
self._bytes_buffer(request), len(request)))
|
||||
|
||||
def periph28335_catalog(self) -> dict[str, tuple[str, ...]]:
|
||||
result: dict[str, tuple[str, ...]] = {}
|
||||
for project in range(int(self.lib.pcan_abi_periph28335_project_count())):
|
||||
raw_name = self.lib.pcan_abi_periph28335_project_name(project)
|
||||
if not raw_name:
|
||||
raise NativeProtocolUnavailable("invalid PM35 project catalog")
|
||||
commands = []
|
||||
for command in range(17):
|
||||
raw_command = self.lib.pcan_abi_periph28335_command_name(
|
||||
project, command)
|
||||
if raw_command is None:
|
||||
raise NativeProtocolUnavailable("invalid PM35 command catalog")
|
||||
commands.append(raw_command.decode("utf-8"))
|
||||
result[raw_name.decode("utf-8")] = tuple(commands)
|
||||
return result
|
||||
|
||||
def encode(self, sequence: int, flags: int, can_id: int, data: bytes) -> bytes:
|
||||
if len(data) > 8:
|
||||
raise ValueError("DLC cannot exceed 8 bytes")
|
||||
|
||||
@@ -1,85 +1,66 @@
|
||||
"""Thin Python port of the shared C99 PM35/TMS320F28335 protocol."""
|
||||
"""Portable RS command helpers from Set_Terminal_28335.
|
||||
|
||||
The byte order and CRC match ``DTrans.pas``/``UNiiefa.pas``. This module is
|
||||
deliberately UI- and serial-port-independent so desktop and Android clients
|
||||
can share the same request builders.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .native import get_native_protocol
|
||||
|
||||
|
||||
REGISTER_COUNT = 128
|
||||
DEFAULT_CONTROLLER = 16
|
||||
DEFAULT_BAUD_RATE = 115_200
|
||||
|
||||
|
||||
def _core():
|
||||
return get_native_protocol()
|
||||
|
||||
|
||||
def crc16_modbus(data: bytes, crc: int = 0xFFFF) -> int:
|
||||
if crc != 0xFFFF:
|
||||
raise ValueError("Произвольное начальное значение CRC не входит в протокол ПМ35")
|
||||
return _core().periph28335_crc16(bytes(data))
|
||||
for byte in data:
|
||||
crc ^= byte
|
||||
for _ in range(8):
|
||||
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
|
||||
return crc & 0xFFFF
|
||||
|
||||
|
||||
def with_crc(payload: bytes) -> bytes:
|
||||
return _core().periph28335_append_crc(bytes(payload))
|
||||
crc = crc16_modbus(payload)
|
||||
return bytes(payload) + crc.to_bytes(2, "little")
|
||||
|
||||
|
||||
def build_read_registers(controller: int, start: int, count: int) -> bytes:
|
||||
_range("Адрес контроллера", controller, 0xFF)
|
||||
_range("Начальный регистр", start, 0xFFFF)
|
||||
if not 1 <= count <= REGISTER_COUNT or start + count > REGISTER_COUNT:
|
||||
if not 1 <= count <= 128 or start + count > 128:
|
||||
raise ValueError("Диапазон регистров должен находиться в 0..127")
|
||||
return _core().periph28335_build_read(controller, start, count)
|
||||
return with_crc(bytes((controller, 3)) + start.to_bytes(2, "big")
|
||||
+ count.to_bytes(2, "big"))
|
||||
|
||||
|
||||
def build_write_register(controller: int, address: int, value: int) -> bytes:
|
||||
_range("Адрес контроллера", controller, 0xFF)
|
||||
_range("Адрес регистра", address, REGISTER_COUNT - 1)
|
||||
_range("Адрес регистра", address, 127)
|
||||
_range("Значение", value, 0xFFFF)
|
||||
return _core().periph28335_build_write(controller, address, value)
|
||||
return with_crc(bytes((controller, 6)) + address.to_bytes(2, "big")
|
||||
+ value.to_bytes(2, "big"))
|
||||
|
||||
|
||||
def build_command(controller: int, command_index: int) -> bytes:
|
||||
_range("Адрес контроллера", controller, 0xFF)
|
||||
if not 0 <= command_index <= 16:
|
||||
raise ValueError("Номер команды должен быть в диапазоне 0..16")
|
||||
return _core().periph28335_build_command(controller, command_index)
|
||||
value = 1 << command_index if command_index < 16 else 0
|
||||
return build_write_register(controller, 127, value)
|
||||
|
||||
|
||||
def expected_read_response_size(count: int) -> int:
|
||||
if not 1 <= count <= REGISTER_COUNT:
|
||||
raise ValueError("Число регистров должно быть в диапазоне 1..128")
|
||||
size = _core().periph28335_expected_read_size(count)
|
||||
if size == 0:
|
||||
raise ValueError("SETProtocol отклонил размер ответа ПМ35")
|
||||
return size
|
||||
return count * 2 + 5
|
||||
|
||||
|
||||
def decode_read_response(data: bytes, count: int,
|
||||
controller: int | None = None) -> tuple[int, ...]:
|
||||
def decode_read_response(data: bytes, count: int) -> tuple[int, ...]:
|
||||
expected = expected_read_response_size(count)
|
||||
if len(data) != expected:
|
||||
raise ValueError(f"Ожидалось {expected} байт, получено {len(data)}")
|
||||
expected_controller = data[0] if controller is None and data else controller
|
||||
if expected_controller is None:
|
||||
raise ValueError("Пустой ответ ПМ35")
|
||||
status, values = _core().periph28335_decode_read(
|
||||
bytes(data), expected_controller, count)
|
||||
errors = {
|
||||
-1: "Неверные аргументы ответа",
|
||||
-2: "Диапазон регистров вне 0..127",
|
||||
-3: "Неверная длина данных ответа",
|
||||
-4: "Ошибка CRC ответа",
|
||||
-5: "Неверный заголовок ответа",
|
||||
-6: "Недостаточный буфер ответа",
|
||||
}
|
||||
if status != 0:
|
||||
raise ValueError(errors.get(status, f"Ошибка ответа ПМ35: {status}"))
|
||||
return values
|
||||
|
||||
|
||||
def validate_write_response(data: bytes, request: bytes) -> bool:
|
||||
return _core().periph28335_validate_write(bytes(data), bytes(request)) == 0
|
||||
if crc16_modbus(data[:-2]) != int.from_bytes(data[-2:], "little"):
|
||||
raise ValueError("Ошибка CRC ответа")
|
||||
# Historical replies have a three-byte header; registers are big-endian.
|
||||
body = data[3:-2]
|
||||
if len(body) != count * 2:
|
||||
raise ValueError("Неверная длина данных ответа")
|
||||
return tuple(int.from_bytes(body[offset:offset + 2], "big")
|
||||
for offset in range(0, len(body), 2))
|
||||
|
||||
|
||||
def bits_lsb_first(value: int) -> tuple[bool, ...]:
|
||||
@@ -104,4 +85,14 @@ def _range(name: str, value: int, maximum: int) -> None:
|
||||
raise ValueError(f"{name} вне диапазона 0..{maximum}")
|
||||
|
||||
|
||||
PROJECT_COMMANDS = _core().periph28335_catalog()
|
||||
PROJECT_COMMANDS = {
|
||||
"По умолчанию": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Light", "Raw", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||
"23470": ("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "-", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||
"23550": ("Test", "Def", "Save", "Load", "Calibr", "Read", "Secret", "Send", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||
"23550.2": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Send", "Raw", "Beep", "", "", "", "", "Log", "Reset", "Nothing at all"),
|
||||
"ICE 22220.1-3": ("Test", "Zero", "Save", "Def", "Calibr", "Read", "ExtLamp", "ExtLite", "-", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||
"ICE 22220.4-5": ("Test", "Def", "Save", "Load", "Raw", "Read", "ExtLamp", "ExtLite", "No log", "-", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||
"Бальзам 161": ("Test", "Zero", "Save", "Def", "Calibr", "Clbr 400", "Stop", "Start", "Init", "Secret", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||
"Бальзам 162": ("Test", "Def", "Save", "Load", "Calibr", "Secret", "Stop", "Start", "Init", "Tune", "-", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||
"Бальзам 163": ("Test", "Def", "Save", "Load", "Calibr", "Calcul", "Stop", "Start", "Init", "Tune", "Secret", "-", "-", "-", "-", "Reset", "Nothing at all"),
|
||||
}
|
||||
|
||||
@@ -67,10 +67,6 @@ class PlotMath:
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def limits(self, left: float, right: float, bottom: float, top: float) -> "Bounds":
|
||||
"""Validate absolute axis limits in the shared core."""
|
||||
return Bounds(*self.call(8, left, right, bottom, top))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Viewport:
|
||||
@@ -96,9 +92,6 @@ class Bounds:
|
||||
bottom: float
|
||||
top: float
|
||||
|
||||
def validated(self, core: PlotMath) -> "Bounds":
|
||||
return core.limits(self.left, self.right, self.bottom, self.top)
|
||||
|
||||
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]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user