Расширить DEIF и генератор сигналов, подключить исходники KDiff3

Добавить общую модель сигналов XY и адаптировать генератор к двум каналам.
Расширить Android-порт DEIF общим контроллером рабочей области и проверками.
Подключить официальный репозиторий KDiff3 вложенным подмодулем.
This commit is contained in:
2026-10-29 23:32:03 +03:00
parent 41e24629ce
commit bf5f094249
13 changed files with 472 additions and 38 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "third_party/kdiff3"]
path = third_party/kdiff3
url = https://invent.kde.org/sdk/kdiff3.git
+4
View File
@@ -62,6 +62,10 @@ templates/
### Инструменты
Исходники внешних приложений находятся в [`third_party`](third_party/README.md):
[`third_party/kdiff3`](third_party/kdiff3) содержит официальный репозиторий KDiff3
как вложенный Git submodule для развития сравнения файлов и каталогов в SETGUI.
| Инструмент | Что делает |
|---|---|
| [`tools/firmware-publish`](tools/firmware-publish) | единый BAT и конфигурации для проверки и публикации `.hex` Keil / `.bin` CCS 12 в каталоге SETGUI |
+28
View File
@@ -94,6 +94,34 @@ fallback протокола отсутствует. Windows source build мож
приложение либо стандартный загрузчик находит компонент рядом с основной
DLL. Новый автономный EXE использует собственную основную DLL с DEIF.
## Android/JVM: настройки и локальный стенд
`DeifSettings` хранит профиль SLAVE, независимые параметры MASTER,
выбор пяти отправляемых объектов, калибровки управления, единицу POWER
и ручные привязки. Сериализация не сохраняет запуск симуляции или таймеров.
`readdressed` переносит связанные SAM/DAM-адреса через C, сохраняя отдельные
ручные источники, индексы объектов и масштабы.
`DeifWorkspace` объединяет пассивное наблюдение, физический CAN-SLAVE
и автономный стенд. Он получает монотонное время от приложения и возвращает
готовые кадры раздельными маршрутами MASTER и SLAVE. Локальная симуляция
не возвращает физические кадры; остановленный локальный SLAVE отклоняет
MASTER и команды экрана вместо переключения на адаптер.
NMT разрешён только работающему локальному SLAVE. Физический сервер
отклоняет запуск встроенного MASTER.
`DeifXdi.master/make/service/selector/bind/binding/selectorInfo/readdress/validate`
являются тонкими вызовами существующего C ABI. Порты не строят COB-ID,
индексы, little-endian I16, NMT или диммер повторно. Перевод касания
в заданный пользователем масштаб относится к управлению интерфейсом;
интерактивная отрисовка, отдельное подтверждение яркости и палитры,
хранилище настроек, USB и жизненный цикл остаются у приложения.
`DeifWorkspaceTest` проверяет точные builders, автономный приём интерактивных
заданий, отсутствие физического TX на локальном стенде, ограничения NMT,
независимые периоды MASTER/heartbeat, ошибки JNI, сохранение профиля
и перенос адресов без потери ручных привязок.
## Проверки
`tests/fixtures/deif-xdi-v1.tsv` — единые эталонные байты для C, Python и JNI.
+4
View File
@@ -30,8 +30,12 @@ typedef struct {
uint16_t count, received, ready, running, rate_high;
uint32_t rate;
set_wave_port port;
uint32_t *xy_samples; /* Упакованные пары DHR12RD: X в битах 0…11, Y в 16…27; память предоставляет плата. */
int (*start_xy)(void *, const uint32_t *, size_t, uint32_t); /* Запускает обе координаты общим таймером и одним DMA без сдвига между каналами. */
uint16_t channels, selected, received_other; /* Режим 1/2, выбранная координата 0/1 и число отсчётов второй координаты для общего commit. */
} set_wave_state;
void set_wave_init(set_wave_state *, uint16_t *, size_t, const set_wave_port *);
void set_wave_enable_xy(set_wave_state *, uint32_t *, int (*)(void *, const uint32_t *, size_t, uint32_t)); /* Подключает необязательный аппаратный порт XY после обычной инициализации. */
/* Atomic sample-block transfer. values=NULL builds a read; otherwise a write.
* At most 120 words: request <=249 bytes, response <=245 bytes. */
PCAN_ABI_API size_t set_wave_block_request(unsigned device,unsigned index,
File diff suppressed because one or more lines are too long
@@ -10,6 +10,15 @@ internal object NativeDeifBridge { // Только JNI-объявления; з
external fun accept(state: ByteBuffer, id: Long, data: ByteArray, flags: Int, nowMs: Long): Boolean // Байты обрабатывает общий C-код.
external fun poll(state: ByteBuffer, nowMs: Long): IntArray // Готовые события возвращаются скалярами id, dlc, data[8].
external fun snapshot(state: ByteBuffer, nowMs: Long, expiryMs: Int): DoubleArray // Числа, статусы и сроки уже вычислены в C.
external fun master(node: Int, values: IntArray, id: Long, dam: Boolean): IntArray // Все пять MPDO и отдельное задание строятся общим C-каталогом.
external fun make(field: Int, node: Int, instance: Int, value: Int, id: Long, dam: Boolean): IntArray // Яркость, палитра и измерения возвращаются готовым кадром.
external fun service(nmt: Boolean, node: Int, value: Int): IntArray // C формирует собственный heartbeat либо адресную локальную NMT-команду.
external fun selector(field: Int, node: Int, instance: Int, dam: Boolean): ByteArray // Каталожный селектор возвращается непрозрачным четырёхбайтным результатом.
external fun bind(state: ByteBuffer, field: Int, selector: ByteArray, scale: Double): Boolean // Явные привязки и масштабы проверяет нативный профиль.
external fun binding(state: ByteBuffer, field: Int): DoubleArray // Исходные коэффициенты читаются из C вместо второй таблицы GUI.
external fun validate(id: Long, data: ByteArray, slave: Int): Boolean // Роли MASTER и серверного SLAVE проверяются одним переносимым списком разрешений.
external fun selectorInfo(selector: ByteArray): IntArray // Редактор получает Node, индекс, подиндекс и режим из общего C-декодера.
external fun readdress(selector: ByteArray, node: Int): ByteArray // C меняет только адрес, сохраняя пользовательский объект и режим SAM/DAM.
external fun inspect(id: Long, data: ByteArray, flags: Int): IntArray // Журнал получает единый результат декодирования.
} // ABI не раскрывает Kotlin layout внутренних структур.
data class DeifFrame(val canId: Long, val data: ByteArray) // Готовый STD data frame передаётся существующему CAN-транспорту приложения.
@@ -41,8 +50,50 @@ class DeifXdi(node: Int = 13, source: Int = 1, xdi: Boolean = true, automatic: B
List(6) { field -> val flags = raw[13 + field * 4].toInt(); if (flags and 2 != 0 && flags and 4 == 0) raw[10 + field * 4] else null }, // Порт отображает valid/stale-флаги, не повторяя таймеры.
List(6) { raw[12 + it * 4].toInt() }, (0..5).filter { raw[36].toInt() and (1 shl it) != 0 }.toSet()) // Имена и цвета ошибок остаются ответственностью GUI.
} // Снимок можно использовать в UI без обращения к байтам протокола.
@Synchronized fun bind(binding: DeifBinding) { // Пользовательский профиль передаёт селектор без разбора адресного байта в Kotlin.
val bytes = selectorBytes(binding.selector) // Снимается только текстовое HEX-кодирование пользовательского поля.
require(NativeDeifBridge.bind(state, binding.field, bytes, binding.scale)) { "Неверная привязка либо масштаб DEIF" } // Нативный профиль проверяет номер поля и конечность коэффициента.
} // Изменение привязки сбрасывает только связанные с ней измерения по правилам C.
@Synchronized fun binding(field: Int): DeifBinding { // GUI получает текущий каталожный либо пользовательский профиль.
val raw = NativeDeifBridge.binding(state, field); require(raw.size == 6) { "Неизвестное поле DEIF" } // Шесть скаляров являются границей JNI, а не смещениями CAN-пакета.
return DeifBinding(field, if (raw[5] == 0.0) "" else (0..3).joinToString(" ") { "%02X".format(raw[it].toInt()) }, raw[4]) // Непрозрачные байты форматируются для сохранения и ручного редактирования.
} // Исходные масштабы не дублируются в приложении Android.
@Synchronized override fun close() { NativeDeifBridge.stop(state) } // Жизненный цикл отменяет heartbeat и очередь до закрытия адаптера.
companion object { // Диагностическое представление не содержит второго парсера.
private fun frames(raw: IntArray): List<DeifFrame> { // Все builders возвращают единый скалярный формат JNI.
require(raw.isNotEmpty() && raw.size % 10 == 0) { "Неверные параметры команды DEIF" } // Пустой результат C означает отказ вместо частично сформированного кадра.
return raw.asList().chunked(10).map { row -> DeifFrame(row[0].toLong(), ByteArray(row[1]) { row[it + 2].toByte() }) } // Копирование готовых байтов не интерпретирует wire-поля.
} // Упаковка JNI одинакова для данных MASTER, диммера и служебных команд.
private fun available() { check(NativeSetProtocol.available) { "Не загружено общее C-ядро DEIF" } } // Нативная ошибка не заменяется алгоритмом Kotlin.
private fun selectorBytes(text: String): ByteArray { // Текст HEX является форматом пользовательских настроек, а не вторым кодеком протокола.
val hex = text.filterNot { it.isWhitespace() }; require(hex.isEmpty() || hex.length == 8) { "Селектор DEIF должен содержать четыре HEX-байта либо быть пустым" } // Размер ограничивает доступ JNI к четырёхбайтному селектору.
return hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray() // Порядок байтов сохраняется буквально и интерпретируется только C-ядром.
} // Одинаковая текстовая обработка применяется к привязкам и миграции их адресов.
fun selectorInfo(selector: String): DeifSelectorInfo? { // Редактор сравнивает смысл адреса, не извлекая маски CANopen в Kotlin.
available(); val bytes = selectorBytes(selector); if (bytes.isEmpty()) return null // Отключённое поле остаётся без адреса и не участвует в миграции.
val raw = NativeDeifBridge.selectorInfo(bytes); require(raw.size == 4) { "Неверный селектор DEIF" } // Все четыре семантических поля возвращает существующий C-декодер.
return DeifSelectorInfo(raw[0], raw[1], raw[2], raw[3] != 0) // Именованная модель исключает номера ABI-позиций из Compose.
} // Произвольный пользовательский индекс не заменяется стандартным объектом из базы.
fun readdress(selector: String, node: Int): String { // Перенос адреса сохраняет ручные привязки и режим SAM/DAM.
available(); val bytes = selectorBytes(selector); if (bytes.isEmpty()) return "" // Явно отключённое поле не включается при смене адресов.
val result = NativeDeifBridge.readdress(bytes, node); require(result.size == 4) { "Неверный Node-ID DEIF" } // Новый адрес проверяет и применяет только переносимое C-ядро.
return result.joinToString(" ") { "%02X".format(it.toInt() and 255) } // HEX остаётся прозрачным форматом хранения непрозрачных байтов.
} // Сетевая формула адресного байта не дублируется в порту.
fun master(node: Int, values: IntArray, id: Long, dam: Boolean): List<DeifFrame> { // Порядок пяти объектов и отдельный азимут задаёт только C-каталог.
available(); return frames(NativeDeifBridge.master(node, values, id, dam)) // Адреса и диапазон I16 проверяются до передачи результата.
} // GUI выбирает нужные готовые кадры без повторного построения payload.
fun make(field: Int, node: Int, instance: Int, value: Int, id: Long, dam: Boolean): DeifFrame { // Единый builder поддерживает команды экрана и измерительные данные.
available(); return frames(NativeDeifBridge.make(field, node, instance, value, id, dam)).single() // Ошибка C становится явным отказом API.
} // Формулы индексов, COB-ID и endian остаются в переносимом ядре.
fun service(nmt: Boolean, node: Int, value: Int): DeifFrame { // Локальное NMT отделено от физической валидации MASTER.
available(); return frames(NativeDeifBridge.service(nmt, node, value)).single() // Готовый служебный кадр создаётся существующим C-builder.
} // Порт не собирает двухбайтовую команду вручную.
fun selector(field: Int, node: Int, instance: Int = 1, dam: Boolean = false): ByteArray { // Каталожный редактор может выбирать экземпляр и режим SAM либо DAM.
available(); return NativeDeifBridge.selector(field, node, instance, dam).also { require(it.size == 4) { "Неверный объект, экземпляр или адрес DEIF" } } // Неверный каталог не подменяется произвольным селектором.
} // Преобразование в HEX при отображении остаётся ответственностью GUI.
fun validate(frame: DeifFrame, slave: Int = 0): Boolean { // Перед физической отправкой проверяется роль владельца кадра.
available(); return NativeDeifBridge.validate(frame.canId, frame.data, slave) // MASTER и ответы собственного SLAVE используют тот же C-список разрешений, что Desktop.
} // Произвольные NMT и SDO не открываются через измерительный маршрут MASTER.
fun inspect(id: Long, data: ByteArray, extended: Boolean = false, remote: Boolean = false): IntArray { // Приложение передаёт целый транспортный кадр.
check(NativeSetProtocol.available) { "Не загружено общее C-ядро DEIF" } // Без C-декодера никакого угадывания формата нет.
return NativeDeifBridge.inspect(id, data, (if (extended) 1 else 0) or (if (remote) 2 else 0)) // Индексы результата закреплены ABI deif_xdi.h.
@@ -30,3 +30,56 @@ JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_in
uint8_t d[8]; int32_t m[12]={0}; jint values[13]; int n=deif_input(e,a,d),code; unsigned i; jintArray result; (void)self; code=n<0 || id<0 || id>0x1FFFFFFF?-1:setp_deif_inspect((uint32_t)id,d,(size_t)n,(unsigned)flags,m); /* Только C знает wire-формат DEIF. */
values[0]=code; for(i=0;i<12;i++) values[i+1]=m[i]; result=(*e)->NewIntArray(e,13); if(result) (*e)->SetIntArrayRegion(e,result,0,13,values); return result; /* Метаданные индексируются по ABI, не по смещениям CAN-data. */
} /* Локализованные подписи формирует Kotlin. */
static jintArray deif_frames_java(JNIEnv *e,const setp_deif_frame_t *frames,size_t count) { /* Единая упаковка ABI возвращает готовые кадры, не интерпретируя их payload. */
jint values[60]; size_t i,j; jintArray result; /* Пять объектов MASTER и отдельное задание азимута занимают не более шести записей по десять скаляров. */
if(count>6U) count=0; /* Неверное число записей не должно выходить за пределы локального JNI-буфера. */
for(i=0;i<count;i++) { values[i*10]=(jint)frames[i].can_id; values[i*10+1]=(jint)frames[i].dlc; for(j=0;j<8;j++) values[i*10+2+j]=frames[i].data[j]; } /* Копируются COB-ID, длина и уже построенные C-ядром байты. */
result=(*e)->NewIntArray(e,(jsize)(count*10)); if(result && count) (*e)->SetIntArrayRegion(e,result,0,(jsize)(count*10),values); return result; /* Результатом владеет JVM, указатели C не сохраняются. */
} /* Общий конвертер не является вторым кодеком CANopen. */
JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_master(JNIEnv *e,jobject self,jint node,jintArray input,jlong id,jboolean dam) { /* Порядок объектов и отдельный set-point берутся из публичного C ABI. */
jint raw[5]; int32_t values[5]; setp_deif_frame_t frames[6]; unsigned i; (void)self; /* Локальные буферы имеют фиксированные размеры контракта MASTER. */
if(!input || (*e)->GetArrayLength(e,input)!=5 || id<0 || id>0x7FF) return deif_frames_java(e,NULL,0); /* Проверяем размеры Java-массива и отсутствие усечения идентификатора до вызова C. */
(*e)->GetIntArrayRegion(e,input,0,5,raw); for(i=0;i<5;i++) values[i]=(int32_t)raw[i]; /* JNI преобразует скаляры без извлечения полей протокола. */
if(setp_deif_master((unsigned)node,values,(uint32_t)id,dam!=0,2,frames)!=5 || !setp_deif_angle_command(frames,frames+5)) return deif_frames_java(e,NULL,0); /* Фактический угол и отдельное задание строит то же ядро, что Desktop. */
return deif_frames_java(e,frames,6); /* Потребитель выбирает разрешённые объекты, сохраняя готовые байты каждого кадра. */
} /* Неверная конфигурация не возвращает частично сформированный цикл. */
JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_make(JNIEnv *e,jobject self,jint field,jint node,jint instance,jint value,jlong id,jboolean dam) { /* Общая операция покрывает измерения, яркость и палитру. */
setp_deif_frame_t frame; (void)self; /* Возвращаемая структура заполняется только проверенным builder ядра. */
if(id<0 || id>0x7FF || !setp_deif_make((unsigned)field,(unsigned)node,(unsigned)instance,(int32_t)value,(uint32_t)id,dam!=0,&frame)) return deif_frames_java(e,NULL,0); /* Диапазоны параметров и CAN-адресацию проверяет общий C-код. */
return deif_frames_java(e,&frame,1); /* Kotlin получает один готовый STD data frame. */
} /* UI не знает индексы диммера и расположение I16 в кадре. */
JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_service(JNIEnv *e,jobject self,jboolean nmt,jint node,jint value) { /* Формирует heartbeat или локальный NMT через существующий C builder. */
setp_deif_frame_t frame; (void)self; /* Буфер служебной команды ограничен одним Classic CAN кадром. */
if(!setp_deif_service(nmt!=0,(unsigned)node,(unsigned)value,&frame)) return deif_frames_java(e,NULL,0); /* Адрес, команда и допустимое NMT-состояние проверяются C. */
return deif_frames_java(e,&frame,1); /* Физический либо локальный маршрут выбирается приложением отдельно. */
} /* NMT не попадает в физический маршрут MASTER автоматически. */
JNIEXPORT jbyteArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_selector(JNIEnv *e,jobject self,jint field,jint node,jint instance,jboolean dam) { /* Каталожные привязки формирует единый переносимый справочник. */
uint8_t selector[4]; jbyteArray result; int ok; (void)self; /* Селектор является непрозрачным четырёхбайтовым результатом C. */
ok=setp_deif_selector((unsigned)field,(unsigned)node,(unsigned)instance,dam!=0,selector); result=(*e)->NewByteArray(e,ok?4:0); /* Неизвестный объект возвращает пустой результат для явной ошибки Kotlin. */
if(result && ok) (*e)->SetByteArrayRegion(e,result,0,4,(jbyte *)selector); return result; /* JNI копирует массив без endian-преобразований. */
} /* Android и Desktop используют один каталог объектов DEIF. */
JNIEXPORT jboolean JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_bind(JNIEnv *e,jobject self,jobject buffer,jint field,jbyteArray input,jdouble scale) { /* Ручная привязка передаётся ядру как непрозрачные байты и масштаб. */
uint8_t selector[8]={0}; int count=deif_input(e,input,selector); (void)self; /* Общий копирующий адаптер ограничивает доступ к Java-массиву. */
if(count!=0 && count!=4) return JNI_FALSE; return setp_deif_bind(deif_context(e,buffer),(unsigned)field,count?selector:NULL,(double)scale)?JNI_TRUE:JNI_FALSE; /* Пустая привязка отключает поле; допустимость масштаба проверяет C. */
} /* Обновление привязок не требует знания private-layout контекста. */
JNIEXPORT jdoubleArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_binding(JNIEnv *e,jobject self,jobject buffer,jint field) { /* Чтение текущей привязки исключает вторую таблицу исходных коэффициентов в GUI. */
uint8_t selector[4]={0}; double scale=0; jdouble values[6]; jdoubleArray result; int bound; unsigned i; (void)self; /* Четыре байта, масштаб и флаг активности составляют скалярный JNI-контракт. */
if(field<0 || field>5 || !deif_context(e,buffer)) return (*e)->NewDoubleArray(e,0); bound=setp_deif_get_binding(deif_context(e,buffer),(unsigned)field,selector,&scale); /* Недопустимый номер не должен возвращать выдуманную привязку. */
for(i=0;i<4;i++) values[i]=selector[i]; values[4]=scale; values[5]=bound; result=(*e)->NewDoubleArray(e,6); /* Байты сохраняются скалярами без интерпретации адресного бита или индекса. */
if(result) (*e)->SetDoubleArrayRegion(e,result,0,6,values); return result; /* JVM владеет неизменяемой копией настроек. */
} /* Пользовательский HEX является только форматом хранения профиля. */
JNIEXPORT jboolean JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_validate(JNIEnv *e,jobject self,jlong id,jbyteArray input,jint slave) { /* Последний рубеж различает разрешённые команды MASTER и ответы собственного SLAVE. */
uint8_t data[8]; int count=deif_input(e,input,data); (void)self; /* Payload остаётся ограниченным размером Classic CAN. */
return count>=0 && id>=0 && id<=0x7FF && setp_deif_validate((uint32_t)id,data,(size_t)count,0,(unsigned)slave)?JNI_TRUE:JNI_FALSE; /* Все списки допустимых объектов и служебных кадров находятся в C-ядре. */
} /* NMT и произвольный SDO не могут пройти в физический маршрут MASTER. */
JNIEXPORT jintArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_selectorInfo(JNIEnv *e,jobject self,jbyteArray input) { /* Редактор получает семантические поля селектора через существующий C-декодер. */
uint8_t selector[8]; int32_t metadata[4]; jint values[4]; jintArray result; unsigned i; int ok; (void)self; /* Четыре скаляра описывают Node-ID, индекс, подиндекс и признак DAM. */
ok=deif_input(e,input,selector)==4 && setp_deif_selector_info(selector,metadata); result=(*e)->NewIntArray(e,ok?4:0); /* Порт не извлекает адресные маски либо little-endian индекс повторно. */
if(result && ok) { for(i=0;i<4;i++) values[i]=(jint)metadata[i]; (*e)->SetIntArrayRegion(e,result,0,4,values); } return result; /* Контейнер результата принадлежит JVM. */
} /* Неизвестная ручная библиотечная переменная сохраняет исходный индекс. */
JNIEXPORT jbyteArray JNICALL Java_ru_setcorp_setprotocol_deif_NativeDeifBridge_readdress(JNIEnv *e,jobject self,jbyteArray input,jint node) { /* Смена адреса сохраняет пользовательский объект и режим SAM/DAM. */
uint8_t selector[8],output[4]; jbyteArray result; int ok; (void)self; /* Размеры ограничены публичным четырёхбайтным контрактом привязки. */
ok=deif_input(e,input,selector)==4 && setp_deif_readdress(selector,(unsigned)node,2,output); result=(*e)->NewByteArray(e,ok?4:0); /* Режим 2 существующего C ABI оставляет выбранный ранее SAM либо DAM. */
if(result && ok) (*e)->SetByteArrayRegion(e,result,0,4,(jbyte *)output); return result; /* Kotlin получает непрозрачный новый селектор без самостоятельной сборки адресного байта. */
} /* Автоматическая миграция адресов использует те же операции, что Desktop. */
@@ -0,0 +1,72 @@
package ru.setcorp.setprotocol.deif // Тесты вызывают тот же общий контроллер и C-библиотеку, что Android GUI.
import org.junit.Assert.* // Проверяются точные кадры, маршруты и принятые физические значения.
import org.junit.Test // Обычные JVM-тесты не требуют запущенного Android-устройства.
class DeifWorkspaceTest { // Здесь отсутствует резервная реализация кодека или автомата CANopen.
private fun hex(frame: DeifFrame) = frame.data.joinToString("") { "%02X".format(it.toInt() and 255) } // HEX служит только удобным форматом сравнения готового результата.
@Test fun masterAndDisplayBuildersUseSharedBytes() { // Расширенный JNI-порт должен совпадать с известными C- и Python-векторами.
val frames = DeifXdi.master(11, intArrayOf(900, -967, -509, -2098, -300), 0x30B, true) // DAM-получатель Node 11 использует RPDO2.
assertEquals(6, frames.size); assertTrue(frames.all { it.canId == 0x30BL && DeifXdi.validate(it) }) // Пять рабочих объектов и отдельное задание азимута проходят общую проверку MASTER.
assertEquals("8B01300284030000", hex(frames[0])); assertEquals("8B81300239FC0000", hex(frames[1])) // Угол и отрицательные обороты сохраняют опубликованный I16 LE формат.
assertEquals("8B01300784030000", hex(frames[5])) // Отдельная уставка не подменяет фактический объект угла.
assertEquals("8B0335024B000000", hex(DeifXdi.make(6, 11, 3, 75, 0x30B, true))) // Яркость 75% группы 3 остаётся U8 без масштаба RPM.
assertEquals("8B03350301000000", hex(DeifXdi.make(7, 11, 3, 1, 0x30B, true))) // Палитра Night передаётся отдельным объектом той же группы.
assertFalse(DeifXdi.validate(DeifXdi.service(true, 11, 1))) // Физический маршрут MASTER не разрешает локальную NMT-команду.
} // Числовые ожидания здесь являются эталонными тестовыми данными, а не кодеком порта.
@Test fun offlineSimulatorReceivesInteractiveCommandsWithoutTransport() { // Полный автономный сценарий должен работать без адаптера и исходящих физических кадров.
DeifWorkspace(DeifSettings(node = 11), true).use { session -> // Локальный SLAVE использует такой же Node-ID, как исходная desktop-пара.
assertTrue(runCatching { session.cycle(0) }.isFailure); assertTrue(runCatching { session.display(false, 75, 0) }.isFailure) // Выбранный локальный маршрут не возвращает физические кадры даже до запуска либо после остановки внутреннего SLAVE.
assertTrue(runCatching { session.startMaster(true, 0) }.isFailure); assertFalse(session.masterRunning) // Ошибочный локальный запуск не оставляет активный цикл, ожидающий будущего изменения маршрута.
session.startSlave(true, 0); assertTrue(session.tick(0).slave.isEmpty()) // Boot-up остаётся внутри локального стенда.
session.nmt(1, 0); session.setControl("angle", 37.1); session.setControl("rpm", 40.32); session.setControl("power", 63.9) // Касания воспроизводят задания со снимка пользователя.
val traffic = session.cycle(1); val snapshot = session.snapshot(1) // Передача проходит реальный C-builder и автомат приёма SLAVE.
assertTrue(traffic.master.isEmpty() && traffic.slave.isEmpty()); assertEquals(6, snapshot.accepted) // Локальный маршрут не вызывает внешний транспорт даже для heartbeat.
assertEquals(37.1, snapshot.angle!!, 0.001); assertEquals(403.2, snapshot.values[1]!!, 0.001); assertEquals(63.9, snapshot.values[3]!!, 0.001) // Угол, rpm и процент мощности сохраняют правильные масштабы.
session.display(false, 75, 2); session.display(true, 1, 2) // Яркость и Color также доступны без оборудования.
assertEquals(75, session.snapshot(2).brightness); assertEquals(1, session.snapshot(2).palette) // Проверяется применение в нативной модели, а не только изменение UI.
session.nmt(2, 3); session.setControl("rpm", 80.0); session.cycle(4) // NMT Stop прекращает обработку новых PDO.
assertEquals(403.2, session.snapshot(4).values[1]!!, 0.001); assertEquals(4, session.snapshot(4).nmt) // Прежнее принятое значение не подменяется новым жёлтым заданием MASTER.
} // Закрытие владельца прекращает таймеры и очередь C.
} // Тест не содержит USB, COM и Android Context.
@Test fun physicalSlaveCannotBecomeMasterOrSendNmt() { // Собственный узел на физической шине должен иметь отдельный ограниченный маршрут.
DeifWorkspace(DeifSettings(node = 13), false).use { session -> // Контроллер возвращает реальные серверные кадры платформенному передатчику.
session.startSlave(true, 0); val boot = session.tick(0).slave.single() // Boot-up должен быть выдан ровно один раз.
assertEquals(0x70DL, boot.canId); assertEquals("00", hex(boot)); assertTrue(DeifXdi.validate(boot, 13)) // Ответ разрешён только от собственного Node-ID.
assertTrue(runCatching { session.cycle(1) }.isFailure); assertTrue(runCatching { session.startMaster(true, 1) }.isFailure) // Кнопка и прямой программный вызов одинаково запрещают конкурирующий MASTER.
assertTrue(runCatching { session.nmt(1, 1) }.isFailure); assertTrue(session.tick(1).slave.isEmpty()) // Локальный NMT не отправляется физическому узлу ни при каком выборе меню.
} // Сервер закрывается до освобождения транспортного подключения приложения.
} // Внешний MASTER управляет жизненным циклом этого SLAVE через обычный RX.
@Test fun settingsAndManualBindingsSurvivePersistence() { // Сохранение должно охватывать профиль, калибровки, выбор объектов и явные привязки.
val selector = DeifXdi.selector(2, 3).joinToString(" ") { "%02X".format(it.toInt() and 255) } // Тест получает каталожный объект без ручной сборки селектора.
val original = DeifSettings(source = 3, xdi = false, automatic = false, pdo = 4, group = 9, powerFull = 2500.0, selected = listOf(true, false, true, true, false), bindings = listOf(DeifBinding(2, selector, 0.25))) // Несовпадающие значения обнаруживают пропущенные поля сериализации.
val restored = DeifSettings.decode(original.encode()); assertEquals(original, restored); restored.validate() // Профиль восстанавливается без автоматического запуска сервера.
restored.model().use { model -> assertEquals(original.bindings.single(), model.binding(2)) } // Ручной масштаб и объект действительно применяются к C-состоянию.
assertTrue(runCatching { restored.copy(xdi = true, pdo = 1).validate() }.isFailure) // C-профиль запрещает занятый SAM RPDO1 для XDi-net.
assertTrue(runCatching { restored.copy(angleFactor = 0.0).validate() }.isFailure) // Нулевой масштаб не должен давать фиктивный угол управления.
} // Проверяется реальное восстановление общего профиля, а не зеркальная копия реализации тестом.
@Test fun periodicMasterHasIndependentHeartbeatAndNoCatchupBurst() { // Пауза GUI не должна создавать лавину просроченных повторов.
DeifWorkspace(DeifSettings(periodMs = 100, heartbeat = true), false).use { session -> // Два периода принадлежат общему контроллеру порта.
session.startMaster(true, 0); assertEquals(7, session.tick(0).master.size) // На старте выдаются шесть рабочих кадров и один Operational heartbeat.
assertTrue(session.tick(20).master.isEmpty()); assertEquals(6, session.tick(100).master.size) // Ранний тик ничего не отправляет, рабочий цикл наступает через 100 миллисекунд.
assertEquals(7, session.tick(2000).master.size); assertTrue(session.tick(2001).master.isEmpty()) // После двухсекундной паузы выдаётся один цикл вместо очереди устаревших повторов.
session.stopMaster(); assertTrue(session.tick(3000).master.isEmpty()) // Остановка прекращает также независимый heartbeat MASTER.
} // Пассивный наблюдатель не принимает исходящие задания за физические измерения.
} // Монотонное время задаётся явно, без задержек и нестабильных тестовых таймеров.
@Test fun addressChangesPreserveObjectsAndIndependentSources() { // Перенос профиля должен менять только связанный адрес, сохраняя пользовательские объекты и масштабы.
fun selector(field: Int, node: Int, dam: Boolean = false) = DeifXdi.selector(field, node, 2, dam).joinToString(" ") { "%02X".format(it.toInt() and 255) } // Эталонные селекторы создаёт общий каталог с явно выбранным экземпляром 2.
val profile = DeifSettings(node = 13, source = 1, bindings = listOf(DeifBinding(0, selector(0, 1), 0.5), DeifBinding(1, selector(1, 13, true), -0.1), DeifBinding(2, selector(2, 7), 0.25), DeifBinding(3, "", 0.1))) // Включены SAM, собственный DAM, независимый источник и отключённое поле.
val moved = profile.copy(node = 14, source = 3).readdressed(13, 1) // Новый профиль переносит только адреса прежнего собственного узла и выбранного SAM-источника.
assertEquals(selector(0, 3), moved.bindings[0].selector); assertEquals(selector(1, 14, true), moved.bindings[1].selector) // SAM и DAM следуют разным пользовательским адресам.
assertEquals(profile.bindings[2], moved.bindings[2]); assertEquals(profile.bindings[3], moved.bindings[3]) // Независимый источник и явное отключение не меняются.
assertEquals(0.5, moved.bindings[0].scale, 0.0); assertEquals(-0.1, moved.bindings[1].scale, 0.0) // Перенос адреса не сбрасывает индивидуальную калибровку либо инверсию.
assertTrue(DeifXdi.selectorInfo(moved.bindings[1].selector)!!.dam); assertEquals(14, DeifXdi.selectorInfo(moved.bindings[1].selector)!!.node) // Семантический разбор режима и узла выполняется тем же C API.
} // Отдельная привязка азимута продолжает управлять приоритетом факта и set-point в C.
@Test fun invalidNativeCommandsAndProfileShapesFailExplicitly() { // Ошибочные параметры JNI должны давать явный отказ без усечения и Kotlin fallback.
assertTrue(runCatching { DeifXdi.master(1, intArrayOf(0), 0x201, false) }.isFailure) // Неполный цикл не превращается в частичную отправку пяти объектов.
assertTrue(runCatching { DeifXdi.master(1, IntArray(5), 0x100000201L, false) }.isFailure) // Идентификатор не должен молча усекаться до допустимого STD CAN ID.
assertTrue(runCatching { DeifXdi.make(6, 1, 1, 101, 0x201, false) }.isFailure); assertTrue(runCatching { DeifXdi.make(7, 1, 1, 3, 0x201, false) }.isFailure) // Диапазон яркости и палитры проверяет существующий C-builder.
assertTrue(runCatching { DeifXdi.readdress("01 81 30 02", 128) }.isFailure); assertTrue(runCatching { DeifXdi.selectorInfo("01 81") }.isFailure) // Неверный Node-ID и короткий селектор не проходят границу JNI.
assertTrue(runCatching { DeifSettings.decode("selected=true,false") }.isFailure) // Повреждённый сохранённый профиль не должен давать выход за границы списка в Compose.
DeifSettings().model().use { model -> assertTrue(runCatching { model.bind(DeifBinding(1, "01 81 30 02", Double.NaN)) }.isFailure) } // Неконечный масштаб не попадает в показания нативной модели.
} // Ошибки проверяются через публичные методы того же порта, которым пользуется Android GUI.
} // Этот же набор тестов подключается к sourceSets Android из templates.
+51 -23
View File
@@ -13,18 +13,41 @@ static size_t seal(uint8_t *p,size_t n) {
uint16_t crc=set_crc16_modbus(p,n);p[n]=(uint8_t)crc;p[n+1]=(uint8_t)(crc>>8);return n+2;
}
void set_wave_init(set_wave_state *s,uint16_t *samples,size_t capacity,const set_wave_port *port) {
memset(s,0,sizeof(*s));s->samples=samples;s->capacity=capacity;s->rate=1000;
memset(s,0,sizeof(*s));s->samples=samples;s->capacity=capacity;s->rate=1000;s->channels=1; /* После reset сохраняет совместимость с прежним одноканальным клиентом. */
if(port) s->port=*port;
}
void set_wave_enable_xy(set_wave_state *s,uint32_t *pairs,int (*start)(void *,const uint32_t *,size_t,uint32_t)) { /* Плата явно объявляет доступность второго выхода и его SRAM. */
s->xy_samples=pairs;s->start_xy=start; /* Нулевой callback оставляет устройство одноканальным, включая прежние F103 и G474RB. */
} /* Подключение порта не запускает периферию и не меняет таблицу. */
static uint16_t sample_get(const set_wave_state *s,unsigned index) { /* Читает выбранную координату без перестановки готовых пар перед DMA. */
return s->channels==2?(uint16_t)((s->xy_samples[index]>>(16u*s->selected))&4095u):s->samples[index]; /* Полуслова DHR12RD разделены 16 битами; код каждого DAC занимает 12 бит. */
} /* Одноканальный формат uint16_t остаётся прежним. */
static void sample_set(set_wave_state *s,unsigned index,uint16_t value) { /* Изменяет только выбранную координату, сохраняя парный отсчёт другого канала. */
if(s->channels==2) { /* Двухканальная таблица уже хранится в формате единственной 32-битной передачи DMA. */
unsigned shift=16u*s->selected; /* Сдвиг 0 выбирает X/PA4, сдвиг 16 выбирает Y/PA5 по DHR12RD из RM0090/RM0440. */
s->xy_samples[index]=(s->xy_samples[index]&~(0xffffu<<shift))|((uint32_t)value<<shift); /* Очищает полуслово вместе с резервными битами и записывает проверенный 12-битный код. */
} else s->samples[index]=value; /* Сохраняет прежнюю компактную таблицу одиночного канала. */
} /* Проверки диапазона выполняет вызывающий обработчик до изменения памяти. */
static unsigned write_register(set_wave_state *s,unsigned reg,unsigned value) {
if(reg==SET_WAVE_BASE+2 && value==0) {
if(s->port.stop) s->port.stop(s->port.context);
s->running=0; return 0;
}
if(reg==SET_WAVE_BASE+10) { /* Выбор X/Y меняет только окно загрузки и чтения, поэтому доступен и при воспроизведении. */
uint16_t previous; /* Временно сохраняет счётчик прежней координаты при переключении окна. */
if(value>=s->channels)return 3; /* Одноканальная плата отвергает Y; произвольные индексы не меняют состояние. */
if(value!=s->selected) {previous=s->received;s->received=s->received_other;s->received_other=previous;s->selected=(uint16_t)value;} /* Меняет местами счётчики, сохраняя оба независимо от порядка загрузки. */
return 0; /* Выбор окна не снимает commit и не меняет фазу аппаратного вывода. */
} /* Остальные изменения таблицы запрещены во время running. */
if(s->running) return 6;
if(reg==SET_WAVE_BASE+9) { /* Число одновременно воспроизводимых каналов задаётся до начала новой загрузки. */
if(value<1||value>2||(value==2&&(!s->xy_samples||!s->start_xy)))return 3; /* Двойной режим допустим только с буфером пар и аппаратным исполнителем. */
s->channels=(uint16_t)value;s->selected=0;s->count=s->received=s->received_other=s->ready=0; /* Смена формата инвалидирует обе координаты, исключая запуск смешанной старой таблицы. */
return 0; /* Пуск остаётся отдельной командой после полного commit. */
} /* Прежние регистры частоты и длины сохраняют адреса. */
if(reg==SET_WAVE_BASE+2 && value==1) {
if(!s->ready||!s->port.start) return 3;
if(s->port.start(s->port.context,s->samples,s->count,s->rate)) return 4;
if(s->channels==2?s->start_xy(s->port.context,s->xy_samples,s->count,s->rate):s->port.start(s->port.context,s->samples,s->count,s->rate)) return 4; /* XY запускается одной аппаратной транзакцией; ошибка порта не выставляет running. */
s->running=1;return 0;
}
if(reg==SET_WAVE_BASE+3) {
@@ -38,37 +61,38 @@ static unsigned write_register(set_wave_state *s,unsigned reg,unsigned value) {
}
if(reg==SET_WAVE_BASE+4) {
if(value<2||value>SET_WAVE_MAX||value>s->capacity) return 3;
s->count=(uint16_t)value;s->received=s->ready=0;return 0;
s->count=(uint16_t)value;s->received=s->received_other=s->ready=0; /* Новая длина начинает загрузку обеих координат с нуля. */return 0;
}
if(reg>=SET_WAVE_DATA&&reg<SET_WAVE_DATA+SET_WAVE_MAX) {
unsigned index=reg-SET_WAVE_DATA;
if(index>=s->count||index>s->received||value>4095||s->ready) return 3;
if(index<s->received) return s->samples[index]==value?0:3;
s->samples[index]=(uint16_t)value;++s->received;return 0;
if(index<s->received) return sample_get(s,index)==value?0:3; /* Повтор подтверждается только при совпадении выбранной координаты; соседнее полуслово пары не учитывается. */
sample_set(s,index,(uint16_t)value); /* Готовит 16-битный отсчёт либо соответствующее полуслово XY. */++s->received;return 0;
}
if(reg==SET_WAVE_BASE+6) {
if(value!=0xA55A||s->count<2||s->received!=s->count) return 3;
if(value!=0xA55A||s->count<2||s->received!=s->count||(s->channels==2&&s->received_other!=s->count)) return 3; /* Общий commit требует полные X и Y одинаковой длины. */
s->ready=1;return 0;
}
return 2;
}
unsigned set_wave_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out) {
set_wave_state *s=(set_wave_state *)ctx;uint16_t w[9];unsigned i;
if(!s||!s->samples||!out||!count||(uint32_t)off+count>9)return 2;
set_wave_state *s=(set_wave_state *)ctx;uint16_t w[13]; /* Девять прежних слов дополнены режимом, селектором, возможностями и вторым счётчиком XY. */unsigned i;
if(!s||!s->samples||!out||!count||(uint32_t)off+count>13)return 2; /* Проверяет весь диапазон расширенного статуса до чтения четырёх дополнительных слов XY. */
w[0]=0x5747;w[1]=3;w[2]=s->running;w[3]=(uint16_t)s->rate;
w[4]=s->count;w[5]=s->received;w[6]=s->ready;w[7]=(uint16_t)s->capacity;w[8]=(uint16_t)(s->rate>>16);
w[9]=s->channels;w[10]=s->selected;w[11]=s->start_xy?2:1;w[12]=s->received_other; /* Расширение 0x1309…0x130C не меняет WG v3 и прежний ответ из восьми слов. */
for(i=0;i<count;++i)out[i]=w[off+i];
return 0;
}
unsigned set_wave_write(void *ctx,uint16_t off,uint16_t value) {
set_wave_state *s=(set_wave_state *)ctx;
if(!s||!s->samples||off>=9)return 2;
if(!s||!s->samples||off>=11)return 2; /* Запись разрешена до селектора 0x130A включительно; возможности и второй счётчик доступны только для чтения. */
return write_register(s,SET_WAVE_BASE+off,value);
}
unsigned set_wave_samples_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out) {
set_wave_state *s=(set_wave_state *)ctx;unsigned i;
if(!s||!s->samples||!out||!count||(uint32_t)off+count>s->received)return 2;
for(i=0;i<count;++i)out[i]=s->samples[off+i];
for(i=0;i<count;++i)out[i]=sample_get(s,off+i) /* Возвращает выбранный канал из общей таблицы пар. */;
return 0;
}
unsigned set_wave_samples_write(void *ctx,uint16_t off,uint16_t value) {
@@ -77,21 +101,22 @@ unsigned set_wave_samples_write(void *ctx,uint16_t off,uint16_t value) {
return write_register(s,SET_WAVE_DATA+off,value);
}
size_t set_wave_rtu(set_wave_state *s,unsigned device,const uint8_t *req,size_t n,uint8_t *out,size_t cap) {
unsigned reg,value,error=0,i;uint16_t status[8];size_t length=0;
unsigned reg,value,error=0,i;uint16_t status[13]; /* Вмещает прежний статус и дополнительные регистры XY без изменения старого запроса. */size_t length=0;
if(!s||!s->samples||!req||!out||cap<21||n!=8||device<1||device>247
||req[0]!=device||set_crc16_modbus(req,n)) return 0;
reg=read16(req+2);value=read16(req+4);
if(!((reg>=SET_WAVE_BASE&&reg<SET_WAVE_BASE+9)||(reg>=SET_WAVE_DATA&&reg<SET_WAVE_DATA+SET_WAVE_MAX))) return 0;
if(!((reg>=SET_WAVE_BASE&&reg<SET_WAVE_BASE+13)||(reg>=SET_WAVE_DATA&&reg<SET_WAVE_DATA+SET_WAVE_MAX))) return 0; /* Общий RTU-кодек узнаёт расширенное окно XY и прежнюю таблицу без захвата чужих адресов. */
out[0]=(uint8_t)device;out[1]=req[1];
if(req[1]==6) {error=write_register(s,reg,value);memcpy(out,req,6);length=6;}
else if(req[1]==3) {
(void)set_wave_read(s,0,8,status);
if(reg==SET_WAVE_BASE&&value==8) {
out[2]=16;for(i=0;i<8;++i) write16(out+3+2*i,status[i]);length=19;
} else if(reg==SET_WAVE_BASE+8&&value==1) {
out[2]=2;write16(out+3,s->rate>>16);length=5;
} else if(reg>=SET_WAVE_DATA&&value==1&&reg-SET_WAVE_DATA<s->received) {
out[2]=2;write16(out+3,s->samples[reg-SET_WAVE_DATA]);length=5;
if(reg>=SET_WAVE_BASE&&reg<SET_WAVE_BASE+13&&value&&value<=13) { /* Отдельный RTU-кодек читает те же регистры, что общий сервер SET v2. */
error=set_wave_read(s,(uint16_t)(reg-SET_WAVE_BASE),(uint16_t)value,status); /* Проверяет весь диапазон до формирования ответа. */
if(!error) { /* Ошибочный диапазон возвращает исключение без чтения за пределами статуса. */
if(cap<5u+2u*value)return 0; /* Не пишет ответ расширенного статуса в прежний короткий буфер. */
out[2]=(uint8_t)(2u*value);for(i=0;i<value;++i)write16(out+3+2*i,status[i]);length=3u+2u*value; /* Отдаёт точное запрошенное число слов с прежним порядком байтов Modbus. */
} /* CRC добавляется общим seal после формирования тела. */
} else if(reg>=SET_WAVE_DATA&&value==1&&reg-SET_WAVE_DATA<s->received) { /* Чтение одного отсчёта сохраняет старый wire-контракт. */
out[2]=2;write16(out+3,sample_get(s,reg-SET_WAVE_DATA));length=5; /* XY возвращает выбранную координату, mono возвращает прежний uint16_t. */
} else error=2;
} else error=1;
if(error) {out[1]|=0x80;out[2]=(uint8_t)error;length=3;}
@@ -111,6 +136,9 @@ size_t set_wave_request(unsigned device,unsigned op,unsigned index,unsigned valu
case 7: if(index>=SET_WAVE_MAX)return 0;reg=SET_WAVE_DATA+index;fn=3;value=1;break;
case 8: reg+=8;fn=3;value=1;break;
case 9: reg+=8;if(value>(SET_WAVE_MAX_RATE>>16))return 0;break;
case 10: reg+=9;if(value<1||value>2)return 0;break; /* Выбирает один или два синхронных канала через существующий RTU/SET v2 транспорт. */
case 11: reg+=10;if(value>1)return 0;break; /* Выбирает X=0 либо Y=1 для прежних команд sample/block/readback. */
case 12: reg+=9;fn=3;value=4;break; /* Читает режим, селектор, возможности порта и количество отсчётов второй координаты. */
default:return 0;
}
out[0]=(uint8_t)device;out[1]=(uint8_t)fn;write16(out+2,reg);write16(out+4,value);return seal(out,6);
@@ -131,7 +159,7 @@ int set_wave_response(const uint8_t *req,const uint8_t *reply,size_t size,uint16
if(req[1]==6) {if(memcmp(req,reply,8)||cap<1)return -1;words[0]=read16(req+4);return 1;}
if(reply[2]!=expected-5||cap<(expected-5)/2)return -1;
for(i=0;i<(expected-5)/2;++i)words[i]=read16(reply+3+2*i);
if(read16(req+2)==SET_WAVE_BASE&&(words[0]!=0x5747||(words[1]<1||words[1]>3)))return -2;
if(read16(req+2)==SET_WAVE_BASE&&read16(req+4)>=2&&(words[0]!=0x5747||(words[1]<1||words[1]>3)))return -2; /* Версия проверяется только при наличии двух слов, исключая чтение за границей короткого ответа. */
return (int)((expected-5)/2);
}
@@ -168,13 +196,13 @@ size_t set_wave_block_rtu(set_wave_state *s,unsigned device,const uint8_t *req,
/* Preflight the whole block, including replayed prefix, before writing. */
for(i=0;i<count;++i) {
unsigned value=read16(req+7+2*i);
if(value>4095||(index+i<s->received&&s->samples[index+i]!=value)) {
if(value>4095||(index+i<s->received&&sample_get(s,index+i)!=value)) {
error=3;break;
}
}
if(!error) {
if(cap<8)return 0;
for(i=0;i<count;++i)s->samples[index+i]=read16(req+7+2*i);
for(i=0;i<count;++i)sample_set(s,index+i,read16(req+7+2*i)); /* После preflight записывает весь блок только выбранной координаты. */
if(index+count>s->received)s->received=(uint16_t)(index+count);
memcpy(out,req,6);length=6;
}
@@ -184,7 +212,7 @@ size_t set_wave_block_rtu(set_wave_state *s,unsigned device,const uint8_t *req,
else {
if(cap<5+2*(size_t)count)return 0;
out[2]=(uint8_t)(2*count);
for(i=0;i<count;++i)write16(out+3+2*i,s->samples[index+i]);
for(i=0;i<count;++i)write16(out+3+2*i,sample_get(s,index+i)); /* Чтение блоков использует тот же селектор, что и запись. */
length=3+2*(size_t)count;
}
} else error=1;
+36 -15
View File
@@ -57,7 +57,14 @@ class WaveTransfer(QRunnable):
result = protocol.status(words)
if result['version'] >= 2:
result = protocol.status(words, exchange(8)[0])
return result
try: # Дополнительные регистры объявляют XY независимо от версии прежнего восьмисловного статуса.
channels,selected,maximum,other=exchange(12) # Четыре слова 0x1309…0x130C читаются общим C-кодеком.
result.update(channels=channels,selected=selected,max_channels=maximum,received_other=other) # Сохраняет фактические возможности порта и состояние обеих таблиц.
except ValueError as error: # Старые WG v1/v2/v3 отвечают исключением адреса либо имеют прежний native-кодек.
if "(-102)" not in str(error) and "Недопустимая команда" not in str(error): # Ошибки CRC и структуры ответа нельзя скрывать как одноканальную плату.
raise # Сохраняет штатный отказ передачи при повреждённом ответе.
result.update(channels=1,max_channels=1) # Старый клиент и прошивка сохраняют одноканальную работу без новых записей.
return result # Возможности XY проверяются до stop и изменения таблиц устройства.
status = read_status() # Positive device identification before any write.
if self.action == "upload":
@@ -66,27 +73,40 @@ class WaveTransfer(QRunnable):
raise ValueError("Таблица не помещается в память устройства")
if status['version'] == 1 and wave.sample_rate > 50000:
raise ValueError("Для частоты выше 50 000 отсчётов/с обновите прошивку МК: нужна карта генератора версии 2")
channel_codes=(wave.codes,) if not hasattr(wave,"codes_y") else (wave.codes,wave.codes_y) # Одна временная сетка содержит один канал либо обе координаты XY.
if len(channel_codes)>status["max_channels"]: # Устройство обязано явно подтвердить аппаратный порт второго канала.
raise ValueError("Для XY нужны прошивка F407/HOPE и native-кодек templates с поддержкой двух каналов") # Отказ происходит до остановки ранее работающего выхода.
if any(len(codes)!=len(wave.codes) for codes in channel_codes): # Нельзя загружать пару с разными длинами даже из вручную созданного задания.
raise ValueError("X и Y должны иметь одинаковую длину") # Не меняет оборудование при несовместимой временной сетке.
exchange(1)
if status["max_channels"]>=2: # Новый порт явно сбрасывает формат перед каждой загрузкой, включая возврат XY→mono.
exchange(10,value=len(channel_codes)) # Выбирает число одновременно воспроизводимых каналов и инвалидирует обе прежние таблицы.
if status['version'] >= 2:
exchange(9, value=wave.sample_rate >> 16)
exchange(2, value=wave.sample_rate & 0xffff)
exchange(3, value=len(wave.codes))
blocks = status['version'] >= 3 and getattr(port, 'supports_wave_blocks', False)
step = 120 if blocks else 1
for index in range(0, len(wave.codes), step):
values = wave.codes[index:index + step]
if blocks:
transfer(protocol.block_request(index, values), protocol.block_response)
else:
exchange(4, index, values[0])
self.signals.progress.emit(int((index + len(values)) * 50 / len(wave.codes)))
for index in range(0, len(wave.codes), step):
values = wave.codes[index:index + step]
actual = (transfer(protocol.block_request(index, count=len(values)), protocol.block_response)
if blocks else exchange(7, index))
if tuple(actual) != tuple(values):
raise ValueError(f"Проверка таблицы не пройдена: отсчёты с {index}")
self.signals.progress.emit(50 + int((index + len(values)) * 50 / len(wave.codes)))
total=len(wave.codes)*len(channel_codes)*2 # Прогресс учитывает запись и чтение каждого кода обеих координат.
completed=0 # Общий счётчик исключает откат процентов при переходе от X к Y.
for channel,codes in enumerate(channel_codes): # Загружает и проверяет X, затем Y без промежуточного пуска.
if status["max_channels"]>=2: # Прежняя прошивка не получает неподдерживаемый селектор окна.
exchange(11,value=channel) # Одинаковые block/sample команды адресуют выбранную координату через общий регистр 0x130A.
for reading in (False,True): # Обратное чтение всей координаты обязательно до общего commit пары.
for index in range(0,len(codes),step): # Блочный транспорт ограничен прежними 120 кодами на запрос.
values=codes[index:index+step] # Последний блок содержит только действительные отсчёты без дополнения.
if reading: # Проверка использует ту же границу блока и тот же селектор, что запись.
actual=transfer(protocol.block_request(index,count=len(values)),protocol.block_response) if blocks else exchange(7,index) # Читает реальные коды из памяти устройства.
if tuple(actual)!=tuple(values): # Любое расхождение запрещает общий commit и дальнейший пуск.
raise ValueError(f"Проверка {'XY'[channel]} не пройдена: отсчёты с {index}") # Показывает ошибочную координату и начало блока.
elif blocks: # SET v2 переносит тот же атомарно проверяемый RTU-блок из общего ядра templates.
transfer(protocol.block_request(index,values),protocol.block_response) # Подтверждает запись всего блока до следующего запроса.
else: # Старый UART/RTU клиент сохраняет поотсчётную загрузку без смены wire-контракта.
exchange(4,index,values[0]) # Записывает очередной код выбранной координаты.
completed+=len(values) # Каждый подтверждённый код учитывается ровно один раз на фазу.
self.signals.progress.emit(int(completed*100/total)) # Общий прогресс монотонно достигает 100 % после проверки обеих таблиц.
if status["max_channels"]>=2: # Итоговый статус возвращается к X для одинакового отображения mono и XY.
exchange(11,value=0) # Не меняет содержимое пар и не запускает аппаратный вывод.
exchange(5)
elif self.action == "start":
if not status["ready"]:
@@ -98,6 +118,7 @@ class WaveTransfer(QRunnable):
raise ValueError("Неизвестное действие")
result = read_status()
if self.action == "upload" and (not result["ready"] or result["running"] or
result.get("channels",1)!=(2 if hasattr(self.waveform,"codes_y") else 1) or # Итоговое число каналов обязано совпасть с подготовленным mono/XY заданием.
result["count"] != len(self.waveform.codes) or result["rate"] != self.waveform.sample_rate):
raise ValueError("Устройство не подтвердило загруженную таблицу")
if self.action in ("start", "stop") and result["running"] != (self.action == "start"):
+12
View File
@@ -0,0 +1,12 @@
from dataclasses import dataclass, fields # Неизменяемая таблица наследует прежнюю модель, сохраняя сравнение обеих координат перед пуском.
from .waveform import Waveform # Общие частота, точки X, Vref и период принадлежат прежней модели генератора.
from .signal_reconstruction import dac12 # Напряжения Y квантует то же C-ядро, что используется одиночным выходом X.
@dataclass(frozen=True) # Изменение любого кода Y создаёт другое задание и требует повторной загрузки перед start.
class XYWaveform(Waveform): # Дополняет один общий период второй синхронной координатой без дублирования настройки таймера.
codes_y: tuple # Ровно один 12-битный код Y соответствует каждому коду X с тем же индексом и временем.
def with_y(wave, volts): # Добавляет вертикальное отклонение к уже построенной и проверенной временной сетке X.
values=tuple(volts) # Однократно фиксирует последовательность, включая генераторы Python.
if len(values)!=len(wave.codes): # Оба DMA-канала должны содержать строго одинаковое число пар, без дополнения и обрезки.
raise ValueError("X и Y должны иметь одинаковое число отсчётов") # Ошибка выявляется до открытия порта и изменения состояния устройства.
codes=dac12(values,wave.vref) # Общая проверка отвергает NaN, бесконечность и выход Y за диапазон 0…Vref.
return XYWaveform(**{field.name:getattr(wave,field.name) for field in fields(Waveform)},codes_y=codes) # Сохраняет точные коды X и добавляет Y без интерполяции и изменения частоты.
+23
View File
@@ -0,0 +1,23 @@
# Внешние проекты
Этот каталог хранит исходные репозитории самостоятельных приложений,
используемых как основа для изучения и дальнейшей интеграции в инструменты.
Они подключены отдельно от переносимых ядер в `c/`, `cpp/` и `python/`.
## KDiff3
- Каталог: [`kdiff3`](kdiff3).
- Официальный Git: <https://invent.kde.org/sdk/kdiff3.git>.
- Тип подключения: вложенный Git submodule с зафиксированным коммитом.
- Исходная ревизия: `49189d0` от 26 октября 2026 года.
- Лицензии и сведения об авторах сохранены в исходных `COPYING` и `LICENSES/`.
- Назначение: исходники программы сравнения и объединения файлов и каталогов
для развития инструментов SETGUI.
При клонировании потребителя нужно инициализировать submodule рекурсивно,
чтобы получить также вложенный KDiff3. Обновление исходников выполняется
отдельно с явным изменением зафиксированной ревизии.
Встроенная вкладка SETGUI пока использует свои панели Qt и Python `difflib`.
Добавление этого репозитория само по себе не заменяет её движок и не запускает
внешнее приложение.
Vendored Submodule
+1
Submodule third_party/kdiff3 added at 49189d01a4