Compare commits
19 Commits
feature/sh
...
feature/pl
| Author | SHA1 | Date | |
|---|---|---|---|
| 27ef6fcfdf | |||
| 235d5d81a2 | |||
| 59f847e900 | |||
| 286e454464 | |||
| e691dfc337 | |||
| f5f15f6a04 | |||
| be066a57eb | |||
| b1f7b965f4 | |||
| 78d3f6690b | |||
| 6a82b309cc | |||
| 2316a5a26a | |||
| d44d57a7aa | |||
| b972b6f33c | |||
| 454baeed98 | |||
| 01ffc0e496 | |||
| 3c4ac9963d | |||
| d9eb7dd9ad | |||
| f6787163dc | |||
| 5f8fc07f2a |
@@ -13,6 +13,7 @@
|
||||
templates/
|
||||
c/ библиотеки на C99: заголовок, реализация, README, где есть — порт и тесты
|
||||
python/ модули на чистом Python 3.9+, только stdlib
|
||||
tools/ общие инструменты сборки и выпуска прошивок
|
||||
```
|
||||
|
||||
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
|
||||
@@ -48,6 +49,12 @@ 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
Normal file
80
RULES.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Правила общего кроссплатформенного кода
|
||||
|
||||
Этот репозиторий — единственный источник общих алгоритмов для `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).
|
||||
@@ -17,6 +17,10 @@ set(SETPROTOCOL_V2_SOURCES
|
||||
|
||||
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
|
||||
set(SETPROTOCOL_LEGACY_SOURCES
|
||||
src/balsam_can.c
|
||||
src/set_crc.c
|
||||
src/periph28335.c
|
||||
src/tms2812.c
|
||||
src/gui_catalog.c
|
||||
src/gui_frame.c
|
||||
src/pcan_abi.c
|
||||
@@ -94,6 +98,15 @@ if(SETP_BUILD_TESTS)
|
||||
add_executable(test_abi tests/test_abi.c)
|
||||
target_link_libraries(test_abi PRIVATE setprotocol_static)
|
||||
add_test(NAME stable_abi COMMAND test_abi)
|
||||
add_executable(test_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_tms2812 tests/test_tms2812.c)
|
||||
target_link_libraries(test_tms2812 PRIVATE setprotocol_static)
|
||||
add_test(NAME shared_tms2812 COMMAND test_tms2812)
|
||||
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,6 +21,12 @@ 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,7 +3,8 @@
|
||||
Численная логика находится в `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 | Адаптер приложения |
|
||||
|---|---|
|
||||
@@ -13,6 +14,7 @@
|
||||
| Перевод значения в долю экрана и обратно, инверсия Y | Canvas/QPainter и оформление шкал |
|
||||
| Перемещение маркера от начальной координаты, ограничение видимой областью | Захват линии пальцем/мышью, редактор положения |
|
||||
| Разность B−A, D−C и множитель единиц, шаги шкалы 1/2/5 | Подписи, цвета, миллисекунды/герцы/единицы сигнала |
|
||||
| Проверка абсолютных границ X/Y для фиксации осей | Диалог ввода и хранение отдельных границ времени/FFT |
|
||||
| Модели маркеров и их размещение в Kotlin/Python-портах | Жизненный цикл экрана, очистка и выбор источника |
|
||||
|
||||
Модель маркеров: A/B — координаты X и вертикальные линии во всю высоту поля;
|
||||
@@ -59,8 +61,9 @@ SETGUI: `ui/plot_interaction.py` адаптирует общий модуль к
|
||||
используются исходные метки приёма. Дискретные дорожки имеют общую X-шкалу,
|
||||
а маркеры уровня Y относятся к аналоговому полю.
|
||||
|
||||
Расчёт FFT не является частью этого модуля. Android вычисляет спектр через
|
||||
`set_spectrum.c`; вкладка спектра SETGUI получает готовые уровни в дБмВ от
|
||||
Расчёт FFT не является частью этого модуля. Android и SETGUI вычисляют спектр через
|
||||
`set_spectrum.c`; там же находится общий поиск доминирующего узкополосного пика,
|
||||
а трёхсекундный таймер его отображения остаётся состоянием GUI. Вкладка спектра 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`; Python 3.9+, stdlib, без Qt |
|
||||
| `python/protocan/trends.py` | Модель, JSON, история и `NativeTrends` (word/CAN/GAS request/ack/data); Python 3.9+, stdlib, без Qt |
|
||||
| `tests/fixtures/trends-v1.json` | Один образец для тестов обоих GUI и обмена настройками |
|
||||
|
||||
## Формат файла
|
||||
@@ -44,6 +44,8 @@ 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 должен подтвердить весь
|
||||
список: при частичном принятии нельзя определить пропущенные адреса, поэтому
|
||||
строить график по смещённым индексам запрещено. При паузе порт приложения
|
||||
|
||||
61
c/set-protocol/include/balsam_can.h
Normal file
61
c/set-protocol/include/balsam_can.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @file balsam_can.h
|
||||
* @brief Legacy Balsam 167 extended-CAN register frames.
|
||||
*
|
||||
* The wire layout is taken from Balsam_167_periph/Source/Internal/ecan.c:
|
||||
* one big-endian address/mask word followed by three big-endian register words.
|
||||
*/
|
||||
#ifndef BALSAM_CAN_H
|
||||
#define BALSAM_CAN_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define BALSAM_CAN_BASE_ID 0x00BA0000UL
|
||||
#define BALSAM_CAN_NODE_COUNT 13U
|
||||
#define BALSAM_CAN_DATA_OFFSET 0x10U
|
||||
#define BALSAM_CAN_DLC 8U
|
||||
#define BALSAM_CAN_REGISTER_COUNT 3U
|
||||
|
||||
typedef enum {
|
||||
BALSAM_CAN_TO_NODE = 0,
|
||||
BALSAM_CAN_FROM_NODE = 1
|
||||
} balsam_can_direction_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t device;
|
||||
uint8_t direction;
|
||||
uint8_t present_mask;
|
||||
uint16_t start_address;
|
||||
uint16_t values[BALSAM_CAN_REGISTER_COUNT];
|
||||
} balsam_can_frame_t;
|
||||
|
||||
/** Return non-zero for the command/data/terminal IDs used by Balsam 167. */
|
||||
int balsam_can_is_id(uint32_t can_id);
|
||||
|
||||
/**
|
||||
* Decode an 8-byte Balsam frame. Returns 1 on success, 0 for another CAN ID,
|
||||
* -1 for invalid arguments and -2 for a Balsam ID with a non-8-byte payload.
|
||||
*/
|
||||
int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||
balsam_can_frame_t *output);
|
||||
|
||||
/** Human-readable Russian name of a Balsam device. */
|
||||
const char *balsam_can_device_name(uint8_t device);
|
||||
|
||||
/**
|
||||
* Format the register name from the Balsam 167 data table into output.
|
||||
* Returns the required length (excluding NUL); an empty string means unknown.
|
||||
*/
|
||||
size_t balsam_can_register_name(uint8_t device, uint16_t address,
|
||||
char *output, size_t output_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BALSAM_CAN_H */
|
||||
@@ -45,6 +45,14 @@ typedef struct {
|
||||
uint8_t payload[PCAN_ABI_GUI_PAYLOAD_MAX];
|
||||
} pcan_abi_gui_frame_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t device;
|
||||
uint8_t direction;
|
||||
uint8_t present_mask;
|
||||
uint16_t start_address;
|
||||
uint16_t values[3];
|
||||
} pcan_abi_balsam_frame_t;
|
||||
|
||||
PCAN_ABI_API uint32_t pcan_abi_version(void);
|
||||
|
||||
PCAN_ABI_API uint32_t pcan_abi_id_pack(uint8_t priority, uint8_t route,
|
||||
@@ -58,6 +66,56 @@ PCAN_ABI_API void pcan_abi_id_unpack(uint32_t raw, uint8_t *priority,
|
||||
|
||||
PCAN_ABI_API uint16_t pcan_abi_crc16(const uint8_t *data, size_t size);
|
||||
|
||||
PCAN_ABI_API int pcan_abi_balsam_decode(uint32_t can_id,
|
||||
const uint8_t *data, size_t size,
|
||||
pcan_abi_balsam_frame_t *output);
|
||||
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);
|
||||
|
||||
/* PM67/TMS320F2812 legacy memory upload. */
|
||||
PCAN_ABI_API uint16_t pcan_abi_tms2812_crc16(
|
||||
const uint8_t *data, size_t size);
|
||||
PCAN_ABI_API size_t pcan_abi_tms2812_build_upload(
|
||||
uint8_t controller, uint32_t word_address, uint32_t byte_count,
|
||||
uint8_t *output, size_t output_size);
|
||||
PCAN_ABI_API size_t pcan_abi_tms2812_expected_upload_size(
|
||||
uint32_t byte_count);
|
||||
PCAN_ABI_API int pcan_abi_tms2812_validate_upload(
|
||||
const uint8_t *data, size_t size, uint8_t controller,
|
||||
uint32_t byte_count);
|
||||
PCAN_ABI_API int pcan_abi_tms2812_decode_upload(
|
||||
const uint8_t *data, size_t size, uint8_t controller,
|
||||
uint32_t byte_count, uint8_t *output, size_t output_size);
|
||||
|
||||
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,
|
||||
|
||||
68
c/set-protocol/include/periph28335.h
Normal file
68
c/set-protocol/include/periph28335.h
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @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 */
|
||||
19
c/set-protocol/include/set_crc.h
Normal file
19
c/set-protocol/include/set_crc.h
Normal file
@@ -0,0 +1,19 @@
|
||||
/** @file set_crc.h @brief Shared checksums used by legacy SET controllers. */
|
||||
#ifndef SET_CRC_H
|
||||
#define SET_CRC_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** CRC-16/Modbus: reflected polynomial 0xA001, initial value 0xFFFF. */
|
||||
uint16_t set_crc16_modbus(const uint8_t *data, size_t size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SET_CRC_H */
|
||||
@@ -16,10 +16,12 @@ enum set_plot_operation {
|
||||
SET_PLOT_VALUE = 3, /* fraction,low,high,inverted -> value (not clamped) */
|
||||
SET_PLOT_DRAG = 4, /* initial,deltaPixels,length,low,high,inverted -> clamped value */
|
||||
SET_PLOT_TICK_STEP = 5, /* range,lengthPixels -> nice step */
|
||||
SET_PLOT_DELTA = 6 /* A,B,multiplier -> (B-A)*multiplier */
|
||||
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 */
|
||||
};
|
||||
|
||||
/** Version of this plot ABI, independently of the transport ABI. */
|
||||
/** Version of this additive 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,6 +28,15 @@ 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,6 +48,13 @@ 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
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
#ifndef SETPROTOCOL_H
|
||||
#define SETPROTOCOL_H
|
||||
|
||||
#include "balsam_can.h"
|
||||
#include "periph28335.h"
|
||||
#include "tms2812.h"
|
||||
|
||||
/* Основной SET protocol v2. */
|
||||
#include "set_protocol.h"
|
||||
#include "set_can.h"
|
||||
|
||||
49
c/set-protocol/include/tms2812.h
Normal file
49
c/set-protocol/include/tms2812.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @file tms2812.h
|
||||
* @brief Shared PM67/TMS320F2812 legacy upload protocol.
|
||||
*/
|
||||
#ifndef TMS2812_H
|
||||
#define TMS2812_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define TMS2812_CMD_UPLOAD 52U
|
||||
#define TMS2812_UPLOAD_REQUEST_SIZE 12U
|
||||
#define TMS2812_UPLOAD_RESPONSE_OVERHEAD 8U
|
||||
|
||||
typedef enum {
|
||||
TMS2812_OK = 0,
|
||||
TMS2812_ERROR_ARGUMENT = -1,
|
||||
TMS2812_ERROR_RANGE = -2,
|
||||
TMS2812_ERROR_LENGTH = -3,
|
||||
TMS2812_ERROR_CRC = -4,
|
||||
TMS2812_ERROR_HEADER = -5,
|
||||
TMS2812_ERROR_CAPACITY = -6
|
||||
} tms2812_status_t;
|
||||
|
||||
uint16_t tms2812_crc16(const uint8_t *data, size_t size);
|
||||
|
||||
size_t tms2812_build_upload_request(
|
||||
uint8_t controller, uint32_t word_address, uint32_t byte_count,
|
||||
uint8_t *output, size_t output_size);
|
||||
|
||||
size_t tms2812_expected_upload_response_size(uint32_t byte_count);
|
||||
|
||||
int tms2812_validate_upload_response(
|
||||
const uint8_t *data, size_t size, uint8_t controller,
|
||||
uint32_t byte_count);
|
||||
|
||||
int tms2812_decode_upload_response(
|
||||
const uint8_t *data, size_t size, uint8_t controller,
|
||||
uint32_t byte_count, uint8_t *output, size_t output_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* TMS2812_H */
|
||||
@@ -12,6 +12,10 @@ LOCAL_SRC_FILES := \
|
||||
set_plot_jni.c \
|
||||
../../src/set_trends.c \
|
||||
../../src/set_spectrum.c \
|
||||
../../src/balsam_can.c \
|
||||
../../src/set_crc.c \
|
||||
../../src/periph28335.c \
|
||||
../../src/tms2812.c \
|
||||
../../src/gui_catalog.c \
|
||||
../../src/gui_frame.c \
|
||||
../../src/pcan_abi.c \
|
||||
|
||||
@@ -14,6 +14,11 @@ workspace, up to 2×16384 doubles). `trends/SpectrumAnalyzer` maps timestamps an
|
||||
errors but does not duplicate FFT/filter math. Run it off the UI thread.
|
||||
`trends/PlotViewport` is a toolkit-free normalized zoom/pan model.
|
||||
|
||||
`legacycan/LegacyCanTerminal.kt` contains the two historical CAN_terminal wire
|
||||
formats, the complete Projects.ini node/command catalog, and shared codecs for
|
||||
register writes and command frames. Android UI code must use this module instead
|
||||
of reproducing the Delphi byte rotation or CAN-ID routing rules.
|
||||
|
||||
`kotlin/ru/setcorp/setprotocol/update/FirmwareCatalog.kt` is a UI-independent
|
||||
firmware release client. It reads the optional `firmware.releases` array from
|
||||
the shared `update.json`, accepts only HTTPS assets, limits their size and
|
||||
|
||||
@@ -175,7 +175,9 @@ object CanBridgeProtocol {
|
||||
stats = Stats(
|
||||
frames = values[0],
|
||||
crcErrors = values[1],
|
||||
resyncBytes = values[2] + values[3],
|
||||
// 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],
|
||||
sequenceLost = values[4],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ package ru.setcorp.setprotocol
|
||||
object NativeSetProtocol {
|
||||
val available: Boolean by lazy {
|
||||
runCatching {
|
||||
System.loadLibrary("setprotocol")
|
||||
val hostLibrary = System.getProperty("setprotocol.library")
|
||||
?: System.getProperty("setplot.library")
|
||||
if (hostLibrary != null) System.load(hostLibrary)
|
||||
else System.loadLibrary("setprotocol")
|
||||
nativeAbiVersion() == 1
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
@@ -13,12 +16,18 @@ 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,
|
||||
@@ -29,6 +38,25 @@ object NativeSetProtocol {
|
||||
): Long
|
||||
external fun nativeUnpackId(raw: Long): IntArray?
|
||||
external fun nativeCrc16(input: ByteArray): Int
|
||||
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 nativeTms2812Crc16(input: ByteArray): Int
|
||||
external fun nativeTms2812BuildUpload(controller: Int, wordAddress: Long, byteCount: Long): ByteArray?
|
||||
external fun nativeTms2812ExpectedUploadSize(byteCount: Long): Int
|
||||
external fun nativeTms2812ValidateUpload(input: ByteArray, controller: Int, byteCount: Int): Boolean
|
||||
external fun nativeTms2812DecodeUpload(input: ByteArray, controller: Int, byteCount: Int): ByteArray?
|
||||
external fun nativeEncodeFrame(
|
||||
sequence: Int,
|
||||
flags: Int,
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package ru.setcorp.setprotocol.balsam
|
||||
|
||||
import ru.setcorp.setprotocol.NativeSetProtocol
|
||||
|
||||
data class BalsamRegister(val address: Int, val value: Int, val name: String) {
|
||||
val displayName: String get() = name.ifBlank { "R%04X".format(address) }
|
||||
val signedValue: Int get() = if (value < 0x8000) value else value - 0x10000
|
||||
}
|
||||
|
||||
data class BalsamFrame(
|
||||
val canId: Long,
|
||||
val device: Int,
|
||||
val deviceName: String,
|
||||
val fromDevice: Boolean,
|
||||
val startAddress: Int,
|
||||
val presentMask: Int,
|
||||
val registers: List<BalsamRegister>,
|
||||
) {
|
||||
fun summary(): String {
|
||||
val direction = if (fromDevice) "данные" else "команда"
|
||||
val values = registers.joinToString { "${it.displayName}=0x%04X (%d)".format(it.value, it.signedValue) }
|
||||
.ifEmpty { "нет отмеченных регистров" }
|
||||
return "BALZAM · $deviceName · $direction · $values"
|
||||
}
|
||||
}
|
||||
|
||||
/** Shared parser for Balsam_167_periph eCAN frames. */
|
||||
object BalsamCanProtocol {
|
||||
const val BASE_ID = 0x00BA_0000L
|
||||
const val TERMINAL_REQUEST_ID = 0x00BA_001CL
|
||||
const val TERMINAL_RESPONSE_ID = 0x00BA_000CL
|
||||
const val PULT_REQUEST_ID = 0x0074_5019L
|
||||
const val PULT_RESPONSE_ID = 0x0074_5009L
|
||||
|
||||
fun isLegacyId(canId: Long): Boolean {
|
||||
val relative = (canId and 0x1FFF_FFFFL) - BASE_ID
|
||||
return relative in 0L..12L || relative in 0x10L..0x1CL ||
|
||||
canId == PULT_REQUEST_ID || canId == PULT_RESPONSE_ID
|
||||
}
|
||||
|
||||
fun decode(canId: Long, data: ByteArray): BalsamFrame? {
|
||||
if (!isRegisterId(canId) || data.size != 8) return null
|
||||
val native = if (NativeSetProtocol.available) {
|
||||
NativeSetProtocol.nativeBalsamDecode(canId, data)
|
||||
} else null
|
||||
val words = native ?: fallbackDecode(canId, data)
|
||||
val device = words[0]
|
||||
val mask = words[2]
|
||||
val start = words[3]
|
||||
val registers = (0..2).filter { mask and (4 shr it) != 0 }.map { index ->
|
||||
val address = start + index
|
||||
BalsamRegister(address, words[4 + index], registerName(device, address))
|
||||
}
|
||||
return BalsamFrame(
|
||||
canId and 0x1FFF_FFFFL,
|
||||
device,
|
||||
deviceName(device),
|
||||
words[1] == 1,
|
||||
start,
|
||||
mask,
|
||||
registers,
|
||||
)
|
||||
}
|
||||
|
||||
fun summary(canId: Long, data: ByteArray? = null): String =
|
||||
data?.let { decode(canId, it)?.summary() } ?: when (canId) {
|
||||
TERMINAL_REQUEST_ID -> "BALZAM legacy · запрос терминала"
|
||||
TERMINAL_RESPONSE_ID -> "BALZAM legacy · ответ терминалу"
|
||||
PULT_REQUEST_ID -> "BALZAM legacy · данные пульта"
|
||||
PULT_RESPONSE_ID -> "BALZAM legacy · команда пульту"
|
||||
in (BASE_ID + 0x10L)..(BASE_ID + 0x1BL) ->
|
||||
"BALZAM legacy · данные · ${deviceName((canId - BASE_ID - 0x0FL).toInt())}"
|
||||
in BASE_ID..(BASE_ID + 0x0BL) ->
|
||||
"BALZAM legacy · команда · ${deviceName((canId - BASE_ID + 1L).toInt())}"
|
||||
else -> "BALZAM legacy · неизвестный ID"
|
||||
}
|
||||
|
||||
private fun isRegisterId(canId: Long): Boolean {
|
||||
val relative = (canId and 0x1FFF_FFFFL) - BASE_ID
|
||||
return relative in 0L..12L || relative in 0x10L..0x1CL
|
||||
}
|
||||
|
||||
private fun fallbackDecode(canId: Long, data: ByteArray): IntArray {
|
||||
val relative = (canId and 0x1FFF_FFFFL) - BASE_ID
|
||||
val header = u16be(data, 0)
|
||||
return intArrayOf(
|
||||
((relative and 0x0F) + 1).toInt(),
|
||||
if (relative >= 0x10) 1 else 0,
|
||||
(header ushr 13) and 7,
|
||||
header and 0x1FFF,
|
||||
u16be(data, 2), u16be(data, 4), u16be(data, 6),
|
||||
)
|
||||
}
|
||||
|
||||
private fun u16be(data: ByteArray, offset: Int): Int =
|
||||
((data[offset].toInt() and 0xFF) shl 8) or (data[offset + 1].toInt() and 0xFF)
|
||||
|
||||
private fun deviceName(device: Int): String = if (NativeSetProtocol.available) {
|
||||
NativeSetProtocol.nativeBalsamDeviceName(device)
|
||||
} else listOf(
|
||||
"Трансформатор 1", "Трансформатор 2", "Силовой блок 1", "Силовой блок 2",
|
||||
"УМП 1", "УМП 2", "Двигатель", "ВЭП", "Задатчик", "Узел 10", "Узел 11",
|
||||
"Узел 12", "Терминал",
|
||||
).getOrElse(device - 1) { "Неизвестный узел" }
|
||||
|
||||
private fun registerName(device: Int, address: Int): String =
|
||||
if (NativeSetProtocol.available) NativeSetProtocol.nativeBalsamRegisterName(device, address)
|
||||
else when {
|
||||
device in 1..2 && address in 0x18..0x2B -> "Показания T° ${address - 0x17}"
|
||||
device in 3..4 && address in 0x18..0x27 -> "Показания T° ${address - 0x17}"
|
||||
device == 7 && address in 0x18..0x1F -> "Показания T° ${address - 0x17}"
|
||||
address == 0x17 -> "Состояние джамперов"
|
||||
address == 0x7F -> "Команды"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package ru.setcorp.setprotocol.legacycan
|
||||
|
||||
/** Wire formats implemented by the historical CAN_terminal application. */
|
||||
enum class LegacyCanFormat {
|
||||
/** Address and a three-bit presence mask are carried in DATA[4..5]. */
|
||||
ROTATING_THREE_WORDS,
|
||||
|
||||
/** Register address is carried in CAN ID[6:0], followed by up to four words. */
|
||||
ADDRESS_IN_IDENTIFIER,
|
||||
}
|
||||
|
||||
enum class LegacyCanSource { TO_DEVICE, FROM_DEVICE }
|
||||
|
||||
data class LegacyCanPacket(
|
||||
val address: Int,
|
||||
val mask: Int,
|
||||
val values: List<Int>,
|
||||
val source: LegacyCanSource,
|
||||
) {
|
||||
val presentValues: List<Pair<Int, Int>>
|
||||
get() = values.mapIndexedNotNull { index, value ->
|
||||
if (formatUses(index)) address + index to value else null
|
||||
}
|
||||
|
||||
private fun formatUses(index: Int): Boolean = mask == 0xFF || mask and (4 shr index) != 0
|
||||
}
|
||||
|
||||
data class LegacyCanWireFrame(val canId: Long, val data: ByteArray)
|
||||
|
||||
data class LegacyCanRegisterValue(
|
||||
val address: Int,
|
||||
val value: Int = 0,
|
||||
val source: LegacyCanSource? = null,
|
||||
val revision: Long = 0,
|
||||
)
|
||||
|
||||
data class LegacyCanNode(
|
||||
val index: Int,
|
||||
val rsAddress: Int,
|
||||
val canAddress: Int,
|
||||
val rxId: Long,
|
||||
val txId: Long,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
data class LegacyCanProject(
|
||||
val name: String,
|
||||
val format: LegacyCanFormat,
|
||||
val baseId: Long,
|
||||
val idOffset: Long,
|
||||
val nodes: List<LegacyCanNode>,
|
||||
val commandNames: List<String>,
|
||||
) {
|
||||
fun nodeFor(canId: Long): LegacyCanNode? {
|
||||
val normalized = LegacyCanTerminalProtocol.routingId(format, canId)
|
||||
return nodes.firstOrNull { it.rxId == normalized || it.txId == normalized }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared, UI-independent codec for the two protocols found in CAN_terminal.pas.
|
||||
* Values are unsigned 16-bit words; callers can interpret them as signed with
|
||||
* [signedWord].
|
||||
*/
|
||||
object LegacyCanTerminalProtocol {
|
||||
fun emptyRegisterBank(): List<LegacyCanRegisterValue> =
|
||||
List(128) { LegacyCanRegisterValue(it) }
|
||||
|
||||
fun applyPacket(
|
||||
bank: List<LegacyCanRegisterValue>,
|
||||
packet: LegacyCanPacket,
|
||||
revision: Long,
|
||||
): List<LegacyCanRegisterValue> {
|
||||
require(bank.size == 128) { "Банк должен содержать 128 регистров" }
|
||||
val updates = packet.presentValues.toMap()
|
||||
return bank.map { current ->
|
||||
updates[current.address]?.let { value ->
|
||||
current.copy(value = value, source = packet.source, revision = revision)
|
||||
} ?: current
|
||||
}
|
||||
}
|
||||
|
||||
fun routingId(format: LegacyCanFormat, canId: Long): Long = when (format) {
|
||||
LegacyCanFormat.ROTATING_THREE_WORDS -> canId and 0x1FFF_FFFFL
|
||||
LegacyCanFormat.ADDRESS_IN_IDENTIFIER -> canId and 0x1FF0_0000L
|
||||
}
|
||||
|
||||
fun decode(
|
||||
format: LegacyCanFormat,
|
||||
node: LegacyCanNode,
|
||||
canId: Long,
|
||||
data: ByteArray,
|
||||
): LegacyCanPacket? {
|
||||
val route = routingId(format, canId)
|
||||
val source = when (route) {
|
||||
node.txId -> LegacyCanSource.TO_DEVICE
|
||||
node.rxId -> LegacyCanSource.FROM_DEVICE
|
||||
else -> return null
|
||||
}
|
||||
return when (format) {
|
||||
LegacyCanFormat.ROTATING_THREE_WORDS -> {
|
||||
if (data.size != 8) return null
|
||||
val mask = (u8(data[4]) ushr 5) and 7
|
||||
val address = ((u8(data[4]) and 0x1F) shl 8) or u8(data[5])
|
||||
LegacyCanPacket(address, mask, listOf(u16be(data, 6), u16be(data, 0), u16be(data, 2)), source)
|
||||
}
|
||||
LegacyCanFormat.ADDRESS_IN_IDENTIFIER -> {
|
||||
if (data.isEmpty() || data.size > 8 || data.size % 2 != 0) return null
|
||||
LegacyCanPacket(
|
||||
address = (canId and 0x7F).toInt(),
|
||||
mask = 0xFF,
|
||||
values = data.indices.step(2).map { u16be(data, it) },
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun encodeWrite(
|
||||
format: LegacyCanFormat,
|
||||
canId: Long,
|
||||
address: Int,
|
||||
values: List<Int>,
|
||||
): LegacyCanWireFrame {
|
||||
require(address in 0..127) { "Адрес регистра должен быть в диапазоне 0..127" }
|
||||
val maximum = if (format == LegacyCanFormat.ADDRESS_IN_IDENTIFIER) 4 else 3
|
||||
require(values.size in 1..maximum) { "Нужно от 1 до $maximum слов данных" }
|
||||
values.forEach { require(it in 0..0xFFFF) { "Значение должно быть в диапазоне 0..65535" } }
|
||||
return when (format) {
|
||||
LegacyCanFormat.ADDRESS_IN_IDENTIFIER -> LegacyCanWireFrame(
|
||||
(canId and 0x1FF0_0000L) + address,
|
||||
values.flatMap { listOf((it ushr 8).toByte(), it.toByte()) }.toByteArray(),
|
||||
)
|
||||
LegacyCanFormat.ROTATING_THREE_WORDS -> {
|
||||
val padded = values + List(3 - values.size) { 0 }
|
||||
val mask = when (values.size) { 1 -> 4; 2 -> 6; else -> 7 }
|
||||
val data = byteArrayOf(
|
||||
(padded[1] ushr 8).toByte(), padded[1].toByte(),
|
||||
(padded[2] ushr 8).toByte(), padded[2].toByte(),
|
||||
((mask shl 5) or (address ushr 8)).toByte(), address.toByte(),
|
||||
(padded[0] ushr 8).toByte(), padded[0].toByte(),
|
||||
)
|
||||
LegacyCanWireFrame(canId and 0x1FFF_FFFFL, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun encodeCommand(project: LegacyCanProject, node: LegacyCanNode, commandIndex: Int): LegacyCanWireFrame {
|
||||
require(commandIndex in 0..16) { "Номер команды должен быть в диапазоне 0..16" }
|
||||
val value = if (commandIndex < 16) 1 shl commandIndex else 0
|
||||
return encodeWrite(project.format, node.rxId, 127, listOf(value))
|
||||
}
|
||||
|
||||
fun signedWord(value: Int): Int = if (value < 0x8000) value else value - 0x10000
|
||||
|
||||
private fun u8(value: Byte): Int = value.toInt() and 0xFF
|
||||
private fun u16be(data: ByteArray, offset: Int): Int = (u8(data[offset]) shl 8) or u8(data[offset + 1])
|
||||
}
|
||||
|
||||
/** Project table migrated from CAN_terminal/Projects.ini (Windows-1251). */
|
||||
object LegacyCanProjects {
|
||||
private val defaultCommands = listOf(
|
||||
"Test", "Def", "Save", "Load", "Calibr", "Calcul", "Secret", "Light", "Raw",
|
||||
"-", "-", "-", "-", "-", "-", "Reset", "Nothing at all",
|
||||
)
|
||||
|
||||
private fun project(
|
||||
name: String,
|
||||
baseId: Long = 0,
|
||||
offset: Long = 0x10,
|
||||
format: LegacyCanFormat = LegacyCanFormat.ROTATING_THREE_WORDS,
|
||||
specs: List<List<Any>>,
|
||||
commands: List<String> = defaultCommands,
|
||||
): LegacyCanProject {
|
||||
val shift = if (format == LegacyCanFormat.ADDRESS_IN_IDENTIFIER) 20 else 0
|
||||
val actualOffset = if (format == LegacyCanFormat.ADDRESS_IN_IDENTIFIER) 1L shl 28 else offset
|
||||
val nodes = specs.map { spec ->
|
||||
val index = spec[0] as Int
|
||||
val canAddress = spec[1] as Int
|
||||
val rsAddress = spec[2] as Int
|
||||
val nodeName = spec[3] as String
|
||||
val routed = canAddress.toLong() shl shift
|
||||
LegacyCanNode(index, rsAddress, canAddress, baseId + routed, baseId + actualOffset + routed, nodeName)
|
||||
}
|
||||
return LegacyCanProject(name, format, baseId, actualOffset, nodes, commands)
|
||||
}
|
||||
|
||||
private fun s(index: Int, can: Int, rs: Int, name: String): List<Any> = listOf(index, can, rs, name)
|
||||
|
||||
val all: List<LegacyCanProject> = listOf(
|
||||
project("Буксир", 0x0031_8200, specs = listOf(
|
||||
s(0, 0, 1, "УКСС СБ"), s(1, 1, 2, "БКСС ГД"), s(2, 2, 3, "УКСВЭП"), s(3, 3, 4, "Задатчик"),
|
||||
)),
|
||||
project("СЭДБМ", 0x0105_1020, specs = listOf(
|
||||
s(0,0,0,"УКСС СК1 СБ1"),s(1,1,1,"УКСС СК2 СБ1"),s(2,2,2,"УКСС СК3 СБ1"),s(3,3,3,"УКСС СК4 СБ1"),
|
||||
s(4,4,4,"УКССВЭП СБ1"),s(5,5,5,"Задатчик СБ1"),s(6,6,6,"БТР ИТЭС"),s(8,0x20,8,"УКСС СК1 СБ2"),
|
||||
s(9,0x21,9,"УКСС СК2 СБ2"),s(10,0x22,10,"УКСС СК3 СБ2"),s(11,0x23,11,"УКСС СК4 СБ2"),
|
||||
s(12,0x24,12,"УКССВЭП СБ2"),s(13,0x25,13,"Задатчик СБ2"),s(14,0x26,14,"УКСС БОИН"),s(15,0x27,15,"УКСВЭП БОИН"),
|
||||
), commands = defaultCommands.toMutableList().also { it[7]="Raw"; it[8]="HiVolt" }),
|
||||
project("Ледокол", 0x001C_E020, -0x20, specs = listOf(
|
||||
s(0,0,1,"УКСС БВ1 ПЧ1"),s(8,1,2,"УКСС БВ1 ПЧ2"),s(1,2,3,"УКСС БВ1 ПЧ1"),s(9,3,4,"УКСС БВ2 ПЧ2"),
|
||||
s(2,4,5,"УКСС БИ1 ПЧ1"),s(10,5,6,"УКСС БИ1 ПЧ2"),s(3,6,7,"УКСС БИ2 ПЧ1"),s(11,7,8,"УКСС БИ2 ПЧ2"),
|
||||
s(4,8,9,"УКССВЭП1 ПЧ1"),s(12,9,10,"УКССВЭП1 ПЧ2"),s(5,10,11,"УКССВЭП2 ПЧ1"),s(13,11,12,"УКССВЭП2 ПЧ2"),
|
||||
), commands = defaultCommands.toMutableList().also { it[4]="Raw"; it[5]="Read"; it[6]="ExtLamp"; it[7]="ExtLite"; it[8]="No log" }),
|
||||
project("Бальзам", 0x00BA_0000, specs = listOf(
|
||||
s(0,0,1,"БКСС Тр1"),s(8,1,2,"БКСС Тр2"),s(1,2,3,"УКСС СБ1"),s(9,3,4,"УКСС СБ2"),
|
||||
s(2,4,5,"УКСС УМП1"),s(10,5,6,"УКСС УМП2"),s(3,6,7,"БКСС ГД"),s(4,7,9,"Задатчик"),s(5,8,11,"УКСС ВЭП"),
|
||||
), commands = defaultCommands.toMutableList().also { it[6]="Stop";it[7]="Start";it[8]="Init";it[9]="Tune";it[10]="Secret";it[11]="Light";it[12]="Raw" }),
|
||||
project("23550", 0x0023_5500, specs = listOf(
|
||||
s(0,0,1,"Задатчик"),s(1,1,2,"Выносной пульт"),s(2,2,3,"УКСВЭП"),s(3,3,4,"БКСС ГД"),
|
||||
), commands = defaultCommands.toMutableList().also { it[5]="Read";it[7]="Send";it[8]="-" }),
|
||||
project("23550.X", format = LegacyCanFormat.ADDRESS_IN_IDENTIFIER, specs = listOf(
|
||||
s(0,0,1,"Задатчик"),s(1,1,2,"Выносной пульт"),s(2,2,3,"УКСВЭП"),s(3,3,4,"БКСС ГД"),
|
||||
), commands = defaultCommands.toMutableList().also { it[5]="Read";it[7]="Send";it[8]="-" }),
|
||||
project("23550.2", format = LegacyCanFormat.ADDRESS_IN_IDENTIFIER, specs = listOf(
|
||||
s(0,0,1,"Задатчик"),s(1,1,2,"Выносной пульт"),s(2,2,3,"БКСС ГД"),s(3,4,4,"УКСС СИ СБ1"),
|
||||
s(4,6,6,"УКСС СВФ СБ1"),s(5,8,8,"УКСВЭП СБ1"),s(11,5,5,"УКСС СИ СБ2"),s(12,7,7,"УКСС СВФ СБ2"),
|
||||
s(13,9,9,"УКСВЭП СБ2"),s(16,0x1F,16,"BroadCast"),
|
||||
), commands = defaultCommands.toMutableList().also { it[5]="Calc";it[7]="Send" }),
|
||||
project("Янтарь", 0x0021_3000, specs = listOf(
|
||||
s(0,0,1,"УКСС БВ"),s(1,1,2,"УКСС БИ1"),s(2,2,3,"УКСС БИ2"),s(3,3,4,"БКСС ГД"),
|
||||
s(4,4,5,"УКСВЭП"),s(5,5,6,"Задатчик"),s(6,6,7,"Выносной пульт"),
|
||||
)),
|
||||
project(
|
||||
"23550 БСУ", 0x0CEB_0F1, -0x10,
|
||||
specs = listOf(s(0,0,0,"БСУ1"),s(1,1,1,"БСУ2")),
|
||||
commands = List(16) { "-" } + "Nothing at all",
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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,37 +10,28 @@ object GuiGasWatch {
|
||||
|
||||
fun request(periodMs: Int, addresses: List<Int>): ByteArray {
|
||||
require(periodMs in 0..65535 && addresses.size <= 64 && addresses.all { it in 0..65535 })
|
||||
if (NativeSetProtocol.available) return requireNotNull(NativeSetProtocol.nativeTrendWatchRequest(periodMs, addresses.toIntArray()))
|
||||
return ByteArray(4 + addresses.size * 2).also { output ->
|
||||
put16(output, 0, periodMs)
|
||||
put16(output, 2, addresses.size)
|
||||
addresses.forEachIndexed { index, address -> put16(output, 4 + index * 2, address) }
|
||||
}
|
||||
check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" }
|
||||
return requireNotNull(NativeSetProtocol.nativeTrendWatchRequest(periodMs, addresses.toIntArray()))
|
||||
}
|
||||
|
||||
fun validateAck(payload: ByteArray, periodMs: Int, count: Int) {
|
||||
require(payload.size == 4 && read16(payload, 0) == periodMs && read16(payload, 2) == count) {
|
||||
require(NativeSetProtocol.available && NativeSetProtocol.nativeTrendWatchAck(payload, periodMs, count)) {
|
||||
"Прибор принял не все адреса GAS. Проверьте карту регистров; отображение по неполной подписке невозможно"
|
||||
}
|
||||
}
|
||||
|
||||
fun values(payload: ByteArray, expectedCount: Int): List<Int> {
|
||||
val values = if (NativeSetProtocol.available) {
|
||||
requireNotNull(NativeSetProtocol.nativeTrendWatchValues(payload)) { "Повреждён GAS_WATCH_DATA" }.toList()
|
||||
} else {
|
||||
require(payload.size >= 6) { "GAS_WATCH_DATA короче заголовка" }
|
||||
val count = read16(payload, 4)
|
||||
require(count <= 64 && payload.size == 6 + count * 2) { "Неверная длина GAS_WATCH_DATA" }
|
||||
List(count) { read16(payload, 6 + it * 2) }
|
||||
}
|
||||
check(NativeSetProtocol.available) { "Общая библиотека SETProtocol недоступна" }
|
||||
val values = requireNotNull(NativeSetProtocol.nativeTrendWatchValues(payload)) { "Повреждён GAS_WATCH_DATA" }.toList()
|
||||
require(values.size == expectedCount) { "Число значений GAS не соответствует подписке" }
|
||||
return values
|
||||
}
|
||||
|
||||
private fun read16(data: ByteArray, offset: Int): Int =
|
||||
(data[offset].toInt() and 0xFF) or ((data[offset + 1].toInt() and 0xFF) shl 8)
|
||||
private fun put16(data: ByteArray, offset: Int, value: Int) {
|
||||
data[offset] = value.toByte()
|
||||
data[offset + 1] = (value ushr 8).toByte()
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +30,12 @@ data class PlotBounds(val left: Double, val right: Double, val bottom: Double, v
|
||||
|
||||
fun plotTickStep(range: Double, pixels: Double): Double = NativePlot.call(5, range, pixels)[0]
|
||||
fun plotDelta(a: Double, b: Double, multiplier: Double = 1.0): Double = NativePlot.call(6, a, b, multiplier)[0]
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package ru.setcorp.setprotocol.trends
|
||||
|
||||
/** Normalized top-left viewport; independent of pixels, units, toolkit and samples. */
|
||||
data class PlotViewport(val x: Double = 0.0, val y: Double = 0.0, val width: Double = 1.0, val height: Double = 1.0) {
|
||||
data class PlotViewport(val x: Double = 0.0, val y: Double = 0.0, val width: Double = 1.0, val height: Double = 1.0,
|
||||
val locked: Boolean = false) {
|
||||
fun transform(zoomX: Double = 1.0, zoomY: Double = 1.0, panX: Double = 0.0, panY: Double = 0.0,
|
||||
focusX: Double = 0.5, focusY: Double = 0.5): PlotViewport {
|
||||
if (locked) return this
|
||||
val result = NativePlot.call(0, x, y, width, height, zoomX, zoomY, panX, panY, focusX, focusY)
|
||||
return PlotViewport(result[0], result[1], result[2], result[3])
|
||||
return PlotViewport(result[0], result[1], result[2], result[3], locked)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ enum class TrendMarker(val title: String, val horizontal: Boolean) {
|
||||
data class TrendMarkers(
|
||||
val xEnabled: Boolean = true,
|
||||
val yEnabled: Boolean = false,
|
||||
val xPairs: Int = 1,
|
||||
val yPairs: Int = 1,
|
||||
val selected: TrendMarker = TrendMarker.A,
|
||||
val a: Double? = null,
|
||||
val b: Double? = null,
|
||||
@@ -20,6 +22,9 @@ data class TrendMarkers(
|
||||
val g: Double? = null,
|
||||
val h: Double? = null,
|
||||
) {
|
||||
init {
|
||||
require(xPairs in 1..2 && yPairs in 1..2) { "Количество пар маркеров должно быть от 1 до 2" }
|
||||
}
|
||||
fun value(marker: TrendMarker): Double? = when (marker) {
|
||||
TrendMarker.A -> a; TrendMarker.B -> b; TrendMarker.C -> c; TrendMarker.D -> d
|
||||
TrendMarker.E -> e; TrendMarker.F -> f; TrendMarker.G -> g; TrendMarker.H -> h
|
||||
@@ -49,5 +54,10 @@ data class TrendMarkers(
|
||||
}.sortedWith(compareBy<Pair<TrendMarker, Double>> { it.second }
|
||||
.thenBy { if (it.first == selected) 0 else 1 }).firstOrNull()?.first
|
||||
}
|
||||
fun enabled(marker: TrendMarker): Boolean = if (marker.horizontal) yEnabled else xEnabled
|
||||
fun enabled(marker: TrendMarker): Boolean = when (marker) {
|
||||
TrendMarker.A, TrendMarker.B -> xEnabled
|
||||
TrendMarker.C, TrendMarker.D -> xEnabled && xPairs >= 2
|
||||
TrendMarker.E, TrendMarker.F -> yEnabled
|
||||
TrendMarker.G, TrendMarker.H -> yEnabled && yPairs >= 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"), CAN_BRIDGE("CAN ↔ RS485"),
|
||||
SET_V1("SET GUI v1"), TMS2812("TMS320F2812 / BALZAM"), TMS28335("TMS320F28335 / ПМ35"), 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 -> listOf(TrendSource.TMS_MEMORY)
|
||||
TrendProfile.TMS2812, TrendProfile.TMS28335 -> 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,6 +34,8 @@ 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 {
|
||||
@@ -64,4 +66,12 @@ 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]) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ class FirmwareCatalogClient(private val userAgent: String) {
|
||||
require(fileName.matches(Regex("[A-Za-zА-Яа-яЁё0-9._ -]+\\.(bin|hex)", RegexOption.IGNORE_CASE))) {
|
||||
"Некорректное имя файла прошивки №${index + 1}"
|
||||
}
|
||||
require(transport in setOf("rs485", "can", "stm32")) { "Некорректный канал прошивки №${index + 1}" }
|
||||
require(transport in setOf("rs485", "can", "stm32", "tms")) { "Некорректный канал прошивки №${index + 1}" }
|
||||
val baseAddress = row.opt("baseAddress")?.takeUnless { it == JSONObject.NULL }?.toString()?.let(::parseAddress)
|
||||
add(FirmwareRelease(
|
||||
product, versionName, versionCode, imageUrl, fileName, sha256,
|
||||
|
||||
@@ -6,6 +6,320 @@
|
||||
#include "setprotocol_abi.h"
|
||||
#include "set_trends.h"
|
||||
#include "set_spectrum.h"
|
||||
#include "balsam_can.h"
|
||||
#include "periph28335.h"
|
||||
#include "tms2812.h"
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812Crc16(
|
||||
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 = tms2812_crc16((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_nativeTms2812BuildUpload(
|
||||
JNIEnv *env, jobject self, jint controller, jlong word_address,
|
||||
jlong byte_count)
|
||||
{
|
||||
(void)self;
|
||||
uint8_t output[TMS2812_UPLOAD_REQUEST_SIZE];
|
||||
if (controller < 0 || controller > 255 || word_address < 0 ||
|
||||
(uint64_t)word_address > UINT32_MAX || byte_count < 1 ||
|
||||
(uint64_t)byte_count > UINT32_MAX) return NULL;
|
||||
size_t written = tms2812_build_upload_request(
|
||||
(uint8_t)controller, (uint32_t)word_address, (uint32_t)byte_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 jint JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812ExpectedUploadSize(
|
||||
JNIEnv *env, jobject self, jlong byte_count)
|
||||
{
|
||||
(void)env; (void)self;
|
||||
if (byte_count < 1 || (uint64_t)byte_count > UINT32_MAX) return 0;
|
||||
size_t size = tms2812_expected_upload_response_size((uint32_t)byte_count);
|
||||
return size <= INT32_MAX ? (jint)size : 0;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812ValidateUpload(
|
||||
JNIEnv *env, jobject self, jbyteArray input, jint controller,
|
||||
jint byte_count)
|
||||
{
|
||||
(void)self;
|
||||
if (input == NULL || controller < 0 || controller > 255 || byte_count < 1)
|
||||
return JNI_FALSE;
|
||||
jsize size = (*env)->GetArrayLength(env, input);
|
||||
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
|
||||
if (data == NULL) return JNI_FALSE;
|
||||
int status = tms2812_validate_upload_response(
|
||||
(const uint8_t *)data, (size_t)size, (uint8_t)controller,
|
||||
(uint32_t)byte_count);
|
||||
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
|
||||
return status == TMS2812_OK ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812DecodeUpload(
|
||||
JNIEnv *env, jobject self, jbyteArray input, jint controller,
|
||||
jint byte_count)
|
||||
{
|
||||
(void)self;
|
||||
if (input == NULL || controller < 0 || controller > 255 || byte_count < 1)
|
||||
return NULL;
|
||||
jsize size = (*env)->GetArrayLength(env, input);
|
||||
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
|
||||
if (data == NULL) return NULL;
|
||||
uint8_t *output = (uint8_t *)malloc((size_t)byte_count);
|
||||
if (output == NULL) {
|
||||
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
|
||||
return NULL;
|
||||
}
|
||||
int status = tms2812_decode_upload_response(
|
||||
(const uint8_t *)data, (size_t)size, (uint8_t)controller,
|
||||
(uint32_t)byte_count, output, (size_t)byte_count);
|
||||
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
|
||||
if (status != TMS2812_OK) { free(output); return NULL; }
|
||||
jbyteArray result = (*env)->NewByteArray(env, byte_count);
|
||||
if (result != NULL) (*env)->SetByteArrayRegion(
|
||||
env, result, 0, byte_count, (const jbyte *)output);
|
||||
free(output);
|
||||
return result;
|
||||
}
|
||||
|
||||
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(
|
||||
JNIEnv *env, jobject self, jlong can_id, jbyteArray input)
|
||||
{
|
||||
(void)self;
|
||||
balsam_can_frame_t frame;
|
||||
jsize size;
|
||||
jbyte data[BALSAM_CAN_DLC];
|
||||
jint values[7];
|
||||
if (input == NULL) return NULL;
|
||||
size = (*env)->GetArrayLength(env, input);
|
||||
if (size != (jsize)BALSAM_CAN_DLC) return NULL;
|
||||
(*env)->GetByteArrayRegion(env, input, 0, size, data);
|
||||
if (balsam_can_decode((uint32_t)can_id, (const uint8_t *)data,
|
||||
(size_t)size, &frame) != 1) return NULL;
|
||||
values[0] = frame.device;
|
||||
values[1] = frame.direction;
|
||||
values[2] = frame.present_mask;
|
||||
values[3] = frame.start_address;
|
||||
values[4] = frame.values[0];
|
||||
values[5] = frame.values[1];
|
||||
values[6] = frame.values[2];
|
||||
jintArray result = (*env)->NewIntArray(env, 7);
|
||||
if (result != NULL) (*env)->SetIntArrayRegion(env, result, 0, 7, values);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeBalsamDeviceName(
|
||||
JNIEnv *env, jobject self, jint device)
|
||||
{
|
||||
(void)self;
|
||||
return (*env)->NewStringUTF(env, balsam_can_device_name((uint8_t)device));
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeBalsamRegisterName(
|
||||
JNIEnv *env, jobject self, jint device, jint address)
|
||||
{
|
||||
(void)self;
|
||||
char name[128];
|
||||
balsam_can_register_name((uint8_t)device, (uint16_t)address,
|
||||
name, sizeof name);
|
||||
return (*env)->NewStringUTF(env, name);
|
||||
}
|
||||
|
||||
JNIEXPORT jdoubleArray JNICALL
|
||||
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeSpectrum(
|
||||
@@ -53,6 +367,30 @@ 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)
|
||||
@@ -75,6 +413,20 @@ 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)
|
||||
@@ -94,6 +446,28 @@ 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;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package ru.setcorp.setprotocol.balsam
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class BalsamCanProtocolTest {
|
||||
@Test
|
||||
fun decodesThreeNamedSensorRegisters() {
|
||||
val frame = requireNotNull(BalsamCanProtocol.decode(
|
||||
0x00BA_0010L,
|
||||
byteArrayOf(0xE0.toByte(), 0x18, 0x00, 0x29, 0xFF.toByte(), 0xFE.toByte(), 0x12, 0x34),
|
||||
))
|
||||
assertEquals(1, frame.device)
|
||||
assertEquals(0x18, frame.startAddress)
|
||||
assertEquals(listOf(41, 0xFFFE, 0x1234), frame.registers.map { it.value })
|
||||
assertEquals("Показания T° 1", frame.registers.first().displayName)
|
||||
assertTrue(frame.summary().contains("Трансформатор 1"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package ru.setcorp.setprotocol.legacycan
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class LegacyCanTerminalTest {
|
||||
@Test fun rotatingFormatRoundTripsAllWordCounts() {
|
||||
val project = LegacyCanProjects.all.first { it.name == "Бальзам" }
|
||||
val node = project.nodes.first()
|
||||
for (values in listOf(listOf(0x1234), listOf(0x1234, 0xABCD), listOf(0x1234, 0xABCD, 0x8001))) {
|
||||
val wire = LegacyCanTerminalProtocol.encodeWrite(project.format, node.rxId, 0x18, values)
|
||||
val decoded = LegacyCanTerminalProtocol.decode(project.format, node, wire.canId, wire.data)!!
|
||||
assertEquals(0x18, decoded.address)
|
||||
assertEquals(values, decoded.presentValues.map { it.second })
|
||||
assertEquals(LegacyCanSource.FROM_DEVICE, decoded.source)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun addressInIdentifierRoundTripsFourWords() {
|
||||
val project = LegacyCanProjects.all.first { it.name == "23550.2" }
|
||||
val node = project.nodes.first { it.name == "БКСС ГД" }
|
||||
val values = listOf(1, 2, 0x7FFF, 0xFFFF)
|
||||
val wire = LegacyCanTerminalProtocol.encodeWrite(project.format, node.txId, 0x7F, values)
|
||||
val decoded = LegacyCanTerminalProtocol.decode(project.format, node, wire.canId, wire.data)!!
|
||||
assertEquals(values, decoded.values)
|
||||
assertEquals(LegacyCanSource.TO_DEVICE, decoded.source)
|
||||
assertEquals(-1, LegacyCanTerminalProtocol.signedWord(decoded.values.last()))
|
||||
}
|
||||
|
||||
@Test fun unrelatedIdDoesNotDecode() {
|
||||
val project = LegacyCanProjects.all.first()
|
||||
assertNull(LegacyCanTerminalProtocol.decode(project.format, project.nodes.first(), 0x123, ByteArray(8)))
|
||||
}
|
||||
|
||||
@Test fun packetReducerUpdatesOnlyPresentRegisters() {
|
||||
val packet = LegacyCanPacket(10, 5, listOf(11, 22, 33), LegacyCanSource.FROM_DEVICE)
|
||||
val bank = LegacyCanTerminalProtocol.applyPacket(LegacyCanTerminalProtocol.emptyRegisterBank(), packet, 42)
|
||||
assertEquals(listOf(11, 33), listOf(bank[10].value, bank[12].value))
|
||||
assertEquals(0, bank[11].value)
|
||||
assertEquals(42, bank[12].revision)
|
||||
}
|
||||
|
||||
@Test fun catalogContainsAllLegacyProjectsAndNodes() {
|
||||
assertEquals(9, LegacyCanProjects.all.size)
|
||||
assertEquals(10, LegacyCanProjects.all.first { it.name == "23550.2" }.nodes.size)
|
||||
assertEquals("Start", LegacyCanProjects.all.first { it.name == "Бальзам" }.commandNames[7])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ class PlotContractTest {
|
||||
@Test fun markerCoordinatesSurviveZoomAndDragUsesGestureStart() {
|
||||
val full = PlotBounds(1000.0, 2000.0, -10.0, 10.0)
|
||||
val markers = TrendMarkers().positioned(full)
|
||||
assertTrue(markers.enabled(TrendMarker.A))
|
||||
assertFalse(markers.enabled(TrendMarker.C))
|
||||
assertTrue(markers.copy(xPairs = 2).enabled(TrendMarker.C))
|
||||
val zoomed = PlotBounds(1250.0, 1750.0, -5.0, 5.0)
|
||||
assertEquals(markers, markers.positioned(zoomed))
|
||||
val dragged = markers.drag(TrendMarker.A, 50.0, 500.0, zoomed)
|
||||
@@ -34,4 +37,12 @@ class PlotContractTest {
|
||||
val crossed = markers.move(TrendMarker.A, 1900.0).move(TrendMarker.B, 1100.0)
|
||||
assertEquals(-800.0, plotDelta(crossed.a!!, crossed.b!!), 0.0)
|
||||
}
|
||||
|
||||
@Test fun lockedViewportIgnoresZoomAndPanAndDbUsesAmplitudeRatio() {
|
||||
val locked = PlotViewport(locked = true)
|
||||
assertEquals(locked, locked.transform(zoomX = 2.0, panY = 0.2))
|
||||
assertEquals(20.0, plotDbDelta(1.0, 10.0)!!, 1e-10)
|
||||
assertEquals(-20.0, plotDbDelta(10.0, 1.0)!!, 1e-10)
|
||||
assertNull(plotDbDelta(0.0, 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,12 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
182
c/set-protocol/src/balsam_can.c
Normal file
182
c/set-protocol/src/balsam_can.c
Normal file
@@ -0,0 +1,182 @@
|
||||
#include "balsam_can.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static uint16_t get_be16(const uint8_t *data)
|
||||
{
|
||||
return (uint16_t)(((uint16_t)data[0] << 8) | data[1]);
|
||||
}
|
||||
|
||||
static size_t copy_name(const char *name, char *output, size_t output_size)
|
||||
{
|
||||
size_t length = strlen(name);
|
||||
if ((output != NULL) && (output_size != 0U)) {
|
||||
size_t copied = length < output_size - 1U ? length : output_size - 1U;
|
||||
memcpy(output, name, copied);
|
||||
output[copied] = '\0';
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
static size_t indexed_name(const char *prefix, unsigned index,
|
||||
char *output, size_t output_size)
|
||||
{
|
||||
int length;
|
||||
if ((output == NULL) || (output_size == 0U)) {
|
||||
char scratch[64];
|
||||
length = snprintf(scratch, sizeof scratch, "%s %u", prefix, index);
|
||||
} else {
|
||||
length = snprintf(output, output_size, "%s %u", prefix, index);
|
||||
}
|
||||
return length > 0 ? (size_t)length : 0U;
|
||||
}
|
||||
|
||||
int balsam_can_is_id(uint32_t can_id)
|
||||
{
|
||||
uint32_t relative = (can_id & 0x1FFFFFFFUL) - BALSAM_CAN_BASE_ID;
|
||||
return relative < BALSAM_CAN_NODE_COUNT
|
||||
|| (relative >= BALSAM_CAN_DATA_OFFSET
|
||||
&& relative < BALSAM_CAN_DATA_OFFSET + BALSAM_CAN_NODE_COUNT);
|
||||
}
|
||||
|
||||
int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||
balsam_can_frame_t *output)
|
||||
{
|
||||
uint32_t relative;
|
||||
uint16_t header;
|
||||
if ((data == NULL) || (output == NULL)) return -1;
|
||||
can_id &= 0x1FFFFFFFUL;
|
||||
if (!balsam_can_is_id(can_id)) return 0;
|
||||
if (size != BALSAM_CAN_DLC) return -2;
|
||||
|
||||
relative = can_id - BALSAM_CAN_BASE_ID;
|
||||
output->direction = relative >= BALSAM_CAN_DATA_OFFSET
|
||||
? BALSAM_CAN_FROM_NODE : BALSAM_CAN_TO_NODE;
|
||||
output->device = (uint8_t)((relative & 0x0FU) + 1U);
|
||||
header = get_be16(data);
|
||||
output->present_mask = (uint8_t)((header >> 13) & 0x07U);
|
||||
output->start_address = (uint16_t)(header & 0x1FFFU);
|
||||
output->values[0] = get_be16(&data[2]);
|
||||
output->values[1] = get_be16(&data[4]);
|
||||
output->values[2] = get_be16(&data[6]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *balsam_can_device_name(uint8_t device)
|
||||
{
|
||||
static const char *const names[BALSAM_CAN_NODE_COUNT] = {
|
||||
"Трансформатор 1", "Трансформатор 2", "Силовой блок 1",
|
||||
"Силовой блок 2", "УМП 1", "УМП 2", "Двигатель", "ВЭП",
|
||||
"Задатчик", "Узел 10", "Узел 11", "Узел 12", "Терминал"
|
||||
};
|
||||
return (device >= 1U && device <= BALSAM_CAN_NODE_COUNT)
|
||||
? names[device - 1U] : "Неизвестный узел";
|
||||
}
|
||||
|
||||
size_t balsam_can_register_name(uint8_t device, uint16_t address,
|
||||
char *output, size_t output_size)
|
||||
{
|
||||
if ((device == 1U || device == 2U) && address < 20U)
|
||||
return indexed_name("Диагностика T°", address + 1U, output, output_size);
|
||||
if ((device == 1U || device == 2U) && address >= 0x18U && address <= 0x2BU)
|
||||
return indexed_name("Показания T°", address - 0x17U, output, output_size);
|
||||
if ((device == 1U || device == 2U) && address >= 0x30U && address <= 0x43U)
|
||||
return indexed_name("Аварийная уставка T°", address - 0x2FU, output, output_size);
|
||||
if ((device == 1U || device == 2U) && address >= 0x48U && address <= 0x5BU)
|
||||
return indexed_name("Предупредительная уставка T°", address - 0x47U, output, output_size);
|
||||
|
||||
if ((device == 3U || device == 4U) && address < 16U)
|
||||
return indexed_name("Диагностика T°", address + 1U, output, output_size);
|
||||
if ((device == 3U || device == 4U) && address >= 0x18U && address <= 0x27U)
|
||||
return indexed_name("Показания T°", address - 0x17U, output, output_size);
|
||||
if ((device == 3U || device == 4U) && address == 0x28U)
|
||||
return copy_name("Действующее Uвх1", output, output_size);
|
||||
if ((device == 3U || device == 4U) && address == 0x29U)
|
||||
return copy_name("Амплитудное Uвх1", output, output_size);
|
||||
if ((device == 3U || device == 4U) && address == 0x2AU)
|
||||
return copy_name("Действующее Uвх2", output, output_size);
|
||||
if ((device == 3U || device == 4U) && address == 0x2BU)
|
||||
return copy_name("Амплитудное Uвх2", output, output_size);
|
||||
|
||||
if ((device == 5U || device == 6U) && address == 0U)
|
||||
return copy_name("Диагностика Utr a", output, output_size);
|
||||
if ((device == 5U || device == 6U) && address == 1U)
|
||||
return copy_name("Диагностика Utr c", output, output_size);
|
||||
if ((device == 5U || device == 6U) && address == 2U)
|
||||
return copy_name("Диагностика Itr a", output, output_size);
|
||||
if ((device == 5U || device == 6U) && address == 3U)
|
||||
return copy_name("Диагностика Itr c", output, output_size);
|
||||
if ((device == 5U || device == 6U) && address == 0x18U)
|
||||
return copy_name("Действующее Utr", output, output_size);
|
||||
if ((device == 5U || device == 6U) && address == 0x19U)
|
||||
return copy_name("Амплитудное Utr", output, output_size);
|
||||
if ((device == 5U || device == 6U) && address == 0x1AU)
|
||||
return copy_name("Действующее Itr", output, output_size);
|
||||
if ((device == 5U || device == 6U) && address == 0x1BU)
|
||||
return copy_name("Амплитудное Itr", output, output_size);
|
||||
if ((device == 5U || device == 6U) && address == 0x1CU)
|
||||
return copy_name("Ток СИФУ, задание (mA*10)", output, output_size);
|
||||
if ((device == 5U || device == 6U) && address == 0x1DU)
|
||||
return copy_name("Ток СИФУ, обратная связь (mA*10)", output, output_size);
|
||||
|
||||
if (device == 7U && address < 8U)
|
||||
return indexed_name("Диагностика T°", address + 1U, output, output_size);
|
||||
if (device == 7U && address >= 0x18U && address <= 0x1FU)
|
||||
return indexed_name("Показания T°", address - 0x17U, output, output_size);
|
||||
|
||||
if (device == 8U) {
|
||||
static const char *const vep_names[] = {
|
||||
"Диагностика 380В Ф1", "Диагностика 380В Ф2",
|
||||
"Диагностика 31В Ф1", "Диагностика 31В Ф2",
|
||||
"Диагностика 31В UC1", "Диагностика 31В UC2",
|
||||
"Диагностика 24В ПУ", "Диагностика 27В ФА",
|
||||
"Диагностика 24В ПК", "Диагностика 15В ДР",
|
||||
"Диагностика +24В ДТ", "Диагностика -24В ДТ",
|
||||
"Диагностика 24В ПМУ", "Диагностика T° 1", "Диагностика T° 2"
|
||||
};
|
||||
static const char *const vep_values[] = {
|
||||
"Показания 380В Ф1", "Показания 380В Ф2", "Показания 31В Ф1",
|
||||
"Показания 31В Ф2", "Показания 31В UC1", "Показания 31В UC2",
|
||||
"Показания 24В ПУ", "Показания 27В ФА", "Показания 24В ПК",
|
||||
"Показания 15В ДР", "Показания +24В ДТ", "Показания -24В ДТ",
|
||||
"Показания 24В ПМУ", "Показания T° 1", "Показания T° 2"
|
||||
};
|
||||
if (address < sizeof vep_names / sizeof vep_names[0])
|
||||
return copy_name(vep_names[address], output, output_size);
|
||||
if (address >= 0x18U && address < 0x18U + sizeof vep_values / sizeof vep_values[0])
|
||||
return copy_name(vep_values[address - 0x18U], output, output_size);
|
||||
if (address == 0x10U)
|
||||
return copy_name("Дискретные датчики (14 бит)", output, output_size);
|
||||
}
|
||||
|
||||
if (device == 9U && address == 0U)
|
||||
return copy_name("Обороты ГЭД, об/мин", output, output_size);
|
||||
if (device == 9U && address == 1U)
|
||||
return copy_name("Обороты ГВ, об/мин", output, output_size);
|
||||
if (device == 9U && address == 2U)
|
||||
return copy_name("Лампы (8 бит)", output, output_size);
|
||||
if (device == 9U && address == 3U)
|
||||
return copy_name("Диоды (16 бит)", output, output_size);
|
||||
if (device == 9U && address == 0x10U)
|
||||
return copy_name("Кнопки (12 бит)", output, output_size);
|
||||
|
||||
if (address == 0x16U)
|
||||
return copy_name("Дискретные входы / кнопки", output, output_size);
|
||||
if (address == 0x17U)
|
||||
return copy_name("Состояние джамперов", output, output_size);
|
||||
if (address == 0x60U)
|
||||
return copy_name("Период быстрого CAN-цикла", output, output_size);
|
||||
if (address == 0x61U)
|
||||
return copy_name("Период медленного CAN-цикла", output, output_size);
|
||||
if (address == 0x62U)
|
||||
return copy_name("Яркость индикации", output, output_size);
|
||||
if (address == 0x63U)
|
||||
return copy_name("Период опроса OWEN", output, output_size);
|
||||
if (address == 0x7EU)
|
||||
return copy_name("Последний режим", output, output_size);
|
||||
if (address == 0x7FU)
|
||||
return copy_name("Команды", output, output_size);
|
||||
|
||||
return copy_name("", output, output_size);
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "pcan_crc.h"
|
||||
#include "balsam_can.h"
|
||||
#include "periph28335.h"
|
||||
#include "tms2812.h"
|
||||
#include "pcan_frame.h"
|
||||
#include "pcan_id.h"
|
||||
#include "gui_frame.h"
|
||||
@@ -60,6 +63,140 @@ uint16_t pcan_abi_crc16(const uint8_t *data, size_t size)
|
||||
return pcan_crc16(data, size);
|
||||
}
|
||||
|
||||
int pcan_abi_balsam_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||
pcan_abi_balsam_frame_t *output)
|
||||
{
|
||||
balsam_can_frame_t decoded;
|
||||
int status;
|
||||
if (output == NULL) return -1;
|
||||
status = balsam_can_decode(can_id, data, size, &decoded);
|
||||
if (status != 1) return status;
|
||||
output->device = decoded.device;
|
||||
output->direction = decoded.direction;
|
||||
output->present_mask = decoded.present_mask;
|
||||
output->start_address = decoded.start_address;
|
||||
memcpy(output->values, decoded.values, sizeof output->values);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *pcan_abi_balsam_device_name(uint8_t device)
|
||||
{
|
||||
return balsam_can_device_name(device);
|
||||
}
|
||||
|
||||
size_t pcan_abi_balsam_register_name(uint8_t device, uint16_t address,
|
||||
char *output, size_t output_size)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
uint16_t pcan_abi_tms2812_crc16(const uint8_t *data, size_t size)
|
||||
{
|
||||
return tms2812_crc16(data, size);
|
||||
}
|
||||
|
||||
size_t pcan_abi_tms2812_build_upload(
|
||||
uint8_t controller, uint32_t word_address, uint32_t byte_count,
|
||||
uint8_t *output, size_t output_size)
|
||||
{
|
||||
return tms2812_build_upload_request(
|
||||
controller, word_address, byte_count, output, output_size);
|
||||
}
|
||||
|
||||
size_t pcan_abi_tms2812_expected_upload_size(uint32_t byte_count)
|
||||
{
|
||||
return tms2812_expected_upload_response_size(byte_count);
|
||||
}
|
||||
|
||||
int pcan_abi_tms2812_validate_upload(
|
||||
const uint8_t *data, size_t size, uint8_t controller,
|
||||
uint32_t byte_count)
|
||||
{
|
||||
return tms2812_validate_upload_response(
|
||||
data, size, controller, byte_count);
|
||||
}
|
||||
|
||||
int pcan_abi_tms2812_decode_upload(
|
||||
const uint8_t *data, size_t size, uint8_t controller,
|
||||
uint32_t byte_count, uint8_t *output, size_t output_size)
|
||||
{
|
||||
return tms2812_decode_upload_response(
|
||||
data, size, controller, byte_count, output, output_size);
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
161
c/set-protocol/src/periph28335.c
Normal file
161
c/set-protocol/src/periph28335.c
Normal file
@@ -0,0 +1,161 @@
|
||||
#include "periph28335.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "set_crc.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)
|
||||
{
|
||||
return set_crc16_modbus(data, size);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
18
c/set-protocol/src/set_crc.c
Normal file
18
c/set-protocol/src/set_crc.c
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "set_crc.h"
|
||||
|
||||
uint16_t set_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;
|
||||
}
|
||||
@@ -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};
|
||||
static const size_t sizes[] = {10, 3, 4, 4, 6, 2, 3, 2, 4};
|
||||
double span, fraction;
|
||||
if (op > SET_PLOT_DELTA || !v || !out || n != sizes[op] ||
|
||||
cap < (op == SET_PLOT_TRANSFORM ? 4U : 1U)) return 0;
|
||||
if (op > SET_PLOT_LIMITS || !v || !out || n != sizes[op] ||
|
||||
cap < (op == SET_PLOT_TRANSFORM || op == SET_PLOT_LIMITS ? 4U : 1U)) return 0;
|
||||
if (op == SET_PLOT_TRANSFORM) {
|
||||
double w, h, fx, fy;
|
||||
if (!finite_values(v, 4) || v[2] < 1.0/128 || v[2] > 1 ||
|
||||
@@ -65,6 +65,14 @@ size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t
|
||||
break;
|
||||
}
|
||||
case SET_PLOT_DELTA: out[0] = (v[1] - v[0]) * v[2]; break;
|
||||
case SET_PLOT_DB_DELTA:
|
||||
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,6 +4,13 @@
|
||||
|
||||
#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;
|
||||
@@ -124,3 +131,35 @@ 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,6 +6,12 @@ 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;
|
||||
@@ -63,6 +69,12 @@ 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;
|
||||
@@ -70,5 +82,6 @@ int set_trend_watch_values(const uint8_t *payload, size_t size, uint16_t *words,
|
||||
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;
|
||||
}
|
||||
|
||||
78
c/set-protocol/src/tms2812.c
Normal file
78
c/set-protocol/src/tms2812.c
Normal file
@@ -0,0 +1,78 @@
|
||||
#include "tms2812.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "set_crc.h"
|
||||
|
||||
static void put_le32(uint8_t *output, uint32_t value)
|
||||
{
|
||||
output[0] = (uint8_t)value;
|
||||
output[1] = (uint8_t)(value >> 8);
|
||||
output[2] = (uint8_t)(value >> 16);
|
||||
output[3] = (uint8_t)(value >> 24);
|
||||
}
|
||||
|
||||
uint16_t tms2812_crc16(const uint8_t *data, size_t size)
|
||||
{
|
||||
return set_crc16_modbus(data, size);
|
||||
}
|
||||
|
||||
size_t tms2812_build_upload_request(
|
||||
uint8_t controller, uint32_t word_address, uint32_t byte_count,
|
||||
uint8_t *output, size_t output_size)
|
||||
{
|
||||
uint16_t crc;
|
||||
if ((output == NULL) || (output_size < TMS2812_UPLOAD_REQUEST_SIZE) ||
|
||||
(byte_count == 0U)) return 0U;
|
||||
output[0] = controller;
|
||||
output[1] = TMS2812_CMD_UPLOAD;
|
||||
put_le32(&output[2], word_address);
|
||||
put_le32(&output[6], byte_count);
|
||||
crc = tms2812_crc16(output, 10U);
|
||||
output[10] = (uint8_t)crc;
|
||||
output[11] = (uint8_t)(crc >> 8);
|
||||
return TMS2812_UPLOAD_REQUEST_SIZE;
|
||||
}
|
||||
|
||||
size_t tms2812_expected_upload_response_size(uint32_t byte_count)
|
||||
{
|
||||
if (byte_count == 0U ||
|
||||
(uint64_t)byte_count + TMS2812_UPLOAD_RESPONSE_OVERHEAD > SIZE_MAX) {
|
||||
return 0U;
|
||||
}
|
||||
return (size_t)byte_count + TMS2812_UPLOAD_RESPONSE_OVERHEAD;
|
||||
}
|
||||
|
||||
int tms2812_validate_upload_response(
|
||||
const uint8_t *data, size_t size, uint8_t controller,
|
||||
uint32_t byte_count)
|
||||
{
|
||||
size_t expected = tms2812_expected_upload_response_size(byte_count);
|
||||
size_t crc_offset;
|
||||
uint16_t expected_crc;
|
||||
uint16_t actual_crc;
|
||||
if (data == NULL) return TMS2812_ERROR_ARGUMENT;
|
||||
if (expected == 0U) return TMS2812_ERROR_RANGE;
|
||||
if (size != expected) return TMS2812_ERROR_LENGTH;
|
||||
if ((data[0] != controller) || (data[1] != TMS2812_CMD_UPLOAD))
|
||||
return TMS2812_ERROR_HEADER;
|
||||
crc_offset = (size_t)byte_count + 2U;
|
||||
expected_crc = tms2812_crc16(data, crc_offset);
|
||||
actual_crc = (uint16_t)(data[crc_offset] |
|
||||
((uint16_t)data[crc_offset + 1U] << 8));
|
||||
return actual_crc == expected_crc ? TMS2812_OK : TMS2812_ERROR_CRC;
|
||||
}
|
||||
|
||||
int tms2812_decode_upload_response(
|
||||
const uint8_t *data, size_t size, uint8_t controller,
|
||||
uint32_t byte_count, uint8_t *output, size_t output_size)
|
||||
{
|
||||
int status = tms2812_validate_upload_response(
|
||||
data, size, controller, byte_count);
|
||||
if (status != TMS2812_OK) return status;
|
||||
if ((output == NULL) || (output_size < byte_count))
|
||||
return TMS2812_ERROR_CAPACITY;
|
||||
memcpy(output, &data[2], byte_count);
|
||||
return TMS2812_OK;
|
||||
}
|
||||
5
c/set-protocol/tests/fixtures/plot-v1.json
vendored
5
c/set-protocol/tests/fixtures/plot-v1.json
vendored
@@ -20,6 +20,11 @@
|
||||
{"name":"ticks","op":5,"input":[100,800],"output":[20]},
|
||||
{"name":"negative_delta","op":6,"input":[10,0,1],"output":[-10]},
|
||||
{"name":"milliseconds","op":6,"input":[0,0.01,1000],"output":[10]},
|
||||
{"name":"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}
|
||||
|
||||
30
c/set-protocol/tests/test_balsam_can.c
Normal file
30
c/set-protocol/tests/test_balsam_can.c
Normal file
@@ -0,0 +1,30 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "balsam_can.h"
|
||||
|
||||
#define CHECK(x) do { if (!(x)) { \
|
||||
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); return 1; \
|
||||
} } while (0)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const uint8_t packet[8] = { 0xE0, 0x18, 0x00, 0x29, 0xFF, 0xFE, 0x12, 0x34 };
|
||||
balsam_can_frame_t frame;
|
||||
char name[64];
|
||||
|
||||
CHECK(balsam_can_is_id(0x00BA0010UL));
|
||||
CHECK(balsam_can_is_id(0x00BA001CUL));
|
||||
CHECK(!balsam_can_is_id(0x00BA001DUL));
|
||||
CHECK(balsam_can_decode(0x00BA0010UL, packet, sizeof packet, &frame) == 1);
|
||||
CHECK(frame.device == 1U);
|
||||
CHECK(frame.direction == BALSAM_CAN_FROM_NODE);
|
||||
CHECK(frame.present_mask == 7U);
|
||||
CHECK(frame.start_address == 0x18U);
|
||||
CHECK(frame.values[0] == 41U && frame.values[1] == 0xFFFEU
|
||||
&& frame.values[2] == 0x1234U);
|
||||
CHECK(balsam_can_register_name(1U, 0x18U, name, sizeof name) > 0U);
|
||||
CHECK(strstr(name, "T° 1") != NULL);
|
||||
CHECK(balsam_can_decode(0x00BA0010UL, packet, 7U, &frame) == -2);
|
||||
return 0;
|
||||
}
|
||||
32
c/set-protocol/tests/test_periph28335.c
Normal file
32
c/set-protocol/tests/test_periph28335.c
Normal file
@@ -0,0 +1,32 @@
|
||||
#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,6 +26,13 @@ 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,6 +17,15 @@ 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;
|
||||
}
|
||||
|
||||
35
c/set-protocol/tests/test_tms2812.c
Normal file
35
c/set-protocol/tests/test_tms2812.c
Normal file
@@ -0,0 +1,35 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "tms2812.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t request[TMS2812_UPLOAD_REQUEST_SIZE];
|
||||
static const uint8_t prefix[] = {
|
||||
0x05U, 0x34U, 0x78U, 0x56U, 0x34U, 0x12U,
|
||||
0x00U, 0x10U, 0x00U, 0x00U
|
||||
};
|
||||
uint8_t reply[4U + TMS2812_UPLOAD_RESPONSE_OVERHEAD];
|
||||
uint8_t decoded[4];
|
||||
uint16_t crc;
|
||||
|
||||
if (tms2812_build_upload_request(5U, 0x12345678UL, 0x1000U,
|
||||
request, sizeof request) != sizeof request ||
|
||||
memcmp(request, prefix, sizeof prefix) != 0) return 1;
|
||||
reply[0] = 5U;
|
||||
reply[1] = TMS2812_CMD_UPLOAD;
|
||||
reply[2] = 0x10U; reply[3] = 0x20U; reply[4] = 0x30U; reply[5] = 0x40U;
|
||||
crc = tms2812_crc16(reply, 6U);
|
||||
reply[6] = (uint8_t)crc;
|
||||
reply[7] = (uint8_t)(crc >> 8);
|
||||
memset(&reply[8], 0, 4U);
|
||||
if (tms2812_decode_upload_response(reply, sizeof reply, 5U, 4U,
|
||||
decoded, sizeof decoded) != TMS2812_OK ||
|
||||
memcmp(decoded, &reply[2], sizeof decoded) != 0) return 2;
|
||||
reply[6] ^= 1U;
|
||||
if (tms2812_validate_upload_response(reply, sizeof reply, 5U, 4U)
|
||||
!= TMS2812_ERROR_CRC) return 3;
|
||||
puts("PM67/TMS320F2812 upload protocol tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -34,8 +34,11 @@ 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");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user