chore(merge): объедини pysetterminal с main
Сохрани актуальный генератор firmware identity с экспортом всей папки сборки и временем UTC+3 при объединении веток.
This commit is contained in:
275
c/firmware-info/BUILD_IDENTITY.md
Normal file
275
c/firmware-info/BUILD_IDENTITY.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Версия работающей прошивки и SHA-256 файла: TMS2812, TMS28335, STM32
|
||||
|
||||
## 1. Какие задачи решает механизм
|
||||
|
||||
Механизм продолжает контракт Climate: устройство сообщает SemVer, дату/время
|
||||
сборки и Git Build ID в 12 регистрах. Компьютер после сборки вычисляет SHA-256
|
||||
готового файла и сохраняет паспорт `*.manifest.json` и файл `*.sha256`.
|
||||
|
||||
Это три разных идентификатора:
|
||||
|
||||
| Идентификатор | Что определяет | Где хранится |
|
||||
|---|---|---|
|
||||
| `major.minor.patch` | Назначенный разработчиком номер версии | В образе и паспорте |
|
||||
| Git Build ID | Исходный коммит; `+` отмечает локальные изменения | В образе и паспорте |
|
||||
| SHA-256 | Точные байты конкретного BIN/HEX, включая заголовки | Рядом с образом |
|
||||
|
||||
SHA Git-коммита и SHA-256 файла прошивки не взаимозаменяемы. Один коммит может
|
||||
давать разные бинарные файлы при смене компилятора, опций, времени сборки или
|
||||
зависимостей. Полные коммиты приложения и templates записываются в паспорт.
|
||||
|
||||
Контроллер не вычисляет SHA своей памяти. Ответ версии подтверждает, какие
|
||||
метаданные содержит работающий образ; SHA файла проверяет файл на компьютере.
|
||||
Для проверки фактической записи нужна отдельная штатная проверка программатора
|
||||
или чтение памяти назад. SHA рядом с файлом не является цифровой подписью.
|
||||
|
||||
## 2. Слои и файлы
|
||||
|
||||
```text
|
||||
firmware_identity.json проекта + Git приложения + Git templates
|
||||
|
|
||||
tools/identity.py prepare (до компиляции)
|
||||
|
|
||||
generated/firmware_identity.h + .json
|
||||
|
|
||||
src/firmware_info_static.c (общий C-код)
|
||||
|
|
||||
12 неизменяемых слов -> порт транспорта -> клиент
|
||||
|
||||
линковка -> преобразование BIN -> проверка размера -> identity.py seal
|
||||
|
|
||||
BIN + BIN.manifest.json + BIN.sha256
|
||||
```
|
||||
|
||||
| Файл | Задача и зависимости |
|
||||
|---|---|
|
||||
| `include/firmware_info_static.h` | Контракт компактного порта и упаковка слов, только `stdint.h` |
|
||||
| `src/firmware_info_static.c` | Константная таблица и чтение одного слова; нужен generated header |
|
||||
| `include/firmware_info.h`, `src/firmware_info.c` | Существующий динамический API Climate, валидация и сериализация |
|
||||
| `src/firmware_info_port.c` | Динамический адаптер конфигурации; поддерживает новый UTC stamp |
|
||||
| `ports/tms320f2812`, `ports/tms320f28335`, `ports/stm32` | Примеры конфигурации для нового генератора |
|
||||
| `tools/identity.py` | Git, JSON, SHA-256 файла; Python 3.10+ и Git, без pip-зависимостей |
|
||||
| `tests/test_static.c` | Совпадение компактного и динамического контрактов, чтение RS/CAN, запрет записи |
|
||||
| `tests/test_identity.py` | Генерация, dirty-зависимость, отсутствие Git, порча файла |
|
||||
|
||||
Ядро не знает HAL, CMSIS, XINTF, EEPROM, UART, CAN-регистры и адрес устройства.
|
||||
`firmware_info_static_read(unsigned index)` принимает индекс **0..11**, возвращает
|
||||
`uint16_t`; вне диапазона возвращает 0. Проверять диапазон запроса и запрещать
|
||||
запись обязан транспорт. Нулевую версию контракта клиент показывает как
|
||||
«версия неизвестна», а не подставляет версию из выбранного файла.
|
||||
|
||||
## 3. Конфигурация проекта и генерация
|
||||
|
||||
В репозитории приложения создайте `firmware_identity.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "MyController",
|
||||
"target": "TMS320F2812",
|
||||
"version": [1, 0, 0]
|
||||
}
|
||||
```
|
||||
|
||||
`product` и `target` нужны для паспорта файла. В контракт v1 они не входят:
|
||||
тип подключённого устройства по-прежнему определяет его собственный протокол.
|
||||
Не используйте совпадение версии как разрешение залить образ другого изделия.
|
||||
|
||||
До каждой компиляции выполните (пути приведены для checkout в `lib/templates`):
|
||||
|
||||
```powershell
|
||||
python lib/templates/c/firmware-info/tools/identity.py prepare `
|
||||
--repository . --templates lib/templates `
|
||||
--config firmware_identity.json --output build/generated
|
||||
```
|
||||
|
||||
Добавьте `build/generated` и `lib/templates/c/firmware-info/include` в include
|
||||
paths. Скомпилируйте **один экземпляр**
|
||||
`lib/templates/c/firmware-info/src/firmware_info_static.c`. Если проект включает
|
||||
этот файл через агрегирующий `.c`, отдельно добавлять его в сборку нельзя.
|
||||
|
||||
Генератор намеренно переписывает header при каждом запуске: зависимый объект
|
||||
должен пересобраться. Отсутствие header — ошибка компиляции, а не `LOCALDEV`.
|
||||
Нужна корректная обработка зависимостей сборщиком; после изменения настроек IDE
|
||||
выполните полную пересборку. Не редактируйте исходники во время сборки.
|
||||
|
||||
Для разных конфигураций и параллельных сборок используйте разные `generated`
|
||||
каталоги. Не запускайте две сборки с одним output одновременно.
|
||||
|
||||
Время в новом пути — **UTC**, снято перед компиляцией. Если задан
|
||||
`SOURCE_DATE_EPOCH`, используется этот Unix timestamp; это помогает повторяемым
|
||||
сборкам, но не делает весь toolchain автоматически воспроизводимым.
|
||||
|
||||
### Значения Build ID
|
||||
|
||||
| Значение | Смысл |
|
||||
|---|---|
|
||||
| `a1b2c3d4` | Приложение и templates доступны в Git и оба дерева чистые |
|
||||
| `a1b2c3d+` | Изменено приложение или templates, включая неотслеживаемые файлы |
|
||||
| `NOGIT000` | Не удалось определить хотя бы один из двух Git-репозиториев |
|
||||
| `LOCALDEV` | Только старый динамический путь без generated header |
|
||||
|
||||
Полные commit SHA находятся в `source.commit` и `templates.commit` паспорта.
|
||||
`+` не идентифицирует содержимое локальных правок. Точный файл определяет его
|
||||
SHA-256; для воспроизводимого выпуска нужны сохранённые исходники и настройки.
|
||||
Добавляйте generated/build каталоги в `.gitignore`, иначе они будут честно
|
||||
делать следующую сборку dirty. Намеренно не игнорируйте новые исходные файлы.
|
||||
|
||||
## 4. Формат: одинаковые 12 слов на всех МК
|
||||
|
||||
| Индекс | Значение |
|
||||
|---:|---|
|
||||
| 0 | Версия контракта: 1 |
|
||||
| 1 | Major, 0..255 |
|
||||
| 2 | Minor, 0..255 |
|
||||
| 3 | Patch, 0..999 |
|
||||
| 4 | Год, 2000..2199 |
|
||||
| 5 | Месяц в старшем октете, день в младшем |
|
||||
| 6 | Час в старшем октете, минута в младшем |
|
||||
| 7 | Секунда |
|
||||
| 8 | ASCII Build ID: символы 0 и 1, первый в старшем октете |
|
||||
| 9 | Символы 2 и 3 |
|
||||
| 10 | Символы 4 и 5 |
|
||||
| 11 | Символы 6 и 7 |
|
||||
|
||||
Например, `abcdef0+` даёт `0x6162, 0x6364, 0x6566, 0x302B` в словах 8..11.
|
||||
Modbus передаёт каждое слово старшим октетом вперёд. Старый
|
||||
`firmware_info_to_le_bytes()` передаёт слова little-endian для соответствующих
|
||||
byte-протоколов; эти два представления нельзя путать.
|
||||
|
||||
На TMS320F2812/F28335 `CHAR_BIT == 16`, `sizeof(uint16_t) == 1`, тип `uint8_t`
|
||||
отсутствует. В динамическом API используется `firmware_info_octet`, основанный
|
||||
на `uint_least8_t`: на STM32 это прежние 8 бит, на C28x — контейнер 16 бит.
|
||||
Размеры byte-API означают число **элементов-октетов**, не физический `sizeof`
|
||||
переданного массива. На проводе всё равно 24 восьмибитных октета.
|
||||
|
||||
## 5. Порты и включение в транспорт
|
||||
|
||||
### TMS320F28335 / ПМ35
|
||||
|
||||
В 167 уже подключено через `Source/Internal/shared_ump.c`. Макрос
|
||||
`UMP_FIRMWARE_INFO_READ` связывает общий порт UMP с
|
||||
`firmware_info_static_read`. Диапазон `0x1200..0x120B` обслуживается до
|
||||
обращения к регистратору. Окно параметров `0..127` и лог не расширяются.
|
||||
|
||||
RS: функция 03, начальный адрес `0x1200` (4608), количество 12.
|
||||
Разрешены частичные чтения, не выходящие за конец окна. Функция 06 возвращает
|
||||
исключение 02, не изменяя ни версию, ни настройки регистратора.
|
||||
|
||||
Служебный CAN ПМ35: тот же UMP-запрос функции 03 на `0x1200`, количество 12,
|
||||
ненулевой token. Ответ — шесть фрагментов по два слова с тем же token.
|
||||
Частичные CAN-чтения этого окна отвергаются. Используйте существующие UMP
|
||||
сборщики/парсеры templates, не создавайте второй кодек в GUI.
|
||||
|
||||
### TMS320F2812 / ПМ67
|
||||
|
||||
Добавьте тот же `firmware_info_static.c` и include paths. Компактный порт не
|
||||
требует FPU и не использует регистры F28335. Конфигурация-пример лежит в
|
||||
`ports/tms320f2812/firmware_info_config.template.h`.
|
||||
|
||||
Выберите свободное окно в карте **конкретного ПМ67**. В обработчике чтения
|
||||
отдавайте `firmware_info_static_read(address - base)` после проверки диапазона.
|
||||
Не переносите автоматически CAN ID и UMP ПМ35: у ПМ67 отдельный протокол.
|
||||
Это поставляемый переносимый порт, а не заявление, что конкретная прошивка
|
||||
ПМ67 уже изменена или испытана на устройстве.
|
||||
|
||||
### STM32 / Modbus или SET
|
||||
|
||||
Компактный порт одинаков для F1/F4/G4. Для HAL он не требует ни инициализации,
|
||||
ни callback. Пример адаптации чтения **после проверки диапазона транспортом**:
|
||||
|
||||
```c
|
||||
#include "firmware_info_static.h"
|
||||
|
||||
/* FW_BASE выбирает проект; соответствие адресов проверяет Modbus/SET handler. */
|
||||
uint16_t app_firmware_word(uint16_t address)
|
||||
{
|
||||
unsigned index = (unsigned)(address - FW_BASE);
|
||||
return firmware_info_static_read(index);
|
||||
}
|
||||
```
|
||||
|
||||
Для STM32 UMP-периферии можно использовать уже существующий
|
||||
`set-protocol/ports/stm32f407-periph` и подставить этот reader в `ump_backend`.
|
||||
Для обычного Modbus используйте собственное свободное input/holding read-only
|
||||
окно, сохраняя 12 слов контракта. В Climate адрес был 1960; это **не** адрес
|
||||
167. Если удобнее заполнить RAM-регистры при старте, оставьте динамический API:
|
||||
`firmware_info_port_describe()` и `firmware_info_to_registers()`.
|
||||
|
||||
## 6. SHA-256 и паспорт после сборки
|
||||
|
||||
Сначала завершите линковку и преобразование в файл, который реально отправляется
|
||||
загрузчику. Затем проверьте формат и допустимый размер. Только после успеха:
|
||||
|
||||
```powershell
|
||||
python lib/templates/c/firmware-info/tools/identity.py seal `
|
||||
--image build/controller.bin --metadata build/generated/firmware_identity.json
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
controller.bin
|
||||
controller.bin.manifest.json
|
||||
controller.bin.sha256
|
||||
```
|
||||
|
||||
Паспорт содержит product, target, version, build_id, built_at_utc, полные commit
|
||||
SHA и dirty-состояния двух репозиториев, имя файла, размер и SHA-256. Передавайте
|
||||
эти три файла вместе. `seal` не извлекает версию из произвольного бинарника:
|
||||
metadata должен быть из **той же сборки**. Нельзя перепривязать старый BIN к
|
||||
новому pre-build JSON. Проверка версии на работающей плате замыкает этот цикл.
|
||||
|
||||
Проверка точного файла:
|
||||
|
||||
```powershell
|
||||
python lib/templates/c/firmware-info/tools/identity.py verify `
|
||||
--image build/controller.bin --manifest build/controller.bin.manifest.json
|
||||
```
|
||||
|
||||
При изменении даже одного байта команда завершится ненулевым кодом. SHA считается
|
||||
для всего файла: для SCI8 — вместе с boot table, для HEX — для текстового HEX,
|
||||
а не для нормализованного содержимого Flash. Выбирайте BIN для однозначного
|
||||
сравнения с бинарным файлом программатора.
|
||||
|
||||
## 7. Выпуск и проверка на устройстве
|
||||
|
||||
1. Назначьте SemVer в конфигурации приложения.
|
||||
2. Сохраните изменения templates отдельным коммитом и закрепите именно эту
|
||||
зависимость в потребителе. Затем сохраните исходники приложения.
|
||||
3. Убедитесь, что оба дерева чистые; соберите в отдельном output.
|
||||
4. Проверьте ограничения образа, наличие паспорта и результат `verify`.
|
||||
5. Загрузите именно этот файл штатным загрузчиком и перезапустите устройство.
|
||||
6. Прочитайте версию с устройства заново, отключив кэш клиента.
|
||||
7. Сравните все 12 слов с SemVer, UTC stamp и Build ID паспорта.
|
||||
8. Сохраните BIN, паспорт, настройки компилятора и результаты проверки вместе.
|
||||
|
||||
Изменение SemVer не должно менять `contract_version`: последняя меняется только
|
||||
при несовместимом изменении формата. При несовпадении даты/Build ID сначала
|
||||
проверьте выбранный BIN, конфигурацию IDE, generated include path и полную
|
||||
пересборку, затем адрес и кэш клиента.
|
||||
|
||||
## 8. Автоматические проверки
|
||||
|
||||
```powershell
|
||||
cmake -S c/firmware-info -B build/firmware-info
|
||||
cmake --build build/firmware-info
|
||||
ctest --test-dir build/firmware-info --output-on-failure
|
||||
python -m unittest discover -s c/firmware-info/tests -p test_identity.py
|
||||
```
|
||||
|
||||
Дополнительно компилируйте core/static через TI C2000 (`-v28 -ml`) и ARM
|
||||
toolchain. Host-тест не заменяет проверку C28x ABI и фактического обмена с платой.
|
||||
Для 167 есть отдельный `tests/firmware_identity/port_test.c`: он проверяет
|
||||
реальную привязку reader, RTU CRC, CAN prepare и сохранение окна регистратора.
|
||||
|
||||
## 9. Совместимость с существующими проектами
|
||||
|
||||
Старый `make_build_id.ps1`, конфигурации STM32F1/F4/G4 и динамический API остаются.
|
||||
Новый генератор не подключается к другим проектам сам: добавьте pre/post-build
|
||||
и generated include path по этой инструкции. На STM32 размеры публичных полей
|
||||
и сигнатура byte-буфера остаются прежними благодаря `uint_least8_t`.
|
||||
|
||||
Изменения сделаны в checkout templates, который уже использует 167:
|
||||
`newProject/SETGUI/third_party/templates`. Другие checkout/submodule не следует
|
||||
обновлять копированием отдельных `.c`: перенесите проверенный коммит templates
|
||||
и обновите ссылку зависимости. ПМ67 и ПМ35 сохраняют разные транспортные порты.
|
||||
@@ -10,3 +10,8 @@ add_executable(test_firmware_info tests/test_firmware_info.c)
|
||||
target_link_libraries(test_firmware_info PRIVATE firmware_info)
|
||||
enable_testing()
|
||||
add_test(NAME firmware_info COMMAND test_firmware_info)
|
||||
add_executable(test_firmware_info_static tests/test_static.c src/firmware_info_static.c
|
||||
../set-protocol/src/ump_protocol.c)
|
||||
target_include_directories(test_firmware_info_static PRIVATE tests/fixtures ../set-protocol/include)
|
||||
target_link_libraries(test_firmware_info_static PRIVATE firmware_info)
|
||||
add_test(NAME firmware_info_static COMMAND test_firmware_info_static)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Портирование firmware-info
|
||||
|
||||
Для компактного пути без парсера даты, TMS320F2812/F28335 и STM32, а также
|
||||
SHA-256/паспортов сборки используйте [BUILD_IDENTITY.md](BUILD_IDENTITY.md).
|
||||
Ниже сохранена инструкция для совместимого динамического API.
|
||||
|
||||
## Назначение и границы
|
||||
|
||||
Это C-библиотека описания работающего образа: SemVer, дата/время компиляции,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Firmware Info
|
||||
|
||||
Компактный вариант для TMS320F2812, TMS320F28335 и STM32, общий pre/post-build,
|
||||
SHA-256 файла и паспорт сборки: [подробная инструкция](BUILD_IDENTITY.md).
|
||||
Он сохраняет 12-словный контракт Climate и не требует парсера даты на MCU.
|
||||
|
||||
Переносимая библиотека версии реально запущенной прошивки. Повторяет контракт
|
||||
`FirmwareInfo` из `john103C6T6NewVer`, но оформлена как template: ядро не знает
|
||||
ни о HAL, ни о конкретном протоколе, ни о модели МК.
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Least-width octet storage preserves STM ABI, and compiles on C28x where
|
||||
* CHAR_BIT=16 and uint8_t does not exist. Values sent on wire are masked to 8 bits. */
|
||||
typedef uint_least8_t firmware_info_octet;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -24,11 +28,11 @@ typedef struct {
|
||||
uint16_t minor;
|
||||
uint16_t patch;
|
||||
uint16_t year;
|
||||
uint8_t month;
|
||||
uint8_t day;
|
||||
uint8_t hour;
|
||||
uint8_t minute;
|
||||
uint8_t second;
|
||||
firmware_info_octet month;
|
||||
firmware_info_octet day;
|
||||
firmware_info_octet hour;
|
||||
firmware_info_octet minute;
|
||||
firmware_info_octet second;
|
||||
char build_id[FIRMWARE_INFO_BUILD_ID_SIZE + 1U];
|
||||
} firmware_info_t;
|
||||
|
||||
@@ -56,7 +60,7 @@ firmware_info_status_t firmware_info_set_build_id(
|
||||
firmware_info_status_t firmware_info_to_registers(
|
||||
const firmware_info_t *info, uint16_t *registers, size_t count);
|
||||
firmware_info_status_t firmware_info_to_le_bytes(
|
||||
const firmware_info_t *info, uint8_t *payload, size_t size);
|
||||
const firmware_info_t *info, firmware_info_octet *payload, size_t size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
22
c/firmware-info/include/firmware_info_static.h
Normal file
22
c/firmware-info/include/firmware_info_static.h
Normal file
@@ -0,0 +1,22 @@
|
||||
/* Small, immutable firmware identity for targets with little code space.
|
||||
* The build tool supplies scalar constants; the C core owns register packing.
|
||||
* No startup parser, RAM copy, heap, HAL or runtime SHA implementation is needed.
|
||||
* Include firmware_identity.h (generated BEFORE compilation) before this file.
|
||||
*/
|
||||
#ifndef FIRMWARE_INFO_STATIC_H
|
||||
#define FIRMWARE_INFO_STATIC_H
|
||||
#include <stdint.h>
|
||||
|
||||
#define FIRMWARE_INFO_STATIC_COUNT 12U
|
||||
/* Cast BEFORE shifting: an int is only 16 bits on both C28x targets. */
|
||||
#define FI_PAIR(high, low) ((uint16_t)(((uint16_t)(high) << 8U) | (uint16_t)(low)))
|
||||
#define FIRMWARE_INFO_STATIC_INITIALIZER { \
|
||||
1U, FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, FIRMWARE_VERSION_PATCH, \
|
||||
FI_YEAR, FI_PAIR(FI_MONTH, FI_DAY), FI_PAIR(FI_HOUR, FI_MINUTE), FI_SECOND, \
|
||||
FI_PAIR(FI_ID_0, FI_ID_1), FI_PAIR(FI_ID_2, FI_ID_3), \
|
||||
FI_PAIR(FI_ID_4, FI_ID_5), FI_PAIR(FI_ID_6, FI_ID_7) }
|
||||
|
||||
/* Return zero outside the contract. The transport must reject invalid ranges
|
||||
* before calling this function; zero is NOT an invented firmware version. */
|
||||
uint16_t firmware_info_static_read(unsigned index);
|
||||
#endif
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef FIRMWARE_INFO_CONFIG_H
|
||||
#define FIRMWARE_INFO_CONFIG_H
|
||||
/* Family-independent STM32 static port: no HAL/CMSIS dependency.
|
||||
* F1/F4/G4 applications may use this instead of the runtime date parser.
|
||||
* Generate the header before compilation, then compile firmware_info_static.c.
|
||||
* Modbus sends each returned word high octet first; SET uses its own encoder. */
|
||||
#include "firmware_identity.h"
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
#ifndef FIRMWARE_INFO_CONFIG_H
|
||||
#define FIRMWARE_INFO_CONFIG_H
|
||||
/* Same C28x word representation as F28335, but transport remains PM67-specific.
|
||||
* Do not route PM67 commands through the PM35 logger/CAN adapter.
|
||||
* Compile firmware_info_static.c and expose its 12 words in a read-only window. */
|
||||
#include "firmware_identity.h"
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
#ifndef FIRMWARE_INFO_CONFIG_H
|
||||
#define FIRMWARE_INFO_CONFIG_H
|
||||
/* Generate before EVERY build. C28x has 16-bit char and no uint8_t.
|
||||
* For the compact port compile firmware_info_static.c, not the date parser.
|
||||
* The same generated header can also configure firmware_info_port.c. */
|
||||
#include "firmware_identity.h"
|
||||
#endif
|
||||
@@ -13,20 +13,20 @@ static const char months[12][4] = {
|
||||
|
||||
static int is_digit(char value) { return (value >= '0') && (value <= '9'); }
|
||||
|
||||
static int two_digits(const char *text, uint8_t *result)
|
||||
static int two_digits(const char *text, firmware_info_octet *result)
|
||||
{
|
||||
char high = text[0];
|
||||
char low = text[1];
|
||||
if (high == ' ') high = '0';
|
||||
if (!is_digit(high) || !is_digit(low)) return 0;
|
||||
*result = (uint8_t)(((uint8_t)(high - '0') * 10U) + (uint8_t)(low - '0'));
|
||||
*result = (firmware_info_octet)(((firmware_info_octet)(high - '0') * 10U) + (firmware_info_octet)(low - '0'));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int four_digits(const char *text, uint16_t *result)
|
||||
{
|
||||
uint16_t value = 0U;
|
||||
uint8_t i;
|
||||
firmware_info_octet i;
|
||||
for (i = 0U; i < 4U; ++i) {
|
||||
if (!is_digit(text[i])) return 0;
|
||||
value = (uint16_t)(value * 10U + (uint16_t)(text[i] - '0'));
|
||||
@@ -37,7 +37,7 @@ static int four_digits(const char *text, uint16_t *result)
|
||||
|
||||
firmware_info_status_t firmware_info_validate(const firmware_info_t *info)
|
||||
{
|
||||
uint8_t i;
|
||||
firmware_info_octet i;
|
||||
if (info == NULL) return FIRMWARE_INFO_INVALID_ARGUMENT;
|
||||
if ((info->major > 255U) || (info->minor > 255U) || (info->patch > 999U)
|
||||
|| (info->year < 2000U) || (info->year > 2199U)
|
||||
@@ -56,7 +56,7 @@ firmware_info_status_t firmware_info_validate(const firmware_info_t *info)
|
||||
firmware_info_status_t firmware_info_set_build_id(firmware_info_t *info,
|
||||
const char *build_id)
|
||||
{
|
||||
uint8_t i;
|
||||
firmware_info_octet i;
|
||||
if ((info == NULL) || (build_id == NULL)) return FIRMWARE_INFO_INVALID_ARGUMENT;
|
||||
for (i = 0U; i < FIRMWARE_INFO_BUILD_ID_SIZE && build_id[i] != '\0'; ++i) {
|
||||
if ((build_id[i] < 0x20) || (build_id[i] > 0x7e))
|
||||
@@ -71,8 +71,8 @@ firmware_info_status_t firmware_info_set_build_id(firmware_info_t *info,
|
||||
firmware_info_status_t firmware_info_parse_build_stamp(
|
||||
const char *date_text, const char *time_text, firmware_info_t *info)
|
||||
{
|
||||
uint8_t month;
|
||||
uint8_t found = 0U;
|
||||
firmware_info_octet month;
|
||||
firmware_info_octet found = 0U;
|
||||
if ((date_text == NULL) || (time_text == NULL) || (info == NULL))
|
||||
return FIRMWARE_INFO_INVALID_ARGUMENT;
|
||||
for (month = 0U; month < 12U; ++month) {
|
||||
@@ -84,14 +84,14 @@ firmware_info_status_t firmware_info_parse_build_stamp(
|
||||
|| !two_digits(&time_text[0], &info->hour)
|
||||
|| !two_digits(&time_text[3], &info->minute)
|
||||
|| !two_digits(&time_text[6], &info->second)) return FIRMWARE_INFO_OUT_OF_RANGE;
|
||||
info->month = (uint8_t)(month + 1U);
|
||||
info->month = (firmware_info_octet)(month + 1U);
|
||||
return FIRMWARE_INFO_OK;
|
||||
}
|
||||
|
||||
firmware_info_status_t firmware_info_to_registers(
|
||||
const firmware_info_t *info, uint16_t *registers, size_t count)
|
||||
{
|
||||
uint8_t i;
|
||||
firmware_info_octet i;
|
||||
firmware_info_status_t status;
|
||||
if ((registers == NULL) || (count < FIRMWARE_INFO_REGISTER_COUNT))
|
||||
return FIRMWARE_INFO_INVALID_ARGUMENT;
|
||||
@@ -104,24 +104,24 @@ firmware_info_status_t firmware_info_to_registers(
|
||||
registers[6] = (uint16_t)(((uint16_t)info->hour << 8U) | info->minute);
|
||||
registers[7] = info->second;
|
||||
for (i = 0U; i < 4U; ++i)
|
||||
registers[8U + i] = (uint16_t)(((uint16_t)(uint8_t)info->build_id[i * 2U] << 8U)
|
||||
| (uint8_t)info->build_id[i * 2U + 1U]);
|
||||
registers[8U + i] = (uint16_t)(((uint16_t)(firmware_info_octet)info->build_id[i * 2U] << 8U)
|
||||
| (firmware_info_octet)info->build_id[i * 2U + 1U]);
|
||||
return FIRMWARE_INFO_OK;
|
||||
}
|
||||
|
||||
firmware_info_status_t firmware_info_to_le_bytes(
|
||||
const firmware_info_t *info, uint8_t *payload, size_t size)
|
||||
const firmware_info_t *info, firmware_info_octet *payload, size_t size)
|
||||
{
|
||||
uint16_t words[FIRMWARE_INFO_REGISTER_COUNT];
|
||||
uint8_t i;
|
||||
firmware_info_octet i;
|
||||
firmware_info_status_t status;
|
||||
if ((payload == NULL) || (size < FIRMWARE_INFO_PAYLOAD_SIZE))
|
||||
return FIRMWARE_INFO_INVALID_ARGUMENT;
|
||||
status = firmware_info_to_registers(info, words, FIRMWARE_INFO_REGISTER_COUNT);
|
||||
if (status != FIRMWARE_INFO_OK) return status;
|
||||
for (i = 0U; i < FIRMWARE_INFO_REGISTER_COUNT; ++i) {
|
||||
payload[i * 2U] = (uint8_t)words[i];
|
||||
payload[i * 2U + 1U] = (uint8_t)(words[i] >> 8U);
|
||||
payload[i * 2U] = (firmware_info_octet)(words[i] & 0xffU);
|
||||
payload[i * 2U + 1U] = (firmware_info_octet)(words[i] >> 8U);
|
||||
}
|
||||
return FIRMWARE_INFO_OK;
|
||||
}
|
||||
|
||||
@@ -15,5 +15,13 @@ firmware_info_status_t firmware_info_port_describe(firmware_info_t *info)
|
||||
info->patch = (uint16_t)FIRMWARE_VERSION_PATCH;
|
||||
status = firmware_info_set_build_id(info, FIRMWARE_BUILD_ID);
|
||||
if (status != FIRMWARE_INFO_OK) return status;
|
||||
#ifdef FI_YEAR
|
||||
/* New build pipeline: use the same UTC stamp as the static port/manifest.
|
||||
* Existing consumers of make_build_id.ps1 keep their compiler-local stamp. */
|
||||
info->year = FI_YEAR; info->month = FI_MONTH; info->day = FI_DAY;
|
||||
info->hour = FI_HOUR; info->minute = FI_MINUTE; info->second = FI_SECOND;
|
||||
return firmware_info_validate(info);
|
||||
#else
|
||||
return firmware_info_parse_build_stamp(__DATE__, __TIME__, info);
|
||||
#endif
|
||||
}
|
||||
|
||||
15
c/firmware-info/src/firmware_info_static.c
Normal file
15
c/firmware-info/src/firmware_info_static.c
Normal file
@@ -0,0 +1,15 @@
|
||||
/* A required generated include deliberately makes a missing pre-build fail.
|
||||
* Silently using LOCALDEV here could disguise an obsolete linked object. */
|
||||
#include "firmware_identity.h"
|
||||
#include "firmware_info_static.h"
|
||||
|
||||
/* On STM these 12 words occupy 24 bytes; on C28x sizeof reports 12 addressable
|
||||
* 16-bit units, which are still 24 wire octets. Never use sizeof as a wire size.
|
||||
* const keeps the identity inside the firmware, away from saved parameters. */
|
||||
static const uint16_t firmware_identity_words[FIRMWARE_INFO_STATIC_COUNT] =
|
||||
FIRMWARE_INFO_STATIC_INITIALIZER;
|
||||
|
||||
uint16_t firmware_info_static_read(unsigned index)
|
||||
{
|
||||
return index < FIRMWARE_INFO_STATIC_COUNT ? firmware_identity_words[index] : 0U;
|
||||
}
|
||||
19
c/firmware-info/tests/fixtures/firmware_identity.h
vendored
Normal file
19
c/firmware-info/tests/fixtures/firmware_identity.h
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/* Deliberately fixed vector, not production identity. Used only by host tests. */
|
||||
#define FIRMWARE_VERSION_MAJOR 1U
|
||||
#define FIRMWARE_VERSION_MINOR 2U
|
||||
#define FIRMWARE_VERSION_PATCH 3U
|
||||
#define FI_YEAR 2026U
|
||||
#define FI_MONTH 10U
|
||||
#define FI_DAY 5U
|
||||
#define FI_HOUR 12U
|
||||
#define FI_MINUTE 34U
|
||||
#define FI_SECOND 56U
|
||||
#define FIRMWARE_BUILD_ID "abcdef0+"
|
||||
#define FI_ID_0 'a'
|
||||
#define FI_ID_1 'b'
|
||||
#define FI_ID_2 'c'
|
||||
#define FI_ID_3 'd'
|
||||
#define FI_ID_4 'e'
|
||||
#define FI_ID_5 'f'
|
||||
#define FI_ID_6 '0'
|
||||
#define FI_ID_7 '+'
|
||||
74
c/firmware-info/tests/test_identity.py
Normal file
74
c/firmware-info/tests/test_identity.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Host lifecycle tests: dirty dependencies, reproducible stamps, file integrity.
|
||||
|
||||
Run: python -m unittest discover -s c/firmware-info/tests -p test_identity.py
|
||||
"""
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
TOOL = Path(__file__).resolve().parents[1] / "tools/identity.py"
|
||||
spec = importlib.util.spec_from_file_location("identity", TOOL)
|
||||
identity = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(identity)
|
||||
|
||||
|
||||
class IdentityTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.config = self.root / "config.json"
|
||||
self.config.write_text(json.dumps({"product":"test", "target":"host", "version":[1,2,3]}))
|
||||
|
||||
def prepare(self, states):
|
||||
with patch.object(identity, "git_state", side_effect=states), patch.dict(os.environ, {"SOURCE_DATE_EPOCH":"1791194400"}):
|
||||
return identity.prepare(self.root, self.root, self.config, self.root)
|
||||
|
||||
def test_clean_and_dirty_dependency(self):
|
||||
clean = {"commit":"abcdef0123456789", "dirty":False}
|
||||
meta = self.prepare([clean,clean])
|
||||
self.assertEqual(meta["build_id"], "abcdef01")
|
||||
header = (self.root / "firmware_identity.h").read_text()
|
||||
self.assertIn("#define FIRMWARE_VERSION_PATCH 3U",header)
|
||||
dirty = dict(clean, dirty=True)
|
||||
self.assertEqual(self.prepare([clean,dirty])["build_id"], "abcdef0+")
|
||||
self.assertEqual(self.prepare([dirty,clean])["build_id"], "abcdef0+")
|
||||
|
||||
def test_missing_git_is_explicit(self):
|
||||
unknown = {"commit":None,"dirty":None}
|
||||
self.assertEqual(self.prepare([unknown,unknown])["build_id"], "NOGIT000")
|
||||
|
||||
def test_invalid_version(self):
|
||||
self.config.write_text(json.dumps({"product":"x","target":"x","version":[1,2,1000]}))
|
||||
with self.assertRaises(ValueError):
|
||||
identity.prepare(self.root,self.root,self.config,self.root)
|
||||
|
||||
def test_untracked_source_is_dirty(self):
|
||||
def git(*args):
|
||||
subprocess.run(["git","-C",str(self.root),*args],check=True,capture_output=True)
|
||||
git("init")
|
||||
git("-c","user.name=Test","-c","user.email=test@example.invalid","commit","--allow-empty","-m","test")
|
||||
# config.json is untracked and must prevent a clean-looking build ID.
|
||||
self.assertTrue(identity.git_state(self.root)["dirty"])
|
||||
|
||||
def test_cli_seal_verify_and_corruption(self):
|
||||
clean = {"commit":"abcdef0123456789", "dirty":False}
|
||||
self.prepare([clean,clean])
|
||||
image = self.root / "firmware.bin"
|
||||
image.write_bytes(b"abc")
|
||||
import sys
|
||||
subprocess.run([sys.executable,str(TOOL),"seal","--image",str(image),"--metadata",str(self.root/"firmware_identity.json")],check=True,capture_output=True)
|
||||
manifest = image.with_suffix(".bin.manifest.json")
|
||||
meta = identity.verify(image,manifest)
|
||||
self.assertEqual(meta["sha256"],"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
|
||||
image.write_bytes(b"abd")
|
||||
with self.assertRaises(ValueError): identity.verify(image,manifest)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
53
c/firmware-info/tests/test_static.c
Normal file
53
c/firmware-info/tests/test_static.c
Normal file
@@ -0,0 +1,53 @@
|
||||
/* One set of vectors checks static metadata against Climate's runtime encoder.
|
||||
* This catches word order and ASCII order changes on the host, before MCU use. */
|
||||
#include "firmware_identity.h"
|
||||
#include "firmware_info.h"
|
||||
#include "firmware_info_static.h"
|
||||
#include "ump_protocol.h"
|
||||
#include <assert.h>
|
||||
|
||||
static unsigned writes;
|
||||
static uint16_t read_identity(void *context, uint16_t address)
|
||||
{
|
||||
(void)context;
|
||||
return firmware_info_static_read(address - UMP_FIRMWARE_INFO);
|
||||
}
|
||||
static int write_identity(void *context, uint16_t address, uint16_t value)
|
||||
{
|
||||
(void)context; (void)address; (void)value;
|
||||
writes++;
|
||||
return 1;
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
firmware_info_t info;
|
||||
uint16_t expected[12], reply[UMP_MAX_WORDS];
|
||||
ump_octet frame[UMP_RTU_CAPACITY];
|
||||
ump_backend backend = {0, read_identity, write_identity};
|
||||
unsigned i, error;
|
||||
info.major = FIRMWARE_VERSION_MAJOR; info.minor = FIRMWARE_VERSION_MINOR;
|
||||
info.patch = FIRMWARE_VERSION_PATCH; info.year = FI_YEAR;
|
||||
info.month = FI_MONTH; info.day = FI_DAY; info.hour = FI_HOUR;
|
||||
info.minute = FI_MINUTE; info.second = FI_SECOND;
|
||||
assert(firmware_info_set_build_id(&info, FIRMWARE_BUILD_ID) == FIRMWARE_INFO_OK);
|
||||
assert(firmware_info_to_registers(&info, expected, 12) == FIRMWARE_INFO_OK);
|
||||
for (i=0; i<12; ++i) assert(firmware_info_static_read(i) == expected[i]);
|
||||
assert(firmware_info_static_read(12) == 0);
|
||||
assert(ump_prepare(&backend, 3, UMP_FIRMWARE_INFO, 12, 1, reply, &error) == 12 && !error);
|
||||
for (i=0; i<12; ++i) assert(reply[i] == expected[i]);
|
||||
for (i=0; i<6; ++i) {
|
||||
assert(ump_can_fragment(reply,12,i,42,0,frame) == 8);
|
||||
assert(frame[0] == expected[2*i] >> 8 && frame[1] == (expected[2*i] & 255));
|
||||
assert(frame[6] == 42 && frame[7] == 0);
|
||||
}
|
||||
assert(ump_rtu_response(&backend, 1, 3, UMP_FIRMWARE_INFO, 12, frame, sizeof(frame)/sizeof(frame[0])) == 29);
|
||||
assert(frame[2] == 24 && ump_crc(frame,29) == 0);
|
||||
assert(ump_validate(3, UMP_FIRMWARE_INFO+11, 1, 0) == 0);
|
||||
assert(ump_validate(3, UMP_FIRMWARE_INFO+11, 2, 0) == 2);
|
||||
assert(ump_validate(3, UMP_FIRMWARE_INFO, 0, 0) == 2);
|
||||
assert(ump_validate(3, UMP_FIRMWARE_INFO, 0xffffU, 0) == 2);
|
||||
assert(ump_validate(3, UMP_FIRMWARE_INFO+1, 11, 1) == 2);
|
||||
ump_prepare(&backend,6,UMP_FIRMWARE_INFO,1,0,reply,&error);
|
||||
assert(error == 2 && writes == 0);
|
||||
return 0;
|
||||
}
|
||||
@@ -149,6 +149,14 @@ if(SETP_BUILD_TESTS)
|
||||
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_periph28335_eeprom tests/test_periph28335_eeprom.c)
|
||||
target_link_libraries(test_periph28335_eeprom PRIVATE setprotocol_static)
|
||||
if(MSVC)
|
||||
target_compile_options(test_periph28335_eeprom PRIVATE /UNDEBUG)
|
||||
else()
|
||||
target_compile_options(test_periph28335_eeprom PRIVATE -UNDEBUG)
|
||||
endif()
|
||||
add_test(NAME periph28335_eeprom COMMAND test_periph28335_eeprom)
|
||||
add_executable(test_tms2812 tests/test_tms2812.c)
|
||||
target_link_libraries(test_tms2812 PRIVATE setprotocol_static)
|
||||
add_test(NAME shared_tms2812 COMMAND test_tms2812)
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# SETProtocol
|
||||
|
||||
`periph28335_eeprom_*` — read-only reader физической EEPROM Бальзам-167
|
||||
(AT25256B, 32 КиБ), со стабильным C ABI и портом
|
||||
`python/protocan/periph28335_eeprom.py`. Разметка платы задана в
|
||||
`include/balsam167_eeprom_layout.h`; PEEK, CRC и порядок байтов реализованы
|
||||
в `src/periph28335.c`. Проверка полного диапазона — `test_periph28335_eeprom`.
|
||||
|
||||
Универсальные сервисы эмуляторов: [адресное пространство, API и порт новой платы](docs/EMULATOR_SERVICES.md).
|
||||
`set_regmap` объединяет УМП, 2812, IGBT и DAC за общим API для RTU, CAN и других оболочек.
|
||||
|
||||
|
||||
44
c/set-protocol/include/balsam167_eeprom_block.h
Normal file
44
c/set-protocol/include/balsam167_eeprom_block.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/* Private PM35 FC03 EEPROM window; not standard Modbus register quantities.
|
||||
* AT25256B keeps its existing 0x8000..0xBFFF / count 1..120 requests.
|
||||
* M95M02 uses four 64-KiB windows: count bit15 marks the profile, bits9:8
|
||||
* select bank 0..3, bits7:0 carry word count. Address 0x8000..0xFFFF is
|
||||
* the word offset within a bank. Every request is self-contained; no global
|
||||
* bank register can be changed by another client. Old firmware rejects the
|
||||
* marked quantity. Neither profile silently reads the other memory type.
|
||||
*/
|
||||
#ifndef BALSAM167_EEPROM_BLOCK_H
|
||||
#define BALSAM167_EEPROM_BLOCK_H
|
||||
#include "balsam167_eeprom_layout.h"
|
||||
#define BALSAM167_EEPROM_RTU_BASE 0x8000U
|
||||
#define BALSAM167_EEPROM_BLOCK_WORDS 120U
|
||||
#define BALSAM167_EEPROM_M95_MARKER 0x8000U
|
||||
#define BALSAM167_EEPROM_WRITE_M95 0x44U
|
||||
static unsigned long balsam167_eeprom_capacity(unsigned profile)
|
||||
{
|
||||
return profile == 0 ? 32768UL : (profile == 1 ? 262144UL : 0UL);
|
||||
}
|
||||
static unsigned long balsam167_eeprom_block_word(unsigned address, unsigned count)
|
||||
{
|
||||
return ((unsigned long)(count & 0x0300U) << 7) + (address - 0x8000U);
|
||||
}
|
||||
static unsigned balsam167_eeprom_block_valid_profile(unsigned fn, unsigned address,
|
||||
unsigned count, unsigned profile)
|
||||
{
|
||||
unsigned n = count & 255U;
|
||||
unsigned long word;
|
||||
if (fn != 3 || address < 0x8000U || !n || n > 120U) return 0;
|
||||
if (profile == 0) return count == n && address < 0xC000U && n <= 0xC000U-address;
|
||||
if (profile != 1 || (count & 0xFC00U) != 0x8000U) return 0;
|
||||
word = balsam167_eeprom_block_word(address, count);
|
||||
return n <= 0x10000UL-address && word+n <= 131072UL;
|
||||
}
|
||||
static unsigned balsam167_eeprom_block_valid(unsigned fn, unsigned address, unsigned count)
|
||||
{
|
||||
#if EEPROM_M95M02
|
||||
return balsam167_eeprom_block_valid_profile(fn, address, count, 1);
|
||||
#else
|
||||
return fn == 3 && address >= 0x8000U && address < 0xC000U &&
|
||||
count && count <= 120U && count <= 0xC000U-address;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
29
c/set-protocol/include/balsam167_eeprom_layout.h
Normal file
29
c/set-protocol/include/balsam167_eeprom_layout.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/* Board-specific layout shared by firmware, linker and host tools.
|
||||
* Fitted part: AT25256B, top marking 5EBL = 256 Kbit = 32768 OCTETS.
|
||||
* ATMLH516 is a manufacturer/date code, not a 512-Kbit density code.
|
||||
* Numeric defines intentionally have no C suffix: TI linker also includes this.
|
||||
*/
|
||||
#ifndef BALSAM167_EEPROM_LAYOUT_H
|
||||
#define BALSAM167_EEPROM_LAYOUT_H
|
||||
/* EEPROM_M95M02 is selected by the board configuration, never autodetected
|
||||
* by writing test patterns. Keep numeric constants usable by the TI linker. */
|
||||
#define BALSAM167_AT25256_BYTES 0x8000
|
||||
#define BALSAM167_M95M02_BYTES 0x40000
|
||||
#ifndef EEPROM_M95M02
|
||||
#define EEPROM_M95M02 0
|
||||
#endif
|
||||
#if EEPROM_M95M02
|
||||
#define BALSAM167_EEPROM_BYTES BALSAM167_M95M02_BYTES
|
||||
#define BALSAM167_EEPROM_PAGE_BYTES 256
|
||||
#else
|
||||
#define BALSAM167_EEPROM_BYTES BALSAM167_AT25256_BYTES
|
||||
#define BALSAM167_EEPROM_PAGE_BYTES 64
|
||||
#endif
|
||||
#define BALSAM167_EEPROM_PARAM_WORDS 129
|
||||
#define BALSAM167_EEPROM_WORDS (BALSAM167_EEPROM_BYTES / 2)
|
||||
/* Preserve the historical end-exclusive parameter boundary. The final word
|
||||
* remains unused; moving it would break existing saved configurations. */
|
||||
#define BALSAM167_EEPROM_PARAMS_END_WORD (BALSAM167_EEPROM_WORDS - 1)
|
||||
#define BALSAM167_EEPROM_PARAMS_START_WORD (BALSAM167_EEPROM_PARAMS_END_WORD - BALSAM167_EEPROM_PARAM_WORDS)
|
||||
#define BALSAM167_EEPROM_FIRMWARE_BYTES (BALSAM167_EEPROM_PARAMS_START_WORD * 2)
|
||||
#endif
|
||||
@@ -82,6 +82,25 @@ PCAN_ABI_API size_t pcan_abi_balsam_register_name(
|
||||
/* PM35/TMS320F28335 direct RS232/485 register terminal. */
|
||||
PCAN_ABI_API uint16_t pcan_abi_periph28335_crc16(
|
||||
const uint8_t *data, size_t size);
|
||||
/* Caller-owned EEPROM reader state: uint32_t[4], 16 bytes, see periph28335.h.
|
||||
* Exported independently of PM67: the EEPROM routing is specific to 167. */
|
||||
PCAN_ABI_API int pcan_abi_periph28335_eeprom_begin(void *state,
|
||||
uint32_t controller, uint32_t start, uint32_t length);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_request(const void *state,
|
||||
uint8_t *output, size_t capacity);
|
||||
PCAN_ABI_API int pcan_abi_periph28335_eeprom_accept(void *state,
|
||||
const uint8_t *reply, size_t size, uint8_t *output, size_t capacity);
|
||||
PCAN_ABI_API uint32_t pcan_abi_periph28335_eeprom_capacity(void);
|
||||
PCAN_ABI_API uint32_t pcan_abi_periph28335_eeprom_capacity_profile(unsigned profile);
|
||||
PCAN_ABI_API int pcan_abi_periph28335_eeprom_begin_profile(void *r, uint32_t c, uint32_t a, uint32_t n, unsigned profile);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_expected_profile(const void *r, unsigned profile);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_request_profile(const void *r, uint8_t *o, size_t n, unsigned profile);
|
||||
PCAN_ABI_API int pcan_abi_periph28335_eeprom_block_accept_profile(void *r, const uint8_t *p, size_t n, uint8_t *o, size_t cap, unsigned profile);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_write_profile(uint32_t controller, uint32_t destination, uint32_t source, uint32_t length, unsigned profile, uint8_t *out, size_t cap);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_expected(const void *state);
|
||||
PCAN_ABI_API size_t pcan_abi_periph28335_eeprom_block_request(const void *state, uint8_t *out, size_t cap);
|
||||
PCAN_ABI_API int pcan_abi_periph28335_eeprom_block_accept(void *state, const uint8_t *reply, size_t size, uint8_t *out, size_t cap);
|
||||
|
||||
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);
|
||||
@@ -110,6 +129,9 @@ PCAN_ABI_API const char *pcan_abi_periph28335_command_name(
|
||||
/* PM67/TMS320F2812 legacy memory upload. */
|
||||
PCAN_ABI_API size_t pcan_abi_tms2812_build_fixed(uint8_t controller, uint8_t command,
|
||||
const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size);
|
||||
PCAN_ABI_API int pcan_abi_tms2812_bus_step(int write, unsigned step, unsigned tetrad, unsigned reg,
|
||||
uint16_t data, uint32_t *address, uint16_t *value, uint8_t *command);
|
||||
|
||||
PCAN_ABI_API size_t pcan_abi_tms2812_build_memory(uint8_t controller, uint8_t command,
|
||||
uint32_t word_address, uint16_t value, uint8_t *output, size_t output_size);
|
||||
PCAN_ABI_API size_t pcan_abi_tms2812_build_test(uint8_t controller, uint16_t duration,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "balsam167_eeprom_layout.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -25,6 +26,46 @@ extern "C" {
|
||||
#define PERIPH28335_REQUEST_SIZE 8U
|
||||
#define PERIPH28335_COMMAND_COUNT 17U
|
||||
|
||||
/* Balsam-167 fitted AT25256B (marking 5EBL): 256 Kbit, NOT 256 Kbyte.
|
||||
* All reader positions below count physical EEPROM octets, not C28x words.
|
||||
* Four uint32_t fields make the caller-owned state stable for ctypes/JNI. */
|
||||
#define PERIPH28335_EEPROM_BYTES BALSAM167_EEPROM_BYTES
|
||||
typedef struct {
|
||||
uint32_t controller;
|
||||
uint32_t start;
|
||||
uint32_t length;
|
||||
uint32_t received;
|
||||
} periph28335_eeprom_reader;
|
||||
|
||||
/* No writes or capacity probes. Odd starts/lengths are allowed: the core reads
|
||||
* the containing 16-bit word and returns only the requested physical octets. */
|
||||
int periph28335_eeprom_begin(periph28335_eeprom_reader *reader,
|
||||
uint32_t controller, uint32_t start, uint32_t length);
|
||||
size_t periph28335_eeprom_request(const periph28335_eeprom_reader *reader,
|
||||
uint8_t *output, size_t capacity);
|
||||
/* Returns 1/2 decoded octets, or a negative status; errors do not advance state.
|
||||
* PEEK payload = little-endian data WORD + timer WORD, not four memory bytes.
|
||||
* SPI16 shifts the data word high-octet first, so physical EEPROM order is
|
||||
* response[3], response[2]. This differs from a C28x little-endian RAM dump. */
|
||||
int periph28335_eeprom_accept(periph28335_eeprom_reader *reader,
|
||||
const uint8_t *reply, size_t size, uint8_t *output, size_t capacity);
|
||||
|
||||
/* FC03 block reader uses the same state/physical byte range as legacy PEEK.
|
||||
* expected() is the exact response length. No state advance on CRC/size error. */
|
||||
size_t periph28335_eeprom_block_expected(const periph28335_eeprom_reader *r);
|
||||
size_t periph28335_eeprom_block_request(const periph28335_eeprom_reader *r, uint8_t *out, size_t cap);
|
||||
int periph28335_eeprom_block_accept(periph28335_eeprom_reader *r,
|
||||
const uint8_t *reply, size_t size, uint8_t *out, size_t cap);
|
||||
|
||||
/* Profile 0 = AT25256B, 1 = M95M02; stable additional API for every host port.
|
||||
* State remains four uint32_t fields. M95 requires block mode, no PEEK fallback. */
|
||||
uint32_t periph28335_eeprom_capacity_profile(unsigned profile);
|
||||
int periph28335_eeprom_begin_profile(periph28335_eeprom_reader *r, uint32_t c, uint32_t a, uint32_t n, unsigned profile);
|
||||
size_t periph28335_eeprom_block_expected_profile(const periph28335_eeprom_reader *r, unsigned profile);
|
||||
size_t periph28335_eeprom_block_request_profile(const periph28335_eeprom_reader *r, uint8_t *o, size_t n, unsigned profile);
|
||||
int periph28335_eeprom_block_accept_profile(periph28335_eeprom_reader *r, const uint8_t *p, size_t n, uint8_t *o, size_t cap, unsigned profile);
|
||||
size_t periph28335_eeprom_write_profile(uint32_t controller, uint32_t destination, uint32_t source, uint32_t length, unsigned profile, uint8_t *out, size_t cap);
|
||||
|
||||
typedef enum {
|
||||
PERIPH28335_OK = 0,
|
||||
PERIPH28335_ERROR_ARGUMENT = -1,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define SET_SPECTRUM_MAX 16384U
|
||||
#define SET_SPECTRUM_MAX 2097152U
|
||||
enum { SET_WINDOW_RECT, SET_WINDOW_HANN, SET_WINDOW_HAMMING, SET_WINDOW_BLACKMAN, SET_WINDOW_FLATTOP };
|
||||
enum { SET_FILTER_NONE, SET_FILTER_LOW_PASS, SET_FILTER_HIGH_PASS, SET_FILTER_BAND_PASS, SET_FILTER_NOTCH };
|
||||
enum { SET_SPECTRUM_OK, SET_SPECTRUM_SHORT, SET_SPECTRUM_INVALID, SET_SPECTRUM_TIMING,
|
||||
|
||||
@@ -36,6 +36,9 @@ typedef enum {
|
||||
TMS2812_ERROR_CAPACITY = -6
|
||||
} tms2812_status_t;
|
||||
|
||||
int tms2812_bus_step(int write, unsigned step, unsigned tetrad, unsigned reg,
|
||||
uint16_t data, uint32_t *address, uint16_t *value, uint8_t *command);
|
||||
|
||||
uint16_t tms2812_crc16(const uint8_t *data, size_t size);
|
||||
|
||||
size_t tms2812_build_fixed_request(uint8_t controller, uint8_t command,
|
||||
|
||||
@@ -17,6 +17,10 @@ extern "C" {
|
||||
#define UMP_BASE 0x1000U
|
||||
#define UMP_DATA 0x1010U
|
||||
#define UMP_LIVE 0x1100U
|
||||
/* Read-only running firmware identity, same 12-word v1 contract as Climate.
|
||||
* Kept outside logger control/data and the EEPROM-backed legacy parameters. */
|
||||
#define UMP_FIRMWARE_INFO 0x1200U
|
||||
#define UMP_FIRMWARE_INFO_WORDS 12U
|
||||
#define UMP_MARKER 0x1F00U
|
||||
#define UMP_MAX_WORDS 124U
|
||||
#define UMP_RTU_CAPACITY 253U
|
||||
|
||||
@@ -8,7 +8,17 @@
|
||||
extern unsigned int ump_log_read(unsigned int address);
|
||||
extern int ump_log_write(unsigned int address, unsigned int value);
|
||||
static uint16_t read_word(void *context, uint16_t address)
|
||||
{ (void)context; return (uint16_t)ump_log_read(address); }
|
||||
{
|
||||
(void)context;
|
||||
/* Optional board binding to the common firmware-info static port. Older
|
||||
* consumers still link without it and return contract=0 (unknown version).
|
||||
* This hook changes neither PM67 CAN nor the saved parameter layout. */
|
||||
#ifdef UMP_FIRMWARE_INFO_READ
|
||||
if (address >= UMP_FIRMWARE_INFO && address < UMP_FIRMWARE_INFO + UMP_FIRMWARE_INFO_WORDS)
|
||||
return UMP_FIRMWARE_INFO_READ(address - UMP_FIRMWARE_INFO);
|
||||
#endif
|
||||
return (uint16_t)ump_log_read(address);
|
||||
}
|
||||
static int write_word(void *context, uint16_t address, uint16_t value)
|
||||
{ (void)context; return ump_log_write(address,value); }
|
||||
unsigned ump_tms28335_prepare(unsigned fn, uint16_t a, uint16_t n,
|
||||
|
||||
@@ -101,6 +101,17 @@ uint16_t pcan_abi_periph28335_crc16(const uint8_t *data, size_t size)
|
||||
return periph28335_crc16_modbus(data, size);
|
||||
}
|
||||
|
||||
int pcan_abi_periph28335_eeprom_begin(void *state,
|
||||
uint32_t controller, uint32_t start, uint32_t length)
|
||||
{ return periph28335_eeprom_begin((periph28335_eeprom_reader *)state, controller, start, length); }
|
||||
size_t pcan_abi_periph28335_eeprom_request(const void *state, uint8_t *out, size_t cap)
|
||||
{ return periph28335_eeprom_request((const periph28335_eeprom_reader *)state, out, cap); }
|
||||
int pcan_abi_periph28335_eeprom_accept(void *state, const uint8_t *reply,
|
||||
size_t size, uint8_t *out, size_t cap)
|
||||
{ return periph28335_eeprom_accept((periph28335_eeprom_reader *)state, reply, size, out, cap); }
|
||||
uint32_t pcan_abi_periph28335_eeprom_capacity(void)
|
||||
{ return PERIPH28335_EEPROM_BYTES; }
|
||||
|
||||
size_t pcan_abi_periph28335_append_crc(
|
||||
const uint8_t *payload, size_t payload_size,
|
||||
uint8_t *output, size_t output_size)
|
||||
@@ -368,3 +379,29 @@ int pcan_abi_gui_parser_stats(const void *parser_storage, uint32_t *frames,
|
||||
if (stray_bytes != NULL) *stray_bytes = parser->stats.stray_bytes;
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t pcan_abi_periph28335_eeprom_block_expected(const void *s)
|
||||
{ return periph28335_eeprom_block_expected((const periph28335_eeprom_reader *)s); }
|
||||
size_t pcan_abi_periph28335_eeprom_block_request(const void *s, uint8_t *out, size_t cap)
|
||||
{ return periph28335_eeprom_block_request((const periph28335_eeprom_reader *)s,out,cap); }
|
||||
int pcan_abi_periph28335_eeprom_block_accept(void *s, const uint8_t *reply, size_t size, uint8_t *out, size_t cap)
|
||||
{ return periph28335_eeprom_block_accept((periph28335_eeprom_reader *)s,reply,size,out,cap); }
|
||||
|
||||
uint32_t pcan_abi_periph28335_eeprom_capacity_profile(unsigned p)
|
||||
{ return periph28335_eeprom_capacity_profile(p); }
|
||||
int pcan_abi_periph28335_eeprom_begin_profile(void *s, uint32_t c, uint32_t a, uint32_t n, unsigned p)
|
||||
{ return periph28335_eeprom_begin_profile((periph28335_eeprom_reader *)s,c,a,n,p); }
|
||||
size_t pcan_abi_periph28335_eeprom_block_expected_profile(const void *s, unsigned p)
|
||||
{ return periph28335_eeprom_block_expected_profile((const periph28335_eeprom_reader *)s,p); }
|
||||
size_t pcan_abi_periph28335_eeprom_block_request_profile(const void *s, uint8_t *o, size_t n, unsigned p)
|
||||
{ return periph28335_eeprom_block_request_profile((const periph28335_eeprom_reader *)s,o,n,p); }
|
||||
int pcan_abi_periph28335_eeprom_block_accept_profile(void *s, const uint8_t *r, size_t n, uint8_t *o, size_t cap, unsigned p)
|
||||
{ return periph28335_eeprom_block_accept_profile((periph28335_eeprom_reader *)s,r,n,o,cap,p); }
|
||||
size_t pcan_abi_periph28335_eeprom_write_profile(uint32_t c, uint32_t d, uint32_t s, uint32_t n, unsigned p, uint8_t *o, size_t cap)
|
||||
{ return periph28335_eeprom_write_profile(c,d,s,n,p,o,cap); }
|
||||
|
||||
int pcan_abi_tms2812_bus_step(int write, unsigned step, unsigned tetrad, unsigned reg,
|
||||
uint16_t data, uint32_t *address, uint16_t *value, uint8_t *command)
|
||||
{
|
||||
return tms2812_bus_step(write, step, tetrad, reg, data, address, value, command);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,143 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "set_crc.h"
|
||||
#include "balsam167_eeprom_block.h"
|
||||
|
||||
static int eeprom_reader_valid(const periph28335_eeprom_reader *r, unsigned profile)
|
||||
|
||||
{
|
||||
return r && r->controller >= 1U && r->controller <= 255U &&
|
||||
r->start < balsam167_eeprom_capacity(profile) && r->length &&
|
||||
r->length <= balsam167_eeprom_capacity(profile) - r->start && r->received < r->length;
|
||||
}
|
||||
|
||||
int periph28335_eeprom_begin_profile(periph28335_eeprom_reader *r,
|
||||
uint32_t controller, uint32_t start, uint32_t length, unsigned profile)
|
||||
{
|
||||
if (!r) return PERIPH28335_ERROR_ARGUMENT;
|
||||
/* Invalidate first: a rejected new range must not leave an old read active. */
|
||||
memset(r, 0, sizeof(*r));
|
||||
if (!controller || controller > 255U || start >= balsam167_eeprom_capacity(profile) ||
|
||||
!length || length > balsam167_eeprom_capacity(profile) - start)
|
||||
return PERIPH28335_ERROR_RANGE;
|
||||
r->controller = controller; r->start = start; r->length = length;
|
||||
return PERIPH28335_OK;
|
||||
}
|
||||
|
||||
size_t periph28335_eeprom_request(const periph28335_eeprom_reader *r,
|
||||
uint8_t *output, size_t capacity)
|
||||
{
|
||||
uint8_t payload[6];
|
||||
uint32_t word;
|
||||
if (!eeprom_reader_valid(r, 0)) return 0;
|
||||
word = (r->start + r->received) / 2U;
|
||||
payload[0] = (uint8_t)r->controller; payload[1] = 56U; /* CMD_PEEK only */
|
||||
/* 167 routes words 0..0x3fff to SPI EEPROM. The old terminal's
|
||||
* 0x10000000 memory selector is NOT implemented by this firmware. */
|
||||
payload[2] = (uint8_t)word; payload[3] = (uint8_t)(word >> 8);
|
||||
payload[4] = 0; payload[5] = 0;
|
||||
return periph28335_append_crc(payload, sizeof(payload), output, capacity);
|
||||
}
|
||||
|
||||
int periph28335_eeprom_accept(periph28335_eeprom_reader *r,
|
||||
const uint8_t *reply, size_t size, uint8_t *output, size_t capacity)
|
||||
{
|
||||
unsigned odd, count;
|
||||
if (!eeprom_reader_valid(r, 0) || !reply || !output) return PERIPH28335_ERROR_ARGUMENT;
|
||||
/* The UART layer may strip two legacy zero-padding bytes after CRC. */
|
||||
if (size != 8U && size != 10U) return PERIPH28335_ERROR_LENGTH;
|
||||
if (periph28335_crc16_modbus(reply, 8U) != 0) return PERIPH28335_ERROR_CRC;
|
||||
if (reply[0] != r->controller || reply[1] != 56U) return PERIPH28335_ERROR_HEADER;
|
||||
odd = (unsigned)((r->start + r->received) & 1U);
|
||||
count = 2U - odd;
|
||||
if (count > r->length - r->received) count = (unsigned)(r->length - r->received);
|
||||
if (capacity < count) return PERIPH28335_ERROR_CAPACITY;
|
||||
output[0] = reply[odd ? 2U : 3U];
|
||||
if (count == 2U) output[1] = reply[2];
|
||||
r->received += count;
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
/* One common codec for all host ports. Physical EEPROM bytes already have
|
||||
* network-word order; only trim an odd first/last byte, never swap them. */
|
||||
static unsigned eeprom_block_words(const periph28335_eeprom_reader *r, unsigned profile)
|
||||
{
|
||||
uint32_t n;
|
||||
if (!eeprom_reader_valid(r, profile)) return 0;
|
||||
n = (r->length - r->received + ((r->start + r->received) & 1U) + 1U) / 2U;
|
||||
/* A response cannot cross a bank boundary; trim the last block there. */
|
||||
if (n > 32768U - ((r->start+r->received)/2U % 32768U))
|
||||
n = 32768U - ((r->start+r->received)/2U % 32768U);
|
||||
return (unsigned)(n > BALSAM167_EEPROM_BLOCK_WORDS ? BALSAM167_EEPROM_BLOCK_WORDS : n);
|
||||
}
|
||||
size_t periph28335_eeprom_block_expected_profile(const periph28335_eeprom_reader *r, unsigned profile)
|
||||
{
|
||||
unsigned n = eeprom_block_words(r, profile);
|
||||
return n ? 5U + 2U*n : 0;
|
||||
}
|
||||
size_t periph28335_eeprom_block_request_profile(const periph28335_eeprom_reader *r, uint8_t *out, size_t cap, unsigned profile)
|
||||
{
|
||||
uint8_t p[6];
|
||||
unsigned n = eeprom_block_words(r, profile), a, quantity;
|
||||
uint32_t word;
|
||||
if (!n) return 0;
|
||||
word = (r->start + r->received) / 2U;
|
||||
a = 0x8000U + (unsigned)(word % 32768U);
|
||||
quantity = n | (profile ? (0x8000U | (unsigned)(word/32768U)*256U) : 0U);
|
||||
if (!balsam167_eeprom_block_valid_profile(3,a,quantity,profile)) return 0;
|
||||
p[0]=(uint8_t)r->controller; p[1]=3;
|
||||
p[2]=(uint8_t)(a>>8); p[3]=(uint8_t)a; p[4]=(uint8_t)(quantity>>8); p[5]=(uint8_t)quantity;
|
||||
return periph28335_append_crc(p,6,out,cap);
|
||||
}
|
||||
int periph28335_eeprom_block_accept_profile(periph28335_eeprom_reader *r,
|
||||
const uint8_t *reply, size_t size, uint8_t *out, size_t cap, unsigned profile)
|
||||
{
|
||||
unsigned n=eeprom_block_words(r, profile), odd, count;
|
||||
if (!n || !reply || !out) return PERIPH28335_ERROR_ARGUMENT;
|
||||
if (size != 5U+2U*n) return PERIPH28335_ERROR_LENGTH;
|
||||
if (periph28335_crc16_modbus(reply,size)) return PERIPH28335_ERROR_CRC;
|
||||
/* Common address 16 is answered by the board's own Mode, 5 or 6. */
|
||||
if ((reply[0] != r->controller && !(r->controller==16 && (reply[0]==5 || reply[0]==6))) ||
|
||||
reply[1]!=3 || reply[2]!=2U*n) return PERIPH28335_ERROR_HEADER;
|
||||
odd=(unsigned)((r->start+r->received)&1U);
|
||||
count=2U*n-odd;
|
||||
if (count>r->length-r->received) count=(unsigned)(r->length-r->received);
|
||||
if (cap<count) return PERIPH28335_ERROR_CAPACITY;
|
||||
memcpy(out,reply+3+odd,count);
|
||||
r->received+=count;
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
/* Keep all existing exported symbols and the 16-byte reader ABI unchanged. */
|
||||
int periph28335_eeprom_begin(periph28335_eeprom_reader *r, uint32_t c, uint32_t a, uint32_t n)
|
||||
{ return periph28335_eeprom_begin_profile(r,c,a,n,0); }
|
||||
size_t periph28335_eeprom_block_expected(const periph28335_eeprom_reader *r)
|
||||
{ return periph28335_eeprom_block_expected_profile(r,0); }
|
||||
size_t periph28335_eeprom_block_request(const periph28335_eeprom_reader *r, uint8_t *o, size_t n)
|
||||
{ return periph28335_eeprom_block_request_profile(r,o,n,0); }
|
||||
int periph28335_eeprom_block_accept(periph28335_eeprom_reader *r, const uint8_t *p, size_t n, uint8_t *o, size_t cap)
|
||||
{ return periph28335_eeprom_block_accept_profile(r,p,n,o,cap,0); }
|
||||
uint32_t periph28335_eeprom_capacity_profile(unsigned profile)
|
||||
{ return (uint32_t)balsam167_eeprom_capacity(profile); }
|
||||
/* Legacy EXTEND carries word addresses and octet lengths. A distinct M95
|
||||
* subcommand makes a wrong-profile write fail closed on either firmware. */
|
||||
size_t periph28335_eeprom_write_profile(uint32_t controller, uint32_t destination,
|
||||
uint32_t source, uint32_t length, unsigned profile, uint8_t *out, size_t cap)
|
||||
{
|
||||
uint8_t p[16]; unsigned i;
|
||||
uint32_t capacity = periph28335_eeprom_capacity_profile(profile);
|
||||
uint32_t limit = capacity ? capacity-(BALSAM167_EEPROM_PARAM_WORDS+1U)*2U : 0;
|
||||
if (!controller || controller>255U || !length || length>199993U ||
|
||||
destination>=limit/2U || length>limit-destination*2U) return 0;
|
||||
p[0]=(uint8_t)controller; p[1]=60;
|
||||
for(i=0;i<4;i++) {
|
||||
p[2+i]=(uint8_t)(destination>>(8*i));
|
||||
p[6+i]=(uint8_t)(source>>(8*i));
|
||||
p[10+i]=(uint8_t)(length>>(8*i));
|
||||
}
|
||||
p[14]=(uint8_t)(profile ? BALSAM167_EEPROM_WRITE_M95 : 4); p[15]=0;
|
||||
return periph28335_append_crc(p,16,out,cap);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *project;
|
||||
|
||||
@@ -137,3 +137,16 @@ int tms2812_decode_upload_response(
|
||||
memcpy(output, &data[2], byte_count);
|
||||
return TMS2812_OK;
|
||||
}
|
||||
|
||||
int tms2812_bus_step(int write, unsigned step, unsigned tetrad, unsigned reg,
|
||||
uint16_t data, uint32_t *address, uint16_t *value, uint8_t *command)
|
||||
{
|
||||
if (!address || !value || !command) return TMS2812_ERROR_ARGUMENT;
|
||||
if ((write != 0 && write != 1) || step > 1U || tetrad > 14U || reg > 15U)
|
||||
return TMS2812_ERROR_RANGE;
|
||||
*command = (!write && step == 1U) ? TMS2812_CMD_PEEK : TMS2812_CMD_POKE;
|
||||
*address = write ? (step == 0U ? 0x200AU : 0x200BU) : (step == 0U ? 0x200BU : 0x200FU);
|
||||
*value = write && step == 0U ? data : (!write && step == 1U ? 0U :
|
||||
(uint16_t)((write ? 0x8000U : 0U) | (tetrad << 4) | reg));
|
||||
return TMS2812_OK;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ unsigned ump_validate(unsigned fn, uint16_t a, uint16_t n, int can)
|
||||
{
|
||||
if (fn == 6) return a >= UMP_BASE && a <= UMP_BASE+3 ? 0 : 2;
|
||||
if (fn != 3) return 1;
|
||||
/* CAN reads the whole immutable block (six fragments); RTU may read slices.
|
||||
* Check subtraction only after the lower bound, avoiding uint16 wraparound. */
|
||||
if (a >= UMP_FIRMWARE_INFO && a < UMP_FIRMWARE_INFO + UMP_FIRMWARE_INFO_WORDS)
|
||||
return can ? ((a == UMP_FIRMWARE_INFO && n == UMP_FIRMWARE_INFO_WORDS) ? 0 : 2)
|
||||
: ((n && n <= UMP_FIRMWARE_INFO + UMP_FIRMWARE_INFO_WORDS - a) ? 0 : 2);
|
||||
if (can) return ((a == UMP_BASE && n == 16) ||
|
||||
(a == UMP_DATA && (n == 40 || n == 80 || n == 120 || n == 42 || n == 84)) ||
|
||||
(a == UMP_LIVE && n == 124)) ? 0 : 2;
|
||||
|
||||
156
c/set-protocol/tests/test_periph28335_eeprom.c
Normal file
156
c/set-protocol/tests/test_periph28335_eeprom.c
Normal file
@@ -0,0 +1,156 @@
|
||||
#include "periph28335.h"
|
||||
#include <assert.h>
|
||||
#include "balsam167_eeprom_block.h"
|
||||
#include <string.h>
|
||||
|
||||
static void response(uint8_t *out, unsigned high, unsigned low)
|
||||
{
|
||||
/* Reply is low data byte, high data byte, then an unrelated timer.
|
||||
* Nonzero timer detects accidentally copying four memory bytes per PEEK. */
|
||||
uint8_t payload[] = {16,56,0,0,0xef,0xbe};
|
||||
payload[2]=(uint8_t)low; payload[3]=(uint8_t)high;
|
||||
assert(periph28335_append_crc(payload,6,out,10)==8);
|
||||
out[8]=out[9]=0;
|
||||
}
|
||||
|
||||
static void blocks(void)
|
||||
{
|
||||
periph28335_eeprom_reader r;
|
||||
uint8_t req[8], reply[245], out[240];
|
||||
unsigned start, transactions, n, a, i, old;
|
||||
int got;
|
||||
assert(!balsam167_eeprom_block_valid(6,0x8000,1));
|
||||
assert(!balsam167_eeprom_block_valid(3,0xbfff,2));
|
||||
assert(!balsam167_eeprom_block_valid(3,0xffff,120));
|
||||
assert(!balsam167_eeprom_block_valid(3,0x8000,121));
|
||||
assert(!balsam167_eeprom_block_valid(3,0x8000,0));
|
||||
assert(balsam167_eeprom_block_valid(3,0xbfff,1));
|
||||
for(start=0;start<2;start++) {
|
||||
assert(!periph28335_eeprom_begin(&r,16,start,32768-start));
|
||||
transactions=0;
|
||||
while(r.received<r.length) {
|
||||
assert(periph28335_eeprom_block_request(&r,req,8)==8);
|
||||
assert(!periph28335_crc16_modbus(req,8));
|
||||
a=((unsigned)req[2]*256+req[3]-0x8000)*2;
|
||||
n=req[5]; assert(n && n<=120 && a+2*n<=32768);
|
||||
reply[0]=5; reply[1]=3; reply[2]=(uint8_t)(2*n);
|
||||
for(i=0;i<2*n;i++) reply[3+i]=(uint8_t)(a+i);
|
||||
assert(periph28335_append_crc(reply,3+2*n,reply,sizeof reply)==5+2*n);
|
||||
old=r.received;
|
||||
reply[4]^=1;
|
||||
assert(periph28335_eeprom_block_accept(&r,reply,5+2*n,out,240)==PERIPH28335_ERROR_CRC);
|
||||
assert(r.received==old); reply[4]^=1;
|
||||
assert(periph28335_eeprom_block_accept(&r,reply,5+2*n,out,0)==PERIPH28335_ERROR_CAPACITY);
|
||||
assert(r.received==old);
|
||||
got=periph28335_eeprom_block_accept(&r,reply,5+2*n,out,240);
|
||||
assert(got>0);
|
||||
for(i=0;i<(unsigned)got;i++) assert(out[i]==(uint8_t)(start+old+i));
|
||||
transactions++;
|
||||
}
|
||||
assert(transactions==137);
|
||||
assert(!periph28335_eeprom_block_request(&r,req,8));
|
||||
}
|
||||
}
|
||||
|
||||
static void profiles(void)
|
||||
{
|
||||
periph28335_eeprom_reader r;
|
||||
uint8_t req[18], reply[245], out[240];
|
||||
unsigned n, i, q, a, bankmask=0;
|
||||
uint32_t pos, before;
|
||||
assert(periph28335_eeprom_capacity_profile(0)==32768);
|
||||
assert(periph28335_eeprom_capacity_profile(1)==262144);
|
||||
assert(periph28335_eeprom_capacity_profile(2)==0);
|
||||
assert(periph28335_eeprom_begin_profile(&r,16,262143,2,1)<0);
|
||||
assert(periph28335_eeprom_begin_profile(&r,16,0,2,2)<0);
|
||||
assert(!balsam167_eeprom_block_valid_profile(3,0xffff,0x8078,1));
|
||||
assert(!balsam167_eeprom_block_valid_profile(3,0x8000,0x8401,1));
|
||||
assert(!balsam167_eeprom_block_valid_profile(3,0x8000,0x8001,0));
|
||||
assert(!balsam167_eeprom_block_valid_profile(3,0x8000,1,1));
|
||||
assert(balsam167_eeprom_block_valid_profile(3,0xffff,0x8301,1));
|
||||
/* Full 256-KiB dump from an odd start exercises every bank transition,
|
||||
* first/last byte trimming, CRC failure and exact physical byte order. */
|
||||
assert(!periph28335_eeprom_begin_profile(&r,16,1,262143,1));
|
||||
while(r.received<r.length) {
|
||||
assert(periph28335_eeprom_block_request_profile(&r,req,8,1)==8);
|
||||
assert(!periph28335_crc16_modbus(req,8));
|
||||
a=req[2]*256U+req[3]; q=req[4]*256U+req[5]; n=q&255U;
|
||||
assert(balsam167_eeprom_block_valid_profile(3,a,q,1));
|
||||
pos=balsam167_eeprom_block_word(a,q)*2UL;
|
||||
assert(pos == (r.start+r.received)/2*2);
|
||||
bankmask |= 1U << ((q>>8)&3U);
|
||||
assert(periph28335_eeprom_block_expected_profile(&r,1)==5+2*n);
|
||||
reply[0]=5; reply[1]=3; reply[2]=(uint8_t)(n*2);
|
||||
for(i=0;i<n*2;i++) reply[3+i]=(uint8_t)((pos+i)^((pos+i)>>16));
|
||||
periph28335_append_crc(reply,3+n*2,reply,sizeof(reply));
|
||||
before=r.received; reply[4]^=1;
|
||||
assert(periph28335_eeprom_block_accept_profile(&r,reply,5+2*n,out,240,1)==PERIPH28335_ERROR_CRC);
|
||||
assert(r.received==before); reply[4]^=1;
|
||||
assert(periph28335_eeprom_block_accept_profile(&r,reply,5+2*n,out,240,1)>0);
|
||||
for(i=0;i<r.received-before;i++) {
|
||||
pos=1+before+i;
|
||||
assert(out[i]==(uint8_t)(pos^(pos>>16)));
|
||||
}
|
||||
}
|
||||
assert(bankmask==15);
|
||||
assert(!periph28335_eeprom_block_request_profile(&r,req,8,1));
|
||||
assert(periph28335_eeprom_write_profile(5,0,0x200000,33514,0,req,18)==0);
|
||||
assert(periph28335_eeprom_write_profile(5,0,0x200000,33514,1,req,18)==18);
|
||||
assert(req[14]==0x44 && req[15]==0 && !periph28335_crc16_modbus(req,18));
|
||||
assert(!periph28335_eeprom_write_profile(5,0x1ff7e,0,2,1,req,18));
|
||||
assert(!periph28335_eeprom_write_profile(5,0xffffffff,0,2,1,req,18));
|
||||
assert(periph28335_eeprom_write_profile(5,0,0,32438,0,req,18)==18 && req[14]==4);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
periph28335_eeprom_reader reader;
|
||||
blocks();
|
||||
profiles();
|
||||
uint8_t request[8], reply[10], data[2];
|
||||
unsigned i;
|
||||
assert(sizeof(reader)==16);
|
||||
assert(periph28335_eeprom_begin(&reader,16,0,0)==PERIPH28335_ERROR_RANGE);
|
||||
assert(periph28335_eeprom_begin(&reader,0,0,2)==PERIPH28335_ERROR_RANGE);
|
||||
assert(periph28335_eeprom_begin(&reader,16,0x8000,1)==PERIPH28335_ERROR_RANGE);
|
||||
assert(periph28335_eeprom_begin(&reader,16,0x7fff,2)==PERIPH28335_ERROR_RANGE);
|
||||
assert(periph28335_eeprom_begin(&reader,16,1,0xffffffffUL)==PERIPH28335_ERROR_RANGE);
|
||||
assert(periph28335_eeprom_request(&reader,request,8)==0);
|
||||
assert(periph28335_eeprom_begin(&reader,16,1,3)==0);
|
||||
assert(periph28335_eeprom_request(&reader,request,7)==0);
|
||||
assert(periph28335_eeprom_request(&reader,request,8)==8);
|
||||
assert(memcmp(request,"\x10\x38\0\0\0\0",6)==0);
|
||||
response(reply,0x12,0x34);
|
||||
assert(periph28335_eeprom_accept(&reader,reply,8,data,2)==1 && data[0]==0x34);
|
||||
assert(reader.received==1);
|
||||
assert(periph28335_eeprom_request(&reader,request,8)==8 && request[2]==1);
|
||||
reply[2]^=1;
|
||||
assert(periph28335_eeprom_accept(&reader,reply,8,data,2)==PERIPH28335_ERROR_CRC);
|
||||
assert(reader.received==1);
|
||||
response(reply,0x56,0x78);
|
||||
assert(periph28335_eeprom_accept(&reader,reply,8,data,1)==PERIPH28335_ERROR_CAPACITY);
|
||||
assert(reader.received==1);
|
||||
assert(periph28335_eeprom_accept(&reader,reply,10,data,2)==2);
|
||||
assert(data[0]==0x56 && data[1]==0x78 && reader.received==3);
|
||||
assert(periph28335_eeprom_request(&reader,request,8)==0);
|
||||
|
||||
/* Every requested word of a complete dump remains in the EEPROM window.
|
||||
* In particular, neither the RAM route nor selector 0x10000000 is used. */
|
||||
assert(periph28335_eeprom_begin(&reader,16,0,32768)==0);
|
||||
for (i=0;i<16384;i++) {
|
||||
assert(periph28335_eeprom_request(&reader,request,8)==8);
|
||||
assert(request[1]==56 && request[4]==0 && request[5]==0);
|
||||
assert(((unsigned)request[3]*256+request[2])==i);
|
||||
assert(periph28335_crc16_modbus(request,8)==0);
|
||||
response(reply,i>>8,i);
|
||||
assert(periph28335_eeprom_accept(&reader,reply,8,data,2)==2);
|
||||
assert(data[0]==(i>>8) && data[1]==(i&255));
|
||||
}
|
||||
assert(reader.received==32768 && periph28335_eeprom_request(&reader,request,8)==0);
|
||||
assert(periph28335_eeprom_begin(&reader,16,0x7fff,1)==0);
|
||||
assert(periph28335_eeprom_request(&reader,request,8)==8);
|
||||
assert(request[2]==0xff && request[3]==0x3f);
|
||||
response(reply,0x9a,0xbc);
|
||||
assert(periph28335_eeprom_accept(&reader,reply,8,data,2)==1 && data[0]==0xbc);
|
||||
return 0;
|
||||
}
|
||||
@@ -18,6 +18,19 @@ static int test_terminal(void)
|
||||
uint8_t reply[] = {0x0a, 0x38, 0x65, 0xa5, 0, 0, 0xaf, 0x9b, 0, 0};
|
||||
const uint8_t test_prefix[] = {7, 66, 25, 0, 18, 0};
|
||||
size_t size;
|
||||
uint32_t bus_address;
|
||||
uint16_t bus_value;
|
||||
uint8_t bus_command;
|
||||
if (tms2812_bus_step(1, 0, 3, 5, 0x1234, &bus_address, &bus_value, &bus_command) ||
|
||||
bus_address != 0x200AU || bus_value != 0x1234 || bus_command != 57) return 20;
|
||||
if (tms2812_bus_step(1, 1, 3, 5, 0x1234, &bus_address, &bus_value, &bus_command) ||
|
||||
bus_address != 0x200BU || bus_value != 0x8035 || bus_command != 57) return 21;
|
||||
if (tms2812_bus_step(0, 0, 3, 5, 0, &bus_address, &bus_value, &bus_command) ||
|
||||
bus_address != 0x200BU || bus_value != 0x35 || bus_command != 57) return 22;
|
||||
if (tms2812_bus_step(0, 1, 3, 5, 0, &bus_address, &bus_value, &bus_command) ||
|
||||
bus_address != 0x200FU || bus_command != 56) return 23;
|
||||
if (tms2812_bus_step(0, 0, 15, 0, 0, &bus_address, &bus_value, &bus_command) != TMS2812_ERROR_RANGE ||
|
||||
tms2812_bus_step(0, 2, 0, 0, 0, &bus_address, &bus_value, &bus_command) != TMS2812_ERROR_RANGE) return 24;
|
||||
if (tms2812_build_memory_request(10, 0, 0, 0, output, sizeof output) != 8U ||
|
||||
memcmp(output, ping, sizeof ping) != 0) return 10;
|
||||
if (tms2812_build_memory_request(10, TMS2812_CMD_POKE, 0x12345678UL, 0xABCD,
|
||||
|
||||
4
cpp/qcustomplot-renderer/.gitignore
vendored
Normal file
4
cpp/qcustomplot-renderer/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
release/
|
||||
debug/
|
||||
Makefile*
|
||||
.qmake.stash
|
||||
674
cpp/qcustomplot-renderer/LICENSE.txt
Normal file
674
cpp/qcustomplot-renderer/LICENSE.txt
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
46
cpp/qcustomplot-renderer/main.cpp
Normal file
46
cpp/qcustomplot-renderer/main.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#include "qcustomplot.h"
|
||||
#include <QApplication>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QBuffer>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <QHash>
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
int main(int argc, char **argv) {
|
||||
_setmode(_fileno(stdout), _O_BINARY);
|
||||
QApplication app(argc,argv);
|
||||
QCustomPlot plot;plot.setNotAntialiasedElements(QCP::aeAll);
|
||||
plot.setBackground(Qt::transparent);
|
||||
plot.axisRect()->setBackground(Qt::transparent);
|
||||
plot.axisRect()->setAutoMargins(QCP::msNone);
|
||||
plot.axisRect()->setMargins(QMargins(0,0,0,0));
|
||||
plot.plotLayout()->setMargins(QMargins(0,0,0,0));
|
||||
for(auto axis: plot.axisRect()->axes()) {axis->setVisible(false);axis->grid()->setVisible(false);}
|
||||
auto graph=plot.addGraph(); graph->setAdaptiveSampling(true);
|
||||
QHash<QString,QSharedPointer<QCPGraphDataContainer>> records;
|
||||
std::string line;
|
||||
while(std::getline(std::cin,line)) {
|
||||
auto o=QJsonDocument::fromJson(QByteArray::fromStdString(line)).object();
|
||||
auto path=o["file"].toString();
|
||||
if(!records.contains(path)) {
|
||||
QFile f(path); if(!f.open(QIODevice::ReadOnly)) return 2;
|
||||
int n=o["count"].toInt(); QByteArray bytes=f.readAll();
|
||||
if(bytes.size()!=qint64(n)*16) return 3;
|
||||
QVector<double> x(n),y(n); memcpy(x.data(),bytes.constData(),n*8);memcpy(y.data(),bytes.constData()+n*8,n*8);
|
||||
auto data=QSharedPointer<QCPGraphDataContainer>::create();
|
||||
QVector<QCPGraphData> points(n);for(int i=0;i<n;i++)points[i]=QCPGraphData(x[i],y[i]);
|
||||
data->set(points,false);records.insert(path,data);
|
||||
if(records.size()>4) { auto old=records.begin();if(old.key()==path)++old;records.erase(old); }
|
||||
}
|
||||
graph->setData(records[path]);graph->setPen(QPen(QColor(o["color"].toString()),1));
|
||||
plot.xAxis->setRange(o["left"].toDouble(),o["right"].toDouble());
|
||||
plot.yAxis->setRange(o["bottom"].toDouble(),o["top"].toDouble());
|
||||
int w=o["width"].toInt(),h=o["height"].toInt();
|
||||
plot.resize(w,h);
|
||||
QByteArray png;QBuffer buf(&png);buf.open(QIODevice::WriteOnly);plot.toPixmap(w,h,1.).save(&buf,"PNG");
|
||||
std::cout<<png.size()<<"\n";std::cout.write(png.constData(),png.size());std::cout.flush();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
35539
cpp/qcustomplot-renderer/qcustomplot.cpp
Normal file
35539
cpp/qcustomplot-renderer/qcustomplot.cpp
Normal file
File diff suppressed because it is too large
Load Diff
7781
cpp/qcustomplot-renderer/qcustomplot.h
Normal file
7781
cpp/qcustomplot-renderer/qcustomplot.h
Normal file
File diff suppressed because it is too large
Load Diff
6
cpp/qcustomplot-renderer/renderer.pro
Normal file
6
cpp/qcustomplot-renderer/renderer.pro
Normal file
@@ -0,0 +1,6 @@
|
||||
QT += widgets printsupport
|
||||
CONFIG += c++17 console release
|
||||
CONFIG -= app_bundle
|
||||
SOURCES += main.cpp qcustomplot.cpp
|
||||
HEADERS += qcustomplot.h
|
||||
TARGET = qcp-renderer
|
||||
@@ -1,4 +1,4 @@
|
||||
# Tektronix DPO4034
|
||||
# Tektronix DPO4034 / DPO4034B / MDO4034C
|
||||
|
||||
Независимый от Qt драйвер: `python/set_devices/tektronix.py`.
|
||||
Установка: `pip install "setcorp-templates[tektronix]"` из пакета templates.
|
||||
@@ -18,7 +18,7 @@
|
||||
## Реализованный обмен
|
||||
|
||||
Команды SCPI с LF. После открытия `*IDN?` проверяет производителя и точную
|
||||
модель DPO4034; `HEADER OFF` отключает заголовки ответов.
|
||||
модель DPO4034, DPO4034B или MDO4034C; `HEADER OFF` отключает заголовки ответов.
|
||||
|
||||
Для снимка запрашивается `ACQUIRE:STATE?`, затем отправляется
|
||||
`ACQUIRE:STATE STOP` и ожидается `*OPC?`. Это позволяет читать несколько
|
||||
@@ -42,11 +42,11 @@
|
||||
Проверяются конечность коэффициентов, положительный шаг времени и число
|
||||
полученных отсчётов. В `finally` восстанавливается RUN, если до снимка сбор
|
||||
шёл; исходное состояние STOP сохраняется. При ошибке UI закрывает сеанс.
|
||||
Тайм-аут операции VISA — 5 с. Драйвер не сбрасывает настройки прибора.
|
||||
Тайм-аут подключения и операции VISA — 15 с. Драйвер не сбрасывает настройки прибора.
|
||||
|
||||
Снимок читает текущую запись, не ожидает нового аппаратного триггера.
|
||||
DATA:STOP ограничивает начало записи, а не прореживает всю запись.
|
||||
В UI предел 250 000 точек на канал (1 млн суммарно).
|
||||
В UI предел 2 000 000 точек на канал. Длина аппаратной записи задаётся отдельно через HORIZONTAL:RECORDLENGTH; после применения считывается фактическое значение.
|
||||
|
||||
## Пример
|
||||
|
||||
@@ -65,3 +65,6 @@ finally:
|
||||
[MSO4000 / DPO4000 Programmer Manual, 077024801](https://download.tek.com/manual/077024801web.pdf).
|
||||
Проверка с реальным прибором требуется для обоих интерфейсов; офлайн-тесты
|
||||
проверяют последовательность команд, масштабирование, ошибки и восстановление RUN.
|
||||
|
||||
MDO4034C проверен на реальном приборе по Ethernet: идентификация и чтение CH1 (10 000 точек).
|
||||
Команды MDO4034C: [Programmer Manual](https://download.tek.com/manual/MDO4000-B-C-MSO-DPO4000B-and-MDO3000-Oscilloscope-Programmer-Manual-077051007.pdf).
|
||||
|
||||
91
python/protocan/periph28335_eeprom.py
Normal file
91
python/protocan/periph28335_eeprom.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Thin binding to the Balsam-167 physical EEPROM reader in the shared C core.
|
||||
|
||||
No address conversion, endian/CRC codec or memory-size inference in this port.
|
||||
The caller owns serial scheduling, timeouts, cancellation and dump file storage.
|
||||
"""
|
||||
import ctypes as ct
|
||||
from .native import get_native_protocol, NativeProtocolUnavailable
|
||||
|
||||
|
||||
class _State(ct.Structure):
|
||||
_fields_ = [(name, ct.c_uint32) for name in ("controller", "start", "length", "received")]
|
||||
|
||||
|
||||
def _api(name, args, result):
|
||||
try:
|
||||
function = getattr(get_native_protocol().lib, "pcan_abi_periph28335_eeprom_" + name)
|
||||
except AttributeError as error:
|
||||
raise NativeProtocolUnavailable("Обновите templates/setprotocol.dll: нет API чтения EEPROM 167") from error
|
||||
function.argtypes, function.restype = args, result
|
||||
return function
|
||||
|
||||
|
||||
def capacity(m95m02=False):
|
||||
return int(_api("capacity_profile", [ct.c_uint], ct.c_uint32)(int(m95m02)))
|
||||
|
||||
|
||||
def write_request(controller, destination, source, length, *, m95m02=False):
|
||||
values = (controller, destination, source, length)
|
||||
if any(type(v) is not int or not 0 <= v <= 0xffffffff for v in values):
|
||||
raise ValueError("Адрес или длина вне диапазона uint32")
|
||||
output = ct.create_string_buffer(18)
|
||||
size = _api("write_profile", [ct.c_uint32]*4 + [ct.c_uint, ct.c_void_p, ct.c_size_t], ct.c_size_t)(
|
||||
*values, int(m95m02), output, len(output))
|
||||
if not size:
|
||||
raise ValueError("LengthLoad или адрес выходит за область прошивки выбранной EEPROM либо пересекает область параметров. "
|
||||
"Предел от адреса 0: AT25256B — 32508 байт, M95M02 — 261884 байт.")
|
||||
return output.raw[:size]
|
||||
|
||||
|
||||
class Reader:
|
||||
def __init__(self, controller, start, length, *, block=False, m95m02=False):
|
||||
self.m95m02 = bool(m95m02)
|
||||
if m95m02 and not block:
|
||||
raise ValueError("Для M95M02 необходимо блочное чтение")
|
||||
self.block = block
|
||||
for value in (controller, start, length):
|
||||
if type(value) is not int or not 0 <= value <= 0xffffffff:
|
||||
raise ValueError("Адрес и длина должны быть неотрицательными целыми числами")
|
||||
self._state = _State()
|
||||
result = _api("begin_profile", [ct.c_void_p, ct.c_uint32, ct.c_uint32, ct.c_uint32, ct.c_uint], ct.c_int)(
|
||||
ct.byref(self._state), controller, start, length, int(self.m95m02))
|
||||
if result:
|
||||
raise ValueError(f"Диапазон вне выбранной EEPROM: 0…0x{capacity(self.m95m02)-1:X}; длина должна быть больше нуля")
|
||||
if block:
|
||||
# Detect an old DLL before the GUI acquires ownership of the port.
|
||||
_api("block_request_profile", [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_uint], ct.c_size_t)
|
||||
_api("block_accept_profile", [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t, ct.c_uint], ct.c_int)
|
||||
self.expected
|
||||
|
||||
@property
|
||||
def received(self): return int(self._state.received)
|
||||
@property
|
||||
def length(self): return int(self._state.length)
|
||||
@property
|
||||
def start(self): return int(self._state.start)
|
||||
|
||||
@property
|
||||
def expected(self):
|
||||
if not self.block: return 8
|
||||
return int(_api("block_expected_profile", [ct.c_void_p, ct.c_uint], ct.c_size_t)(ct.byref(self._state), int(self.m95m02)))
|
||||
|
||||
def request(self):
|
||||
output = ct.create_string_buffer(8)
|
||||
args = [ct.c_void_p, ct.c_void_p, ct.c_size_t] + ([ct.c_uint] if self.block else [])
|
||||
extra = [int(self.m95m02)] if self.block else []
|
||||
size = _api("block_request_profile" if self.block else "request", args, ct.c_size_t)(
|
||||
ct.byref(self._state), output, len(output), *extra)
|
||||
return output.raw[:size]
|
||||
|
||||
def accept(self, reply):
|
||||
output = ct.create_string_buffer(240 if self.block else 2)
|
||||
reply = bytes(reply)
|
||||
args = [ct.c_void_p, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t] + ([ct.c_uint] if self.block else [])
|
||||
extra = [int(self.m95m02)] if self.block else []
|
||||
size = _api("block_accept_profile" if self.block else "accept", args, ct.c_int)(
|
||||
ct.byref(self._state), reply, len(reply), output, len(output), *extra)
|
||||
if size < 0:
|
||||
errors = {-3: "Неверная длина ответа EEPROM", -4: "Ошибка CRC ответа EEPROM",
|
||||
-5: "Неверный адрес контроллера или команда ответа EEPROM"}
|
||||
raise ValueError(errors.get(size, "Ответ EEPROM не соответствует активному чтению"))
|
||||
return output.raw[:size]
|
||||
@@ -9,6 +9,7 @@ See c/set-protocol/include/set_spectrum.h for filtering and normalization semant
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import ctypes
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
|
||||
@@ -63,11 +64,40 @@ class NativeSpectrum:
|
||||
self._peak.restype = ctypes.c_int
|
||||
|
||||
def analyze(self, times, values, *, max_size=4096, window=Window.HANN, filter=Filter.NONE,
|
||||
low_hz=10.0, high_hz=100.0, remove_mean=True) -> Spectrum:
|
||||
if type(max_size) is not int or not 16 <= max_size <= 16384 or max_size & (max_size - 1):
|
||||
raise ValueError("FFT size must be a power of two in 16..16384")
|
||||
low_hz=10.0, high_hz=100.0, remove_mean=True, full_record=False) -> Spectrum:
|
||||
if type(max_size) is not int or not 16 <= max_size <= 2097152 or max_size & (max_size - 1):
|
||||
raise ValueError("FFT size must be a power of two in 16..2097152")
|
||||
if len(times) != len(values):
|
||||
raise ValueError("Timestamp and value counts differ")
|
||||
if full_record and len(times) >= 16:
|
||||
source_count = len(times)
|
||||
n = 1 << (min(source_count, max_size).bit_length() - 1)
|
||||
dt = (times[-1] - times[0]) / (source_count - 1)
|
||||
if not math.isfinite(dt) or dt <= 0:
|
||||
raise ValueError("Invalid spectrum timestamps")
|
||||
if any(not math.isfinite(value) for value in values) or any(
|
||||
not math.isfinite(times[i]) or abs((times[i] - times[i - 1]) / dt - 1) > .05
|
||||
for i in range(1, source_count)):
|
||||
raise ValueError("Full-record FFT requires uniformly spaced finite samples")
|
||||
# Integrate piecewise-constant samples over equal fractional bins.
|
||||
# Unlike picking every kth sample, this averages before decimation.
|
||||
width = source_count / n
|
||||
averaged = []
|
||||
index = 0
|
||||
position = 0.
|
||||
for output_index in range(n):
|
||||
end = (output_index + 1) * width
|
||||
total = 0.
|
||||
while position < end and index < source_count:
|
||||
stop = min(end, index + 1.)
|
||||
total += values[index] * (stop - position)
|
||||
position = stop
|
||||
if position >= index + 1:
|
||||
index += 1
|
||||
averaged.append(total / width)
|
||||
start = times[0]
|
||||
times = [start + ((i + .5) * width - .5) * dt for i in range(n)]
|
||||
values = averaged
|
||||
count = min(len(times), max_size)
|
||||
origin = times[len(times) - count] if count else 0.0
|
||||
t = (ctypes.c_double * count)(*(value - origin for value in times[-count:])) if count else (ctypes.c_double * 0)()
|
||||
|
||||
@@ -136,3 +136,22 @@ def memory_response(raw, controller, command):
|
||||
return None
|
||||
return data
|
||||
return None
|
||||
|
||||
|
||||
def bus_steps(write, tetrad, register, data=0):
|
||||
"""Two bus register operations planned by the shared C core."""
|
||||
_range(tetrad, 14, "Адрес шины")
|
||||
_range(register, 15, "Регистр шины")
|
||||
_range(data, 0xFFFF, "Данные шины")
|
||||
function = _api("bus_step", [ct.c_int, ct.c_uint, ct.c_uint, ct.c_uint,
|
||||
ct.c_uint16, ct.POINTER(ct.c_uint32), ct.POINTER(ct.c_uint16),
|
||||
ct.POINTER(ct.c_uint8)], ct.c_int)
|
||||
result = []
|
||||
for step in range(2):
|
||||
address, value, command = ct.c_uint32(), ct.c_uint16(), ct.c_uint8()
|
||||
status = function(bool(write), step, tetrad, register, data,
|
||||
ct.byref(address), ct.byref(value), ct.byref(command))
|
||||
if status:
|
||||
raise ValueError(f"Ошибка планирования шины: {status}")
|
||||
result.append((address.value, None if command.value == PEEK else value.value))
|
||||
return result
|
||||
|
||||
@@ -15,7 +15,7 @@ saleae = ["logic2-automation>=1.0.7,<2"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["firmware_image*", "altera_logic*", "logic_analyzer*", "set_devices*", "protocan*", "setprotocol*"]
|
||||
include = ["firmware_image*", "altera_logic*", "logic_analyzer*", "set_devices*", "protocan*", "setprotocol*", "pysetterminal*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
set_devices = ["gas_logger_maps/*.json"]
|
||||
|
||||
5
python/pysetterminal/.gitignore
vendored
Normal file
5
python/pysetterminal/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
/.vscode
|
||||
/__pycache__
|
||||
/macros.json
|
||||
/BusTerminal.exe
|
||||
88
python/pysetterminal/README.md
Normal file
88
python/pysetterminal/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# PySetTerminal
|
||||
|
||||
Shared PySide6 terminal migrated from PySetTerminal-master/pysetterminal.
|
||||
|
||||
- Standalone entry point: `python -m pysetterminal` with templates/python on PYTHONPATH.
|
||||
- SETGUI embeds `pysetterminal.panel.ByteWorkPanel` in TMS2812 / Работа с байтом.
|
||||
- Serial bus, macro editor, asynchronous macro execution with cancellation, PEEK/POKE and RAW console are included.
|
||||
- COM transport uses QtSerialPort. Embedded operations reuse the TMS2812 connection and configured controller address.
|
||||
- PEEK/POKE packet construction and response validation use the existing protocan native C ABI, with 16-bit TMS2812 values.
|
||||
- Macro files are stored under the application data directory, outside the source checkout.
|
||||
- The standalone launcher uses PySide6; SETGUI Windows 7 uses its existing PySide2/Qt5 compatibility layer. Frozen SETGUI builds bundle Python and Qt. Pyserial is not required. A native set-protocol runtime remains required.
|
||||
|
||||
`build_and_clean.py` is the migrated original standalone packaging helper.
|
||||
|
||||
## Подключение «Работы с байтом» в другую вкладку
|
||||
|
||||
Вся панель — самостоятельный `ByteWorkPanel`: чтение и запись слова, HEX/BIN,
|
||||
операции с шиной, макросы, PEEK/POKE и RAW. Она не требует `Tms2812Tab`.
|
||||
Данные — 16 бит, адрес слова — 32 бита; протокол по умолчанию — TMS2812.
|
||||
|
||||
```python
|
||||
from pysetterminal.panel import ByteWorkPanel
|
||||
from pysetterminal.rawprotocol import RawProtocol
|
||||
|
||||
# Сохраните протокол в объекте, который владеет соединением.
|
||||
self.byte_protocol = RawProtocol()
|
||||
self.byte_page = ByteWorkPanel(
|
||||
self.byte_protocol,
|
||||
device_address=lambda: self.controller_address.value(),
|
||||
)
|
||||
self.tabs.addTab(self.byte_page, "Работа с байтом")
|
||||
|
||||
# Подключение выполняется явным действием пользователя.
|
||||
# self.byte_protocol.connect("COM3", 57600)
|
||||
|
||||
# Перед закрытием соединения:
|
||||
# self.byte_page.stop()
|
||||
# self.byte_protocol.disconnect()
|
||||
```
|
||||
|
||||
В существующую компоновку панель добавляется через
|
||||
`layout.addWidget(self.byte_page)`. Фабрика
|
||||
`build_byte_page(protocol, **options)` возвращает такой же виджет.
|
||||
|
||||
Параметры:
|
||||
|
||||
- `protocol` — существующий протокол с методами `request_async`,
|
||||
`cancel_async`, `peek`, `poke`, `send_raw`, `receive_raw`.
|
||||
Асинхронный ответ передаётся в `callback(success, result)`: для чтения
|
||||
`result["value"]`, для записи `result["raw"]`; ошибки — строкой.
|
||||
Синхронные `peek/poke` возвращают `(success, result)`; чтение дополнительно
|
||||
содержит `result["raw_response"]["raw"]` для журнала.
|
||||
- `device_address` — адрес контроллера или функция, возвращающая текущий адрес;
|
||||
используется всеми блоками панели. Адрес фиксируется на время записи с
|
||||
проверкой и выполнения макроса.
|
||||
- `macros_file` — отдельный JSON-файл макросов для конкретной вкладки.
|
||||
По умолчанию сохраняется существующее хранилище приложения.
|
||||
- `default_address`, `hint`, `initial_status` — начальный адрес и тексты.
|
||||
- `start_word(write, address, value)` — необязательный обработчик для вкладок,
|
||||
у которых уже есть очередь памяти. При чтении `value=None`.
|
||||
Обработчик отвечает за чтение, проверку записи и остановку своей очереди.
|
||||
|
||||
Без `start_word` панель выполняет асинхронный запрос, а запись подтверждает
|
||||
последующим чтением. Пример подключения к существующей очереди:
|
||||
|
||||
```python
|
||||
self.byte_page = ByteWorkPanel(
|
||||
self.protocol,
|
||||
device_address=lambda: self.address.value(),
|
||||
start_word=lambda write, address, value: self.start_memory(
|
||||
write=write, address=address, value=value),
|
||||
macros_file=self.storage_dir / "byte_macros.json",
|
||||
)
|
||||
self.tabs.addTab(self.byte_page, "Работа с байтом")
|
||||
|
||||
# В обработчике ответа очереди:
|
||||
self.byte_page.set_read_value(address, value)
|
||||
self.byte_page.set_status("Чтение завершено")
|
||||
```
|
||||
|
||||
`set_status(text)` показывает внешний статус. Сигнал `status_changed(str)`
|
||||
передаёт ошибки ввода и результаты операций панели владельцу.
|
||||
`stop()` отменяет её асинхронный обмен и макрос; внешнюю очередь памяти
|
||||
останавливает владелец вкладки.
|
||||
|
||||
Каждому независимому соединению нужен собственный протокол. Если вкладки
|
||||
используют одно соединение, владелец транспорта должен запрещать одновременные
|
||||
запросы и маршрутизировать ответы. В SETGUI это делает существующая очередь TMS2812.
|
||||
1
python/pysetterminal/__init__.py
Normal file
1
python/pysetterminal/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Shared PySide6 PySetTerminal project; protocol codecs use protocan native ABI."""
|
||||
2
python/pysetterminal/__main__.py
Normal file
2
python/pysetterminal/__main__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .mainterm import main
|
||||
main()
|
||||
124
python/pysetterminal/build_and_clean.py
Normal file
124
python/pysetterminal/build_and_clean.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import subprocess
|
||||
import shutil
|
||||
import os
|
||||
from pathlib import Path
|
||||
import PySide6
|
||||
from PyInstaller.utils.hooks import collect_data_files
|
||||
# install: pip install PySide6 lxml nuitka pyinstaller
|
||||
# - PyInstaller
|
||||
# - nuitka
|
||||
# - PySide6
|
||||
# - clang
|
||||
|
||||
# === Конфигурация ===
|
||||
USE_NUITKA = True # True — сборка через Nuitka, False — через PyInstaller
|
||||
MAIN_SCRIPT_NAME = "mainterm"
|
||||
OUTPUT_NAME = "BusTerminal"
|
||||
USE_ICON = False # True — использовать иконку, False — без иконки
|
||||
|
||||
|
||||
SRC_PATH = Path("./")
|
||||
SCRIPT_PATH = SRC_PATH / (MAIN_SCRIPT_NAME + ".py")
|
||||
|
||||
DIST_PATH = Path("./").resolve()
|
||||
WORK_PATH = Path("./build_temp").resolve()
|
||||
SPEC_PATH = WORK_PATH
|
||||
ICON_PATH = SRC_PATH / "icon.png"
|
||||
ICON_ICO_PATH = SRC_PATH / "icon.ico"
|
||||
|
||||
TEMP_FOLDERS = [
|
||||
"build_temp",
|
||||
"__pycache__",
|
||||
MAIN_SCRIPT_NAME + ".build",
|
||||
MAIN_SCRIPT_NAME + ".onefile-build",
|
||||
MAIN_SCRIPT_NAME + ".dist"
|
||||
]
|
||||
# === Пути к DLL и прочим зависимостям ===
|
||||
LIBS = {
|
||||
}
|
||||
|
||||
# === PySide6 плагины ===
|
||||
PySide6_path = Path(PySide6.__file__).parent
|
||||
datas = []
|
||||
datas += collect_data_files('PySide6', includes=['plugins/platforms/*'])
|
||||
datas += collect_data_files('PySide6', includes=['plugins/styles/*'])
|
||||
datas += collect_data_files('PySide6', includes=['plugins/imageformats/*'])
|
||||
|
||||
add_data_list = [f"{src};{dest}" for src, dest in datas]
|
||||
|
||||
# Проверка наличия DLL и добавление
|
||||
add_binary_list = []
|
||||
for name, path in LIBS.items():
|
||||
if path.exists():
|
||||
add_binary_list.append(f"{str(path)};{name}")
|
||||
else:
|
||||
print(f"WARNING: {path.name} не найден — он не будет включён в сборку")
|
||||
|
||||
def clean_temp():
|
||||
for folder in TEMP_FOLDERS:
|
||||
path = Path(folder)
|
||||
if path.exists():
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
if USE_NUITKA:
|
||||
# Формируем include-data-file только для DLL
|
||||
include_data_files = [f"--include-data-file={str(path)}={name}" for name, path in LIBS.items() if path.exists()]
|
||||
|
||||
# Добавляем опциональную иконку как встроенный ресурс
|
||||
if USE_ICON and ICON_ICO_PATH.exists():
|
||||
include_data_files.append(f"--include-data-file={ICON_ICO_PATH}=icon.ico")
|
||||
|
||||
cmd = [
|
||||
"python", "-m", "nuitka",
|
||||
"--standalone",
|
||||
"--onefile",
|
||||
"--enable-plugin=pyside2",
|
||||
"--windows-console-mode=disable",
|
||||
f"--output-dir={DIST_PATH}",
|
||||
f"--output-filename={OUTPUT_NAME}.exe",
|
||||
]
|
||||
|
||||
# Добавляем параметр иконки только если она нужна и существует
|
||||
if USE_ICON and ICON_ICO_PATH.exists():
|
||||
cmd.append(f"--windows-icon-from-ico={ICON_ICO_PATH}")
|
||||
|
||||
cmd.extend(include_data_files)
|
||||
cmd.append(str(SCRIPT_PATH))
|
||||
|
||||
else:
|
||||
# PyInstaller
|
||||
cmd = [
|
||||
"pyinstaller",
|
||||
"--name", OUTPUT_NAME,
|
||||
"--distpath", str(DIST_PATH),
|
||||
"--workpath", str(WORK_PATH),
|
||||
"--specpath", str(SPEC_PATH),
|
||||
"--windowed",
|
||||
"--hidden-import=PySide6.QtWidgets",
|
||||
"--hidden-import=PySide6.QtGui",
|
||||
"--hidden-import=PySide6.QtCore",
|
||||
*[arg for b in add_binary_list for arg in ("--add-binary", b)],
|
||||
*[arg for d in add_data_list for arg in ("--add-data", d)],
|
||||
]
|
||||
|
||||
# Добавляем иконку только если она нужна и существует
|
||||
if USE_ICON and ICON_ICO_PATH.exists():
|
||||
cmd.append(f"--icon={ICON_ICO_PATH}")
|
||||
else:
|
||||
print("WARNING: Иконка не используется или не найдена")
|
||||
|
||||
cmd.append(str(SCRIPT_PATH))
|
||||
|
||||
# === Запуск сборки ===
|
||||
print("Выполняется сборка с помощью " + ("Nuitka" if USE_NUITKA else "PyInstaller"))
|
||||
print(" ".join(cmd))
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
print("\nСборка успешно завершена!")
|
||||
|
||||
# Удаление временных папок после сборки
|
||||
clean_temp()
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
print("\nОшибка при сборке.")
|
||||
209
python/pysetterminal/mainterm.py
Normal file
209
python/pysetterminal/mainterm.py
Normal file
@@ -0,0 +1,209 @@
|
||||
import sys
|
||||
from PySide6.QtSerialPort import QSerialPortInfo
|
||||
from PySide6.QtWidgets import *
|
||||
from PySide6.QtCore import *
|
||||
from PySide6.QtGui import *
|
||||
|
||||
from pysetterminal.rawprotocol import RawProtocol, RawProtocolWidget
|
||||
from pysetterminal.serialbus import SerialBus, SerialTab
|
||||
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Главное окно приложения"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Создаем низкоуровневый протокол
|
||||
self.raw_protocol = RawProtocol()
|
||||
# Создаем высокоуровневый протокол, передавая ему low-level
|
||||
self.serial_bus = SerialBus(self.raw_protocol)
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
self.setWindowTitle("Терминал")
|
||||
self.setGeometry(100, 100, 800, 600)
|
||||
|
||||
# Главный виджет и компоновка
|
||||
main_widget = QWidget()
|
||||
main_layout = QVBoxLayout()
|
||||
|
||||
# Центральный виджет с вкладками
|
||||
self.tab_widget = QTabWidget()
|
||||
|
||||
# Виджет для RAW протокола (всегда внизу)
|
||||
self.raw_widget = RawProtocolWidget(self.raw_protocol)
|
||||
|
||||
# Вкладка для серийной шины
|
||||
self.serial_tab = SerialTab(self.serial_bus, self.raw_protocol, self.raw_widget)
|
||||
self.tab_widget.addTab(self.serial_tab, "Серийная шина")
|
||||
|
||||
main_layout.addWidget(self.tab_widget)
|
||||
|
||||
main_layout.addWidget(self.raw_widget)
|
||||
|
||||
main_widget.setLayout(main_layout)
|
||||
self.setCentralWidget(main_widget)
|
||||
|
||||
# Создание меню
|
||||
self.create_menu()
|
||||
|
||||
# Создание панели инструментов
|
||||
self.create_toolbar()
|
||||
|
||||
def create_menu(self):
|
||||
menubar = self.menuBar()
|
||||
|
||||
# Меню Файл
|
||||
file_menu = menubar.addMenu("Файл")
|
||||
|
||||
exit_action = QAction("Выход", self)
|
||||
exit_action.triggered.connect(self.close)
|
||||
file_menu.addAction(exit_action)
|
||||
|
||||
# Меню Помощь
|
||||
help_menu = menubar.addMenu("Помощь")
|
||||
|
||||
protocol_action = QAction("Информация о протоколе", self)
|
||||
protocol_action.triggered.connect(self.show_protocol_info)
|
||||
help_menu.addAction(protocol_action)
|
||||
|
||||
def create_toolbar(self):
|
||||
toolbar = self.addToolBar("Подключение")
|
||||
|
||||
# Выбор COM порта
|
||||
toolbar.addWidget(QLabel("COM порт:"))
|
||||
|
||||
self.port_combo = QComboBox()
|
||||
self.port_combo.setMinimumWidth(150)
|
||||
self.refresh_ports()
|
||||
toolbar.addWidget(self.port_combo)
|
||||
|
||||
# Кнопка обновления портов
|
||||
refresh_btn = QToolButton()
|
||||
refresh_btn.setText("⟳")
|
||||
refresh_btn.setToolTip("Обновить список портов")
|
||||
refresh_btn.clicked.connect(self.refresh_ports)
|
||||
toolbar.addWidget(refresh_btn)
|
||||
|
||||
# Кнопка подключения/отключения
|
||||
self.connect_btn = QPushButton("Подключить")
|
||||
self.connect_btn.clicked.connect(self.toggle_connection)
|
||||
toolbar.addWidget(self.connect_btn)
|
||||
|
||||
# Индикатор статуса
|
||||
self.status_label = QLabel("Отключено")
|
||||
self.status_label.setStyleSheet("color: red;")
|
||||
toolbar.addWidget(self.status_label)
|
||||
|
||||
toolbar.addSeparator()
|
||||
|
||||
# Настройки скорости
|
||||
toolbar.addWidget(QLabel("Скорость:"))
|
||||
self.baud_combo = QComboBox()
|
||||
self.baud_combo.addItems(["9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"])
|
||||
self.baud_combo.setCurrentText("115200")
|
||||
toolbar.addWidget(self.baud_combo)
|
||||
|
||||
def refresh_ports(self):
|
||||
"""Обновление списка доступных COM портов"""
|
||||
self.port_combo.clear()
|
||||
ports = QSerialPortInfo.availablePorts()
|
||||
for port in ports:
|
||||
self.port_combo.addItem(f"{port.portName()} - {port.description()}")
|
||||
|
||||
if not ports:
|
||||
self.port_combo.addItem("Порты не найдены")
|
||||
|
||||
def toggle_connection(self):
|
||||
"""Подключение/отключение от COM порта"""
|
||||
if self.raw_protocol.is_connected:
|
||||
# Отключение
|
||||
self.raw_protocol.disconnect()
|
||||
self.connect_btn.setText("Подключить")
|
||||
self.status_label.setText("Отключено")
|
||||
self.status_label.setStyleSheet("color: red;")
|
||||
|
||||
# Блокировка элементов управления
|
||||
self.serial_tab.bus_write_btn.setEnabled(False)
|
||||
self.serial_tab.bus_read_btn.setEnabled(False)
|
||||
self.raw_widget.raw_send_btn.setEnabled(False)
|
||||
self.raw_widget.raw_read_btn.setEnabled(False)
|
||||
else:
|
||||
# Подключение
|
||||
port_text = self.port_combo.currentText()
|
||||
if " - " in port_text:
|
||||
port_name = port_text.split(" - ")[0]
|
||||
else:
|
||||
port_name = port_text
|
||||
|
||||
try:
|
||||
baudrate = int(self.baud_combo.currentText())
|
||||
if self.raw_protocol.connect(port_name, baudrate):
|
||||
self.connect_btn.setText("Отключить")
|
||||
self.status_label.setText("Подключено")
|
||||
self.status_label.setStyleSheet("color: green;")
|
||||
|
||||
# Разблокировка элементов управления
|
||||
self.serial_tab.bus_write_btn.setEnabled(True)
|
||||
self.serial_tab.bus_read_btn.setEnabled(True)
|
||||
self.raw_widget.raw_send_btn.setEnabled(True)
|
||||
self.raw_widget.raw_read_btn.setEnabled(True)
|
||||
else:
|
||||
QMessageBox.warning(self, "Ошибка", "Не удалось подключиться к порту")
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Ошибка", f"Ошибка подключения: {e}")
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.serial_tab.stop_macro()
|
||||
self.raw_protocol.cancel_async()
|
||||
self.raw_protocol.disconnect()
|
||||
super().closeEvent(event)
|
||||
|
||||
def show_protocol_info(self):
|
||||
"""Показать информацию о протоколе"""
|
||||
info = """
|
||||
ИНФОРМАЦИЯ О ПРОТОКОЛЕ:
|
||||
|
||||
На основе кода контроллера:
|
||||
|
||||
1. ФОРМАТ СООБЩЕНИЙ:
|
||||
[адрес][команда][данные][CRC16-IBM]
|
||||
|
||||
2. КЛЮЧЕВЫЕ АДРЕСА:
|
||||
- 0x200A: Регистр данных шины (запись)
|
||||
- 0x200B: Регистр управления шиной
|
||||
- 0x200F: Регистр данных шины (чтение)
|
||||
|
||||
3. ПРОТОКОЛ ШИНЫ:
|
||||
ЗАПИСЬ:
|
||||
1. POKE 0x200A <данные>
|
||||
2. POKE 0x200B 0x80A0 | (адрес << 4)
|
||||
|
||||
ЧТЕНИЕ:
|
||||
1. POKE 0x200B 0x00A0 | (адрес << 4)
|
||||
2. PEEK 0x200F
|
||||
|
||||
4. ЧТО НУЖНО УТОЧНИТЬ:
|
||||
- Точные коды команд CMD_RS232_PEEK и CMD_RS232_POKE
|
||||
- Длину полей в сообщениях
|
||||
- Формат ответов от контроллера
|
||||
"""
|
||||
|
||||
QMessageBox.information(self, "Информация о протоколе", info)
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Установка стиля
|
||||
app.setStyle("Fusion")
|
||||
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
183
python/pysetterminal/panel.py
Normal file
183
python/pysetterminal/panel.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""Reusable byte/word terminal page with an injected protocol or word handler."""
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QLabel, QHBoxLayout, QGroupBox, QGridLayout,
|
||||
QLineEdit, QPushButton, QScrollArea, QSizePolicy,
|
||||
)
|
||||
|
||||
from .rawprotocol import RawProtocolWidget
|
||||
from .serialbus import SerialBus, SerialTab
|
||||
|
||||
|
||||
class ByteWorkPanel(QScrollArea):
|
||||
"""Embed the complete terminal page in any layout or QTabWidget.
|
||||
|
||||
protocol supplies request_async/cancel_async, peek/poke and RAW methods.
|
||||
device_address can be an integer or a callable returning the current
|
||||
controller address. An optional start_word(write, address, value) handler
|
||||
lets an existing tab keep its own memory queue; it must report results with
|
||||
set_read_value() and set_status(). Otherwise this panel performs asynchronous
|
||||
reads and verifies writes by reading the same word back.
|
||||
"""
|
||||
|
||||
status_changed = Signal(str)
|
||||
|
||||
def __init__(self, protocol, parent=None, *, device_address=10,
|
||||
start_word=None, macros_file=None, default_address="$1000",
|
||||
hint=None, initial_status="Нет данных от контроллера"):
|
||||
super().__init__(parent)
|
||||
self.protocol = protocol
|
||||
self.device_address = device_address
|
||||
self._start_word = start_word
|
||||
self._word_running = False
|
||||
self._word_generation = 0
|
||||
self.setWidgetResizable(True)
|
||||
content = QWidget()
|
||||
self.setWidget(content)
|
||||
layout = QVBoxLayout(content)
|
||||
self.hint = QLabel(hint if hint is not None else
|
||||
"Адрес слова — 32 бита; значение — 16 бит (0000…FFFF). "
|
||||
"Адрес: десятичный, $HEX или 0xHEX. Запись проверяется чтением.")
|
||||
self.hint.setWordWrap(True)
|
||||
layout.addWidget(self.hint)
|
||||
panels = QHBoxLayout()
|
||||
for write in (True, False):
|
||||
group = QGroupBox("Запись" if write else "Чтение")
|
||||
group.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
|
||||
form = QGridLayout(group)
|
||||
address = QLineEdit(default_address)
|
||||
value = QLineEdit("0000" if write else "")
|
||||
binary = QLineEdit("0000 0000 0000 0000" if write else "")
|
||||
address.setFixedWidth(150)
|
||||
value.setFixedWidth(100)
|
||||
binary.setFixedWidth(240)
|
||||
value.setReadOnly(not write)
|
||||
binary.setReadOnly(not write)
|
||||
for row, (title, widget) in enumerate((("Адрес", address), ("Байт (hex)", value),
|
||||
("Байт (bin)", binary))):
|
||||
form.addWidget(QLabel(title), row, 0)
|
||||
form.addWidget(widget, row, 1)
|
||||
button = QPushButton("Записать" if write else "Прочитать")
|
||||
form.addWidget(button, 3, 1, Qt.AlignLeft)
|
||||
form.setColumnStretch(2, 1)
|
||||
if write:
|
||||
self.byte_write_address, self.byte_write_value, self.byte_write_binary = address, value, binary
|
||||
self.byte_write_button = button
|
||||
value.textEdited.connect(lambda text: self.sync_byte_value(text, 16))
|
||||
binary.textEdited.connect(lambda text: self.sync_byte_value(text, 2))
|
||||
button.clicked.connect(lambda: self.start_byte(True))
|
||||
else:
|
||||
self.byte_read_address, self.byte_read_value, self.byte_read_binary = address, value, binary
|
||||
self.byte_read_button = button
|
||||
button.clicked.connect(lambda: self.start_byte(False))
|
||||
panels.addWidget(group)
|
||||
panels.addStretch()
|
||||
layout.addLayout(panels)
|
||||
self.status = QLabel(initial_status)
|
||||
self.status.setWordWrap(True)
|
||||
layout.addWidget(self.status)
|
||||
self.raw_widget = RawProtocolWidget(protocol, device_address=device_address)
|
||||
self.bus_widget = SerialTab(SerialBus(protocol), protocol, self.raw_widget,
|
||||
device_address=device_address, macros_file=macros_file)
|
||||
layout.addWidget(self.bus_widget)
|
||||
layout.addWidget(self.raw_widget)
|
||||
layout.addStretch()
|
||||
|
||||
def set_status(self, text):
|
||||
"""Display external connection/operation status without emitting it again."""
|
||||
self.status.setText(text)
|
||||
|
||||
def _report(self, text):
|
||||
self.set_status(text)
|
||||
self.status_changed.emit(text)
|
||||
|
||||
@staticmethod
|
||||
def _binary(value):
|
||||
bits = f"{value:016b}"
|
||||
return " ".join(bits[i:i + 4] for i in range(0, 16, 4))
|
||||
|
||||
def sync_byte_value(self, text, base):
|
||||
try:
|
||||
value = int(text.replace(" ", ""), base)
|
||||
if not 0 <= value <= 0xFFFF:
|
||||
return
|
||||
except ValueError:
|
||||
return
|
||||
target = self.byte_write_binary if base == 16 else self.byte_write_value
|
||||
target.setText(self._binary(value) if base == 16 else f"{value:04X}")
|
||||
|
||||
def set_read_value(self, address, value):
|
||||
self.byte_read_address.setText(f"${address:X}")
|
||||
self.byte_read_value.setText(f"{value:04X}")
|
||||
self.byte_read_binary.setText(self._binary(value))
|
||||
|
||||
def start_byte(self, write):
|
||||
if self._word_running:
|
||||
self._report("Дождитесь завершения текущего обмена")
|
||||
return
|
||||
try:
|
||||
field = self.byte_write_address if write else self.byte_read_address
|
||||
text = field.text().strip()
|
||||
address = int(text[1:], 16) if text.startswith("$") else int(
|
||||
text, 16 if text.lower().startswith("0x") else 10)
|
||||
value = int(self.byte_write_value.text().replace(" ", ""), 16) if write else None
|
||||
if not 0 <= address <= 0xFFFFFFFF:
|
||||
raise ValueError()
|
||||
if write and (not 0 <= value <= 0xFFFF or
|
||||
int(self.byte_write_binary.text().replace(" ", ""), 2) != value):
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
self._report("Проверьте адрес и значение HEX/BIN")
|
||||
return
|
||||
if self._start_word is not None:
|
||||
self._start_word(write, address, value)
|
||||
return
|
||||
controller = self.device_address() if callable(self.device_address) else self.device_address
|
||||
self._word_generation += 1
|
||||
generation = self._word_generation
|
||||
self._set_word_running(True)
|
||||
self._report("Запись и проверка чтением…" if write else "Чтение…")
|
||||
self.protocol.request_async(address, value, controller,
|
||||
lambda success, result: self._word_reply(generation, address, value, controller, success, result))
|
||||
|
||||
def _word_reply(self, generation, address, expected, controller, success, result):
|
||||
if generation != self._word_generation or not self._word_running:
|
||||
return
|
||||
if not success:
|
||||
self._set_word_running(False)
|
||||
self._report(str(result))
|
||||
return
|
||||
if expected is not None and "value" not in result:
|
||||
self.protocol.request_async(address, None, controller,
|
||||
lambda ok, reply: self._word_reply(generation, address, expected, controller, ok, reply))
|
||||
return
|
||||
value = result["value"]
|
||||
self.set_read_value(address, value)
|
||||
self._set_word_running(False)
|
||||
if expected is not None and value != expected:
|
||||
self._report(f"Запись 0x{address:08X} не подтверждена: прочитано {value:04X}, ожидалось {expected:04X}")
|
||||
else:
|
||||
self._report("Запись проверена чтением" if expected is not None else "Чтение завершено")
|
||||
|
||||
def _set_word_running(self, running):
|
||||
self._word_running = running
|
||||
self.byte_write_button.setEnabled(not running)
|
||||
self.byte_read_button.setEnabled(not running)
|
||||
|
||||
def stop(self):
|
||||
"""Cancel this page's scheduled work before closing its connection."""
|
||||
self.bus_widget.stop_macro()
|
||||
if self._word_running:
|
||||
self._word_generation += 1
|
||||
self._set_word_running(False)
|
||||
self.protocol.cancel_async()
|
||||
self._report("Обмен прерван; переданные команды могли быть выполнены")
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.stop()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def build_byte_page(protocol, **options):
|
||||
"""Create a standalone page; add the returned widget to a tab or layout."""
|
||||
return ByteWorkPanel(protocol, **options)
|
||||
341
python/pysetterminal/rawprotocol.py
Normal file
341
python/pysetterminal/rawprotocol.py
Normal file
@@ -0,0 +1,341 @@
|
||||
"""Qt transport over the common native TMS2812 codec."""
|
||||
from PySide6.QtWidgets import *
|
||||
from PySide6.QtCore import *
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtSerialPort import QSerialPort
|
||||
from protocan import tms_terminal as memory
|
||||
|
||||
|
||||
class RawProtocol:
|
||||
def __init__(self, host=None):
|
||||
self.host = host
|
||||
self.serial_port = QSerialPort() if host is None else None
|
||||
self.buffer = bytearray()
|
||||
self._loop = None
|
||||
self._callback = None
|
||||
self._command = None
|
||||
self._error = None
|
||||
self._packet = None
|
||||
self._raw_rx = bytearray()
|
||||
if host is None:
|
||||
self.serial_port.readyRead.connect(self._receive)
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
return self.port.isOpen()
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return self.host._serial if self.host else self.serial_port
|
||||
|
||||
def connect(self, port_name, baudrate=115200):
|
||||
self.port.setPortName(port_name)
|
||||
self.port.setBaudRate(baudrate)
|
||||
return self.port.open(QIODevice.ReadWrite)
|
||||
|
||||
def disconnect(self):
|
||||
self.port.close()
|
||||
|
||||
def _receive(self):
|
||||
self.feed(bytes(self.port.readAll()))
|
||||
|
||||
def feed(self, raw):
|
||||
self._raw_rx.extend(raw)
|
||||
del self._raw_rx[:-65536]
|
||||
if self._loop is None and self._callback is None:
|
||||
return
|
||||
self.buffer.extend(raw)
|
||||
try:
|
||||
packet = memory.memory_response(self.buffer, self._controller, self._command)
|
||||
if packet is not None:
|
||||
self._packet = packet
|
||||
if len(packet) == (10 if self._command == memory.PEEK else 6):
|
||||
self._settle.stop()
|
||||
self._finish_async() if self._callback else self._loop.quit()
|
||||
elif not self._settle.isActive():
|
||||
self._settle.start(30)
|
||||
except ValueError as error:
|
||||
self._error = str(error)
|
||||
self._finish_async() if self._callback else self._loop.quit()
|
||||
|
||||
def request_async(self, address, value, device_address, callback):
|
||||
"""Begin one request and return immediately; completion runs on the Qt thread."""
|
||||
if not self.is_connected:
|
||||
callback(False, "Подключите COM-порт")
|
||||
return
|
||||
if self._loop is not None or self._callback is not None or (self.host and self.host._pending_address is not None):
|
||||
callback(False, "Дождитесь завершения текущего обмена")
|
||||
return
|
||||
self._controller = (device_address if device_address is not None else
|
||||
self.host.address.value() if self.host else 1)
|
||||
self._command = memory.PEEK if value is None else memory.POKE
|
||||
try:
|
||||
request = memory.request(self._controller, address, value)
|
||||
except (ValueError, RuntimeError) as error:
|
||||
callback(False, str(error))
|
||||
return
|
||||
self.buffer.clear()
|
||||
self._error = self._packet = None
|
||||
self._callback = callback
|
||||
self._settle = QTimer()
|
||||
self._settle.setSingleShot(True)
|
||||
self._settle.timeout.connect(self._finish_async)
|
||||
self._async_timer = QTimer()
|
||||
self._async_timer.setSingleShot(True)
|
||||
self._async_timer.timeout.connect(lambda: self.cancel_async("Нет ответа от устройства"))
|
||||
if self.host:
|
||||
self.host._pending_address = self._controller
|
||||
self.host._pending_kind = "pysetterminal"
|
||||
self.host._log("TX", request)
|
||||
self.port.readAll()
|
||||
self._async_timer.start(self.host.timeout_ms.value() if self.host else 1000)
|
||||
if self.port.write(request) != len(request):
|
||||
self.cancel_async("Ошибка передачи")
|
||||
|
||||
def _finish_async(self):
|
||||
callback = self._callback
|
||||
if callback is None:
|
||||
return
|
||||
self._callback = None
|
||||
self._async_timer.stop()
|
||||
self._settle.stop()
|
||||
if self.host:
|
||||
self.host._pending_address = self.host._pending_kind = None
|
||||
if self._error or self._packet is None:
|
||||
callback(False, self._error or "Обмен прерван")
|
||||
return
|
||||
packet = self._packet
|
||||
result = {'address': self._controller, 'command': self._command, 'raw': packet}
|
||||
if self._command == memory.PEEK:
|
||||
result = {'value': memory.decode_reply(packet, self._controller, self._command), 'raw_response': result}
|
||||
callback(True, result)
|
||||
|
||||
def cancel_async(self, reason="Макрос остановлен"):
|
||||
if self._callback is not None:
|
||||
self._error = reason
|
||||
self._finish_async()
|
||||
|
||||
def _exchange(self, address, value, device_address):
|
||||
if not self.is_connected:
|
||||
return False, "Подключите COM-порт"
|
||||
if self._loop is not None or self._callback is not None or (self.host and self.host._pending_address is not None):
|
||||
return False, "Дождитесь завершения текущего обмена"
|
||||
self._controller = (device_address if device_address is not None else
|
||||
self.host.address.value() if self.host else 1)
|
||||
self._command = memory.PEEK if value is None else memory.POKE
|
||||
try:
|
||||
request = memory.request(self._controller, address, value)
|
||||
except (ValueError, RuntimeError) as error:
|
||||
return False, str(error)
|
||||
self.buffer.clear()
|
||||
self._error = self._packet = None
|
||||
loop = QEventLoop()
|
||||
timer = QTimer()
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(loop.quit)
|
||||
self._loop = loop
|
||||
self._settle = QTimer()
|
||||
self._settle.setSingleShot(True)
|
||||
self._settle.timeout.connect(loop.quit)
|
||||
if self.host:
|
||||
self.host.byte_page.setEnabled(False)
|
||||
self.host._pending_address = self._controller
|
||||
self.host._pending_kind = "pysetterminal"
|
||||
self.host._log("TX", request)
|
||||
try:
|
||||
self.port.readAll()
|
||||
if self.port.write(request) != len(request):
|
||||
return False, "Ошибка передачи"
|
||||
if self._error is None and (self._packet is None or self._settle.isActive()):
|
||||
timer.start(self.host.timeout_ms.value() if self.host else 1000)
|
||||
loop.exec()
|
||||
if self._error or self._packet is None:
|
||||
return False, self._error or "Нет ответа от устройства"
|
||||
packet = self._packet
|
||||
result = {'address': self._controller, 'command': self._command, 'raw': packet}
|
||||
if value is None:
|
||||
return True, {'value': memory.decode_reply(packet, self._controller, self._command), 'raw_response': result}
|
||||
return True, result
|
||||
finally:
|
||||
timer.stop()
|
||||
self._settle.stop()
|
||||
self._loop = None
|
||||
if self.host:
|
||||
self.host._pending_address = self.host._pending_kind = None
|
||||
self.host.byte_page.setEnabled(True)
|
||||
|
||||
def poke(self, mem_address, data_value, device_address=None):
|
||||
return self._exchange(mem_address, data_value, device_address)
|
||||
|
||||
def peek(self, mem_address, device_address=None):
|
||||
return self._exchange(mem_address, None, device_address)
|
||||
|
||||
def send_raw(self, hex_data):
|
||||
if not self.is_connected:
|
||||
return False, "Подключите COM-порт"
|
||||
if self._loop is not None or self._callback is not None or (self.host and self.host._pending_address is not None):
|
||||
return False, "Дождитесь завершения текущего обмена"
|
||||
if self.host and self.host._cycle_timer.isActive():
|
||||
return False, "Остановите циклический обмен перед RAW"
|
||||
try:
|
||||
data = bytes.fromhex(hex_data)
|
||||
except ValueError:
|
||||
return False, "Некорректные HEX данные"
|
||||
if not data:
|
||||
return False, "Введите HEX данные"
|
||||
if self.port.write(data) != len(data):
|
||||
return False, "Ошибка передачи"
|
||||
if self.host:
|
||||
self.host._log("TX", data)
|
||||
return True, "TX " + data.hex(" ")
|
||||
|
||||
def receive_raw(self, max_bytes=1024):
|
||||
data = bytes(self._raw_rx[:max_bytes])
|
||||
del self._raw_rx[:max_bytes]
|
||||
return (True, data.hex(" ")) if data else (False, "Нет данных")
|
||||
|
||||
|
||||
class RawProtocolWidget(QWidget):
|
||||
"""Виджет для отладки RAW протокола (всегда виден внизу окна)"""
|
||||
|
||||
def __init__(self, raw_protocol, parent=None, *, device_address=10):
|
||||
super().__init__(parent)
|
||||
self.raw_protocol = raw_protocol
|
||||
self.device_address = device_address
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Группа для ПРЯМОГО ДОСТУПА К ПАМЯТИ (PEEK/POKE)
|
||||
mem_group = QGroupBox("Прямой доступ к памяти (PEEK/POKE)")
|
||||
mem_layout = QGridLayout()
|
||||
|
||||
mem_layout.addWidget(QLabel("Адрес памяти (hex):"), 0, 0)
|
||||
self.mem_addr_edit = QLineEdit("2000")
|
||||
self.mem_addr_edit.setMaximumWidth(100)
|
||||
mem_layout.addWidget(self.mem_addr_edit, 0, 1)
|
||||
|
||||
mem_layout.addWidget(QLabel("Данные (hex):"), 1, 0)
|
||||
self.mem_data_edit = QLineEdit("0000")
|
||||
self.mem_data_edit.setMaximumWidth(100)
|
||||
mem_layout.addWidget(self.mem_data_edit, 1, 1)
|
||||
|
||||
self.mem_write_btn = QPushButton("Записать (POKE)")
|
||||
self.mem_write_btn.clicked.connect(self.memory_write)
|
||||
mem_layout.addWidget(self.mem_write_btn, 0, 2)
|
||||
|
||||
self.mem_read_btn = QPushButton("Прочитать (PEEK)")
|
||||
self.mem_read_btn.clicked.connect(self.memory_read)
|
||||
mem_layout.addWidget(self.mem_read_btn, 1, 2)
|
||||
|
||||
self.mem_result_label = QLabel("---")
|
||||
mem_layout.addWidget(self.mem_result_label, 1, 3)
|
||||
|
||||
mem_layout.setColumnStretch(4, 1)
|
||||
mem_group.setLayout(mem_layout)
|
||||
layout.addWidget(mem_group)
|
||||
|
||||
# Группа для RAW отправки
|
||||
raw_group = QGroupBox("RAW отправка/чтение")
|
||||
raw_layout = QVBoxLayout()
|
||||
|
||||
self.raw_send_edit = QLineEdit()
|
||||
self.raw_send_edit.setPlaceholderText("Введите hex данные для отправки")
|
||||
self.raw_send_edit.setMaximumWidth(640)
|
||||
raw_layout.addWidget(self.raw_send_edit)
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
self.raw_send_btn = QPushButton("Отправить RAW")
|
||||
self.raw_send_btn.clicked.connect(self.send_raw_data)
|
||||
hbox.addWidget(self.raw_send_btn)
|
||||
|
||||
self.raw_read_btn = QPushButton("Прочитать RAW")
|
||||
self.raw_read_btn.clicked.connect(self.read_raw_data)
|
||||
hbox.addWidget(self.raw_read_btn)
|
||||
|
||||
|
||||
hbox.addStretch()
|
||||
raw_layout.addLayout(hbox)
|
||||
|
||||
self.raw_output = QTextEdit()
|
||||
self.raw_output.setReadOnly(True)
|
||||
self.raw_output.setMaximumHeight(100)
|
||||
raw_layout.addWidget(self.raw_output)
|
||||
|
||||
|
||||
self.raw_clear_btn = QPushButton("Очистить логи")
|
||||
self.raw_clear_btn.clicked.connect(self.clear_logs)
|
||||
raw_layout.addWidget(self.raw_clear_btn, 0, Qt.AlignLeft)
|
||||
|
||||
raw_group.setLayout(raw_layout)
|
||||
layout.addWidget(raw_group)
|
||||
|
||||
self.setLayout(layout)
|
||||
def memory_write(self):
|
||||
"""Запись в память (POKE)"""
|
||||
try:
|
||||
device_addr = self.device_address() if callable(self.device_address) else self.device_address
|
||||
mem_addr = int(self.mem_addr_edit.text(), 16)
|
||||
data = int(self.mem_data_edit.text(), 16)
|
||||
|
||||
success, result = self.raw_protocol.poke(mem_addr, data, device_addr)
|
||||
|
||||
if success:
|
||||
self.raw_output.append(f"POKE 0x{mem_addr:04X} = 0x{data:04X}")
|
||||
self.raw_output.append(f"Ответ: {result['raw'].hex()}")
|
||||
else:
|
||||
self.raw_output.append(f"Ошибка POKE: {result}")
|
||||
|
||||
except ValueError as e:
|
||||
self.raw_output.append(f"Ошибка в данных: {e}")
|
||||
|
||||
def memory_read(self):
|
||||
"""Чтение из памяти (PEEK)"""
|
||||
try:
|
||||
device_addr = self.device_address() if callable(self.device_address) else self.device_address
|
||||
mem_addr = int(self.mem_addr_edit.text(), 16)
|
||||
|
||||
success, result = self.raw_protocol.peek(mem_addr, device_addr)
|
||||
|
||||
if success:
|
||||
value = result['value']
|
||||
self.mem_result_label.setText(f"0x{value:04X} ({value})")
|
||||
self.raw_output.append(f"PEEK 0x{mem_addr:04X} = 0x{value:04X}")
|
||||
self.raw_output.append(f"Ответ: {result['raw_response']['raw'].hex()}")
|
||||
else:
|
||||
self.mem_result_label.setText(f"Ошибка: {result}")
|
||||
self.raw_output.append(f"Ошибка PEEK: {result}")
|
||||
|
||||
except ValueError as e:
|
||||
self.mem_result_label.setText(f"Ошибка: {e}")
|
||||
self.raw_output.append(f"Ошибка в данных: {e}")
|
||||
|
||||
def send_raw_data(self):
|
||||
"""Отправка сырых данных"""
|
||||
hex_str = self.raw_send_edit.text()
|
||||
success, result = self.raw_protocol.send_raw(hex_str)
|
||||
|
||||
if success:
|
||||
self.raw_output.append(result)
|
||||
else:
|
||||
self.raw_output.append(f"Ошибка: {result}")
|
||||
|
||||
def read_raw_data(self):
|
||||
"""Чтение сырых данных"""
|
||||
success, result = self.raw_protocol.receive_raw()
|
||||
|
||||
if success:
|
||||
self.raw_output.append(f"Получено: {result}")
|
||||
else:
|
||||
self.raw_output.append(f"Ошибка: {result}")
|
||||
|
||||
def append_text(self, text):
|
||||
"""Добавить текст в вывод (используется SerialTab)"""
|
||||
if text:
|
||||
self.raw_output.append(text)
|
||||
|
||||
|
||||
def clear_logs(self):
|
||||
"""Очистить лог"""
|
||||
self.raw_output.clear()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user