19 Commits

Author SHA1 Message Date
27ef6fcfdf Merge remote-tracking branch 'origin/codex/trend-display-scale' into HEAD 2026-09-05 11:43:08 +03:00
235d5d81a2 Share PM67 upload protocol through C core 2026-09-05 11:37:03 +03:00
59f847e900 Добавить множитель и IQ в Python-тренды 2026-09-05 03:45:05 +03:00
286e454464 Добавить выпуск прошивок из Keil и CCS 2026-09-05 03:16:16 +03:00
e691dfc337 Добавить публикацию каталога прошивок 2026-09-05 03:05:17 +03:00
f5f15f6a04 Сохранить совместимость ABI графиков 2026-09-05 02:44:08 +03:00
be066a57eb Дополнить общий контракт границ осей 2026-09-05 02:43:07 +03:00
b1f7b965f4 Расширить общие API графиков и GAS обмена 2026-09-05 02:37:11 +03:00
78d3f6690b Align Android parser statistics with C core 2026-09-04 21:10:02 +03:00
6a82b309cc Move PM35 protocol into shared C core 2026-09-04 21:06:40 +03:00
2316a5a26a Добавить Android API протокола ПМ35 2026-09-04 20:39:04 +03:00
d44d57a7aa Убрать лишнюю строку в Legacy CAN API 2026-09-04 19:17:27 +03:00
b972b6f33c Добавить канал TMS в каталог прошивок 2026-09-04 19:15:48 +03:00
454baeed98 Добавить общий протокол Periph28335 2026-09-04 19:15:48 +03:00
01ffc0e496 Добавить общий Python API старого CAN terminal 2026-09-04 19:15:47 +03:00
3c4ac9963d Добавить общий API старого CAN terminal 2026-09-04 19:15:47 +03:00
d9eb7dd9ad feat(plot): lock axes and calculate marker levels in dB 2026-09-04 19:15:47 +03:00
f6787163dc feat(plot): configure one or two marker pairs per axis 2026-09-04 19:15:47 +03:00
5f8fc07f2a Merge pull request 'Добавить общие графики и драйвер parallel NAND' (#3) from feature/shared-plot-and-parallel-nand into master
Reviewed-on: #3
2026-09-04 12:39:12 +03:00
74 changed files with 3809 additions and 51 deletions

View File

@@ -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
View 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).

View File

@@ -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)

View File

@@ -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).
## Структура
| Каталог | Назначение |

View File

@@ -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 и оформление шкал |
| Перемещение маркера от начальной координаты, ограничение видимой областью | Захват линии пальцем/мышью, редактор положения |
| Разность BA, DC и множитель единиц, шаги шкалы 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 получает готовые уровни в дБмВ от
прибора. Общая интерактивная часть не меняет эти данные или единицы.
## Проверка и сборка

View File

@@ -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 должен подтвердить весь
список: при частичном принятии нельзя определить пропущенные адреса, поэтому
строить график по смещённым индексам запрещено. При паузе порт приложения

View 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 */

View File

@@ -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,

View 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 */

View 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 */

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View 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 */

View File

@@ -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 \

View File

@@ -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

View File

@@ -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],
)
}

View File

@@ -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,

View File

@@ -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 -> ""
}
}

View File

@@ -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",
),
)
}

View File

@@ -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" }
}
}

View File

@@ -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))
}
}

View File

@@ -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])
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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]) }
}
}

View File

@@ -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,

View File

@@ -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,
&timestamp, 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;

View File

@@ -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"))
}
}

View File

@@ -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])
}
}

View File

@@ -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))
}
}

View File

@@ -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))
}
}

View File

@@ -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)
}
}

View 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);
}

View File

@@ -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)

View 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;
}

View 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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View 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;
}

View File

@@ -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}

View 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;
}

View 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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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;
}

View File

@@ -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), &timestamp, 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