From bf5f094249404f57522f35a098ed8a393d459a6e Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 29 Oct 2026 23:32:03 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20DEIF=20=D0=B8=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D1=80=20=D1=81=D0=B8=D0=B3=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2,=20=D0=BF=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B8=D1=81=D1=85=D0=BE=D0=B4=D0=BD=D0=B8?= =?UTF-8?q?=D0=BA=D0=B8=20KDiff3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавить общую модель сигналов XY и адаптировать генератор к двум каналам. Расширить Android-порт DEIF общим контроллером рабочей области и проверками. Подключить официальный репозиторий KDiff3 вложенным подмодулем. --- .gitmodules | 3 + README.md | 4 + c/set-protocol/docs/DEIF_XDI.md | 28 ++++ c/set-protocol/include/set_wavegen.h | 4 + .../setcorp/setprotocol/deif/DeifWorkspace.kt | 134 ++++++++++++++++++ .../ru/setcorp/setprotocol/deif/DeifXdi.kt | 51 +++++++ c/set-protocol/ports/android/set_deif_jni.c | 53 +++++++ .../setprotocol/deif/DeifWorkspaceTest.kt | 72 ++++++++++ c/set-protocol/src/set_wavegen.c | 74 +++++++--- python/set_devices/qt_ports/wavegen.py | 51 +++++-- python/set_devices/waveform_xy.py | 12 ++ third_party/README.md | 23 +++ third_party/kdiff3 | 1 + 13 files changed, 472 insertions(+), 38 deletions(-) create mode 100644 .gitmodules create mode 100644 c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifWorkspace.kt create mode 100644 c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/deif/DeifWorkspaceTest.kt create mode 100644 python/set_devices/waveform_xy.py create mode 100644 third_party/README.md create mode 160000 third_party/kdiff3 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..71ac551 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/kdiff3"] + path = third_party/kdiff3 + url = https://invent.kde.org/sdk/kdiff3.git diff --git a/README.md b/README.md index 0c1001c..88cd789 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/c/set-protocol/docs/DEIF_XDI.md b/c/set-protocol/docs/DEIF_XDI.md index 598cb13..5247930 100644 --- a/c/set-protocol/docs/DEIF_XDI.md +++ b/c/set-protocol/docs/DEIF_XDI.md @@ -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. diff --git a/c/set-protocol/include/set_wavegen.h b/c/set-protocol/include/set_wavegen.h index 7ca876c..da94891 100644 --- a/c/set-protocol/include/set_wavegen.h +++ b/c/set-protocol/include/set_wavegen.h @@ -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, diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifWorkspace.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifWorkspace.kt new file mode 100644 index 0000000..d2833f7 --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifWorkspace.kt @@ -0,0 +1,134 @@ +package ru.setcorp.setprotocol.deif // Общий контроллер UI находится в templates и использует единственный C-кодек через JNI. +import java.io.StringReader // Текст пользовательского профиля читается без Android Context и доступа к файловой системе. +import java.io.StringWriter // Профиль возвращается вызывающему GUI для сохранения в его собственном хранилище. +import java.util.Properties // Стандартный формат JVM хранит настройки без дополнительной зависимости от JSON-библиотеки. +import kotlin.math.roundToInt // Округление относится к переводу жеста интерфейса в заданный пользователем масштаб управления. + +data class DeifBinding(val field: Int, val selector: String, val scale: Double) // Привязка содержит непрозрачный HEX-селектор и пользовательский множитель индикации. +data class DeifSelectorInfo(val node: Int, val index: Int, val subindex: Int, val dam: Boolean) // Метаданные текущего селектора получены из C и не раскрывают wire-разметку приложению. +data class DeifCommands(val power: Double = 0.0, val rpm: Double = 0.0, val angle: Double = 0.0) // POWER и RPM задаются процентом шкалы, угол — градусами. +data class DeifTraffic(val master: List = emptyList(), val slave: List = emptyList()) // Раздельные маршруты не позволяют командам MASTER выдать себя за ответы SLAVE. +data class DeifSettings( // Сохраняемые параметры независимы от временного запуска сервера и циклической передачи. + val node: Int = 13, val source: Int = 1, val xdi: Boolean = true, val automatic: Boolean = true, // Собственный адрес SLAVE и источник SAM имеют независимый смысл. + val pdo: Int = 2, val group: Int = 1, val monitorId: Long = 0, val bindings: List = emptyList(), // RPDO, группа экрана и ручные привязки передаются непосредственно C-профилю. + val masterNode: Int = 1, val masterId: Long = 0x201, val masterDam: Boolean = false, // Адресация MASTER не изменяется при выборе собственного узла SLAVE. + val periodMs: Int = 100, val heartbeat: Boolean = false, val heartbeatNode: Int = 1, // Период цикла в миллисекундах независим от штатного heartbeat каждые 500 мс. + val selected: List = List(5) { true }, // Порядок соответствует публичному ABI цикла MASTER: угол, RPM, RPM%, POWER и POWER%. + val powerFull: Double = 1000.0, val rpmFull: Double = 1000.0, val angleFactor: Double = 10.0, val powerPercentFactor: Double = 10.0, // Калибровки интерфейса: POWER RAW при 100%, rpm при 100%, RAW/градус и RAW/процент. + val powerUnit: String = "RAW", // Неподтверждённая физическая единица мощности не подменяется автоматически киловаттами. +) { // Конструктор данных не загружает JNI при запуске приложения с другим протоколом. + init { require(selected.size == 5) { "Профиль MASTER должен содержать пять флажков отправки" } } // Повреждённый текст настроек не должен приводить к выходу за границы списка в интерфейсе. + fun readdressed(previousNode: Int, previousSource: Int): DeifSettings { // Изменение адресов переносит только связанные с прежним узлом привязки, сохраняя независимые источники. + return copy(bindings = bindings.map { binding -> // Каждое поле сохраняет собственный индекс, подиндекс и физический масштаб. + val info = DeifXdi.selectorInfo(binding.selector) // Адрес и SAM/DAM сообщает общий C-декодер без масок и endian-копии в Kotlin. + val target = if (info?.dam == true && info.node == previousNode) node else if (info?.dam == false && info.node == previousSource) source else null // DAM следует физическому получателю, SAM — выбранному источнику рассылки. + if (target != null) binding.copy(selector = DeifXdi.readdress(binding.selector, target)) else binding // Независимые ручные источники и отключённые поля остаются прежними. + }) // Применение возвращает один полный профиль для последующей общей проверки. + } // Смена SAM-источника не должна оставлять стандартные поля подписанными на старый Node-ID. + fun model(services: Boolean = false): DeifXdi { // Оба GUI-режима получают профиль единственного переносимого автомата. + val result = DeifXdi(node, source, xdi, automatic, pdo, group, services, monitorId) // Все сетевые диапазоны и исходные привязки проверяет C-конструктор. + try { bindings.forEach { result.bind(it) } } catch (error: Exception) { result.close(); throw error } // Неверная ручная привязка не оставляет частично настроенный сервер. + return result // Владение контекстом и его закрытием получает контроллер сеанса. + } // Создание пассивного профиля ничего не отправляет в CAN. + fun validate() { // Перед применением проверяются и сетевой профиль, и настройки управления интерфейсом. + require(periodMs >= 0 && selected.size == 5 && group in 1..9) { "Проверьте период, группу экрана и выбор объектов DEIF" } // Размер списка закреплён публичным ABI MASTER, номер группы выбирается явно. + require(listOf(powerFull, rpmFull, angleFactor, powerPercentFactor).all { it.isFinite() && it > 0 }) { "Масштабы управления должны быть положительными числами" } // Нулевой либо бесконечный множитель не должен создавать недостоверные задания. + require(bindings.map { it.field }.distinct().size == bindings.size && bindings.all { it.field in 0..4 }) { "Привязки пяти показаний должны быть уникальными" } // Угол задания автоматически обслуживается C-привязкой фактического азимута. + model(true).close() // Самый строгий серверный профиль проверяет RPDO, протокол и оба адреса до изменения UI. + DeifXdi.master(masterNode, IntArray(5), masterId, masterDam) // Общий builder проверяет точный COB-ID и адресацию передатчика. + DeifXdi.service(false, heartbeatNode, 5) // Допустимость адреса heartbeat также проверяет существующий C-builder. + } // Ошибка оставляет прежний работающий профиль без изменений. + fun encode(): String { // Хранилище приложения получает только текст настроек без состояния работающих таймеров. + val p = Properties() // Стандартные ключи JVM сохраняют совместимость Android и host-тестов. + listOf("node" to node, "source" to source, "xdi" to xdi, "automatic" to automatic, "pdo" to pdo, "group" to group, "monitorId" to monitorId, "masterNode" to masterNode, "masterId" to masterId, "masterDam" to masterDam, "periodMs" to periodMs, "heartbeat" to heartbeat, "heartbeatNode" to heartbeatNode, "powerFull" to powerFull, "rpmFull" to rpmFull, "angleFactor" to angleFactor, "powerPercentFactor" to powerPercentFactor, "powerUnit" to powerUnit).forEach { (key, value) -> p.setProperty(key, value.toString()) } // Сохраняются только явные настройки, без флагов запуска SLAVE и MASTER. + p.setProperty("selected", selected.joinToString(",")) // Включение отдельных объектов восстанавливается независимо от значений заданий. + bindings.forEach { p.setProperty("binding.${it.field}", "${it.scale}|${it.selector}") } // HEX хранится непрозрачно, индекс и endian в Kotlin не вычисляются. + return StringWriter().also { p.store(it, "DEIF") }.toString() // Запись в SharedPreferences или файл остаётся задачей платформенного слоя. + } // Сериализация не вызывает CAN-транспорт или системные часы протокола. + companion object { // Загрузка профиля не требует Android API и не запускает нативный сервер. + fun decode(text: String): DeifSettings { // Старый либо отсутствующий профиль получает исходные значения DEIF. + val p = Properties().apply { load(StringReader(text)) } // Properties снимает только текстовое экранирование формата хранения. + fun value(key: String, default: Any) = p.getProperty(key, default.toString()) // Каждый новый параметр имеет явное совместимое исходное значение. + val defaults = DeifSettings() // Единый набор исходных параметров не дублируется в приложении Android. + return DeifSettings(node = value("node", defaults.node).toInt(), source = value("source", defaults.source).toInt(), xdi = value("xdi", true).toBooleanStrict(), automatic = value("automatic", true).toBooleanStrict(), // Адреса и профиль восстанавливаются без объявления узла на CAN-шине. + pdo = value("pdo", 2).toInt(), group = value("group", 1).toInt(), monitorId = value("monitorId", 0).toLong(), bindings = (0..4).mapNotNull { field -> p.getProperty("binding.$field")?.split('|', limit = 2)?.let { DeifBinding(field, it[1], it[0].toDouble()) } }, // Список ручных привязок сохраняет индивидуальные масштабы и отключённые поля. + masterNode = value("masterNode", 1).toInt(), masterId = value("masterId", defaults.masterId).toLong(), masterDam = value("masterDam", false).toBooleanStrict(), // Собственный MASTER остаётся независимым от адреса получателя SLAVE. + periodMs = value("periodMs", 100).toInt(), heartbeat = value("heartbeat", false).toBooleanStrict(), heartbeatNode = value("heartbeatNode", 1).toInt(), selected = value("selected", "true,true,true,true,true").split(',').map { it.toBooleanStrict() }, // Восстанавливаем выбор объектов без автоматического запуска циклической отправки. + powerFull = value("powerFull", 1000).toDouble(), rpmFull = value("rpmFull", 1000).toDouble(), angleFactor = value("angleFactor", 10).toDouble(), powerPercentFactor = value("powerPercentFactor", 10).toDouble(), powerUnit = value("powerUnit", "RAW")) // Калибровки и единицы относятся к интерфейсу конкретной библиотеки прибора. + } // Ошибку повреждённого текста вызывающий GUI показывает либо заменяет безопасными исходными настройками. + } // Конструктор не содержит скрытого алгоритмического fallback вместо C. +} // Один профиль используется локальным стендом, пассивным наблюдателем и физическим сервером. + +class DeifWorkspace(val settings: DeifSettings, val local: Boolean) : AutoCloseable { // Общий контроллер управляет маршрутом, а протокольный автомат остаётся в C. + private var model = settings.model() // Исходный пассивный наблюдатель не производит исходящих кадров. + private val values = IntArray(5) // Значения MASTER хранятся отдельно от принятых измерений модели SLAVE. + var slaveEnabled = false; private set // Запуск сервера всегда требует отдельного явного действия пользователя. + var masterRunning = false; private set // Открытие экрана и восстановление настроек не запускают передачу. + var sent = 0L; private set // Счётчик показывает число переданных маршруту кадров без обещания физической доставки. + var lastService: DeifFrame? = null; private set // Последний служебный кадр помогает диагностировать NMT, heartbeat и SDO. + var lastRpm: DeifFrame? = null; private set // Последний выбранный кадр RPM доступен диагностике без повторной распаковки в GUI. + var cycleGapMs: Long? = null; private set // Интервал между вызовами рабочего цикла измеряется в переданных порту монотонных миллисекундах. + var maxCycleGapMs = 0L; private set // Максимальный интервал выявляет паузы интерфейса, не подтверждая доставку CAN-кадра. + private var lastCycleAt: Long? = null // До первого цикла не показывается выдуманное измерение интервала отправки. + private var nextCycle = 0L; private var nextHeartbeat = 0L // Дедлайны порта используют переданные монотонные миллисекунды без чтения часов в библиотеке. + val commands: DeifCommands get() = DeifCommands(values[3] / settings.powerFull * 100, values[1] / 10.0 / settings.rpmFull * 100, values[0] / settings.angleFactor) // Жёлтые маркеры строятся из фактически ограниченных INT16 заданий. + fun setControl(key: String, value: Double) { // Жест меняет только черновик MASTER и никогда сам не выполняет отправку. + require(value.isFinite()) { "Задание DEIF должно быть конечным числом" } // Некорректная координата интерфейса не попадает в конструктор CAN-кадра. + fun raw(number: Double) = number.coerceIn(-32767.0, 32765.0).roundToInt() // Рабочий диапазон I16 исключает три служебных значения протокола при управлении мышью или касанием. + when (key) { // Ключи описывают визуальные элементы, а не индексы wire-объектов. + "angle" -> values[0] = raw(value * settings.angleFactor) // Градусы переводятся по пользовательской калибровке RAW на градус. + "power" -> { values[3] = raw(value / 100 * settings.powerFull); values[4] = raw(values[3] / settings.powerFull * 100 * settings.powerPercentFactor) } // Процент мощности согласуется с фактически ограниченным абсолютным заданием. + "rpm" -> { values[1] = raw(value / 100 * settings.rpmFull * 10); values[2] = raw(values[1] / 10.0 / settings.rpmFull * 1000) } // Обороты и их процент используют ту же визуальную калибровку, что Desktop. + else -> error("Неизвестный регулятор DEIF") // Ошибка UI не должна молча менять другую физическую величину. + } // Никакие байты CAN здесь не формируются. + } // Последующая разовая либо циклическая отправка использует единый набор значений. + fun startSlave(enabled: Boolean, now: Long) { // Локальный стенд и физический сервер различаются только маршрутом готовых кадров. + stopMaster() // Предыдущий MASTER не должен продолжить отправку после переключения роли. + model.close(); model = settings.model(enabled); slaveEnabled = enabled // Новый профиль не наследует старые измерения и незавершённые служебные события. + if (enabled) model.start(now) // Boot-up, self-start и NMT-состояние создаются C-ядром. + } // События выдаются через tick, а физическую отправку выполняет платформа. + fun startMaster(enabled: Boolean, now: Long) { // Таймер MASTER включается независимо от таймера служебного heartbeat SLAVE. + require(!enabled || !local || slaveEnabled) { "Для локального обмена сначала запустите симуляцию SLAVE" } // Отказ маршрута происходит до изменения флага запуска, а не на первом тике таймера. + require(!enabled || !slaveEnabled || local) { "В режиме CAN-SLAVE передаёт внешний MASTER" } // Физический сервер не конкурирует со встроенным передатчиком приложения. + masterRunning = enabled; nextCycle = now; nextHeartbeat = now; lastCycleAt = null; cycleGapMs = null; maxCycleGapMs = 0 // Новый сеанс начинает отсчёт периодов и статистики с явного запуска. + } // При periodMs=0 порт передаёт не более одного цикла за очередной вызов tick. + fun stopMaster() { masterRunning = false } // Остановка не изменяет принятые физические показания SLAVE. + private fun events(now: Long): List { // Доставка очереди C выполняется также при отсутствии новых входящих кадров. + val frames = model.poll(now); frames.lastOrNull()?.let { lastService = it } // Сохраняем последний готовый сервисный кадр без повторного разбора payload. + return if (local && slaveEnabled) emptyList() else frames // На локальном стенде Boot-up, SDO и heartbeat не выходят в физический адаптер. + } // Один poll извлекает каждый кадр из очереди только один раз. + private fun route(frames: List, now: Long): DeifTraffic { // Решение о локальном маршруте относится к порту, а не к wire-протоколу. + require(!local || slaveEnabled) { "Для локального обмена сначала запустите симуляцию SLAVE" } // Выбранный локальный режим никогда не перенаправляется в физический CAN при остановленном сервере. + sent += frames.size // Счётчик не является подтверждением приёма физическим прибором. + if (local && slaveEnabled) { frames.forEach { model.accept(it.canId, it.data, nowMs = now) }; events(now); return DeifTraffic() } // Внутренний стенд принимает точные C-байты без вызова транспорта. + return DeifTraffic(master = frames) // Внешний маршрут возвращает кадры вызывающему приложению с отдельной проверкой MASTER. + } // Черновик задания не подменяет физический RX-снимок. + fun cycle(now: Long): DeifTraffic { // Один цикл использует C-каталог пяти объектов и отдельного задания азимута. + require(!local || slaveEnabled) { "Для локального обмена сначала запустите симуляцию SLAVE" } // Прямой вызов разового цикла не обходит выбор автономного маршрута пользователя. + require(!slaveEnabled || local) { "В режиме CAN-SLAVE передаёт внешний MASTER" } // Прямой вызов кнопки также не обходит ограничения физического сервера. + val service = events(now) // Сначала учитываем self-start и доставляем ранее накопленные служебные ответы. + val built = DeifXdi.master(settings.masterNode, values, settings.masterId, settings.masterDam) // Проверенный C-builder формирует полный цикл до выбора отправляемых объектов. + cycleGapMs = lastCycleAt?.let { (now - it).coerceAtLeast(0) }; maxCycleGapMs = maxOf(maxCycleGapMs, cycleGapMs ?: 0); lastCycleAt = now // Отмечается интервал вызовов порта, а не сетевой ACK либо срок Data lost. + lastRpm = if (settings.selected[1]) built[1] else null // Семантическая позиция RPM закреплена публичным контрактом цикла MASTER, payload не разбирается. + val frames = built.filterIndexed { index, _ -> settings.selected[if (index < 5) index else 0] } // Отдельный set-point следует выбору общего объекта угла. + return route(frames, now).copy(slave = service) // Все выбранные кадры получают единый локальный либо физический маршрут. + } // Частично собранный цикл не возвращается при ошибке builder ядра. + fun tick(now: Long): DeifTraffic { // Android поставляет время, а сроки self-start и Data lost вычисляет C. + var traffic = DeifTraffic(slave = events(now)) // Служебный таймер обслуживается независимо от передачи MASTER. + if (masterRunning && now >= nextCycle) { val cycle = cycle(now); traffic = DeifTraffic(traffic.master + cycle.master, traffic.slave + cycle.slave); nextCycle = now + settings.periodMs.coerceAtLeast(1) } // Один вызов не создаёт пачку просроченных повторов после паузы GUI. + if (masterRunning && settings.heartbeat && now >= nextHeartbeat) { val heartbeat = route(listOf(DeifXdi.service(false, settings.heartbeatNode, 5)), now); traffic = DeifTraffic(traffic.master + heartbeat.master, traffic.slave + heartbeat.slave); nextHeartbeat = now + 500 } // Штатный Operational heartbeat MASTER имеет независимый период 500 миллисекунд. + return traffic // Порт физического транспорта получает только готовые кадры с сохранёнными ролями. + } // Все действия повторяемы при одних и тех же переданных монотонных часах. + fun accept(frame: DeifFrame, extended: Boolean, remote: Boolean, now: Long) { // Внешний RX не должен изменять внутренний локальный стенд. + if (!local || !slaveEnabled) model.accept(frame.canId, frame.data, extended, remote, now) // STD, RTR, адреса и известные объекты проверяет общий C-код. + } // TX-эхо и ошибки адаптера отфильтровывает платформенный слой. + fun nmt(command: Int, now: Long) { // Локальное управление NMT сохраняет то же ограничение, что Desktop. + require(local && slaveEnabled) { "NMT доступен только в локальной симуляции SLAVE" } // Команды управления сетью не отправляются физическому MASTER. + val frame = DeifXdi.service(true, settings.node, command); model.accept(frame.canId, frame.data, nowMs = now); events(now) // Адресный двухбайтный NMT строится и обрабатывается только C-ядром. + } // Выбор пункта меню сам по себе не вызывает этот метод. + fun display(palette: Boolean, value: Int, now: Long, group: Int = settings.group): DeifTraffic { // Подтверждение фиксирует группу отдельно от ещё не сохранённого черновика остальных настроек. + return route(listOf(DeifXdi.make(if (palette) 7 else 6, settings.masterNode, group, value, settings.masterId, settings.masterDam)), now) // Выбранная группа, U8 яркости и Day/Night/Dusk проверяются общим C-builder. + } // Команды яркости и палитры не включаются в циклическую отправку MASTER. + fun snapshot(now: Long): DeifSnapshot = model.snapshot(now) // Числа, статусы, угол и Data lost поступают из единственного C-автомата. + override fun close() { stopMaster(); model.close(); slaveEnabled = false } // Закрытие владельца прекращает и циклические команды, и служебную очередь SLAVE. +} // Контроллер не содержит Android, Compose, системных часов, USB и второго CAN-кодека. diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifXdi.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifXdi.kt index 77c42b9..e98daa6 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifXdi.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/deif/DeifXdi.kt @@ -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 { // Все 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 { // Порядок пяти объектов и отдельный азимут задаёт только 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. diff --git a/c/set-protocol/ports/android/set_deif_jni.c b/c/set-protocol/ports/android/set_deif_jni.c index a637be5..f77f6a0 100644 --- a/c/set-protocol/ports/android/set_deif_jni.c +++ b/c/set-protocol/ports/android/set_deif_jni.c @@ -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;iNewIntArray(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. */ diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/deif/DeifWorkspaceTest.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/deif/DeifWorkspaceTest.kt new file mode 100644 index 0000000..c56cfbd --- /dev/null +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setprotocol/deif/DeifWorkspaceTest.kt @@ -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. diff --git a/c/set-protocol/src/set_wavegen.c b/c/set-protocol/src/set_wavegen.c index 1b8d199..e46ef2d 100644 --- a/c/set-protocol/src/set_wavegen.c +++ b/c/set-protocol/src/set_wavegen.c @@ -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<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&®=s->count||index>s->received||value>4095||s->ready) return 3; - if(indexreceived) return s->samples[index]==value?0:3; - s->samples[index]=(uint16_t)value;++s->received;return 0; + if(indexreceived) 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;isamples||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;isamples[off+i]; + for(i=0;isamples||!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&®=SET_WAVE_DATA&®=SET_WAVE_BASE&®=SET_WAVE_DATA&®rate>>16);length=5; - } else if(reg>=SET_WAVE_DATA&&value==1&®-SET_WAVE_DATAreceived) { - out[2]=2;write16(out+3,s->samples[reg-SET_WAVE_DATA]);length=5; + if(reg>=SET_WAVE_BASE&®=SET_WAVE_DATA&&value==1&®-SET_WAVE_DATAreceived) { /* Чтение одного отсчёта сохраняет старый 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;i4095||(index+ireceived&&s->samples[index+i]!=value)) { + if(value>4095||(index+ireceived&&sample_get(s,index+i)!=value)) { error=3;break; } } if(!error) { if(cap<8)return 0; - for(i=0;isamples[index+i]=read16(req+7+2*i); + for(i=0;is->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;isamples[index+i]); + for(i=0;i= 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"): diff --git a/python/set_devices/waveform_xy.py b/python/set_devices/waveform_xy.py new file mode 100644 index 0000000..3a1d018 --- /dev/null +++ b/python/set_devices/waveform_xy.py @@ -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 без интерполяции и изменения частоты. diff --git a/third_party/README.md b/third_party/README.md new file mode 100644 index 0000000..6c6746c --- /dev/null +++ b/third_party/README.md @@ -0,0 +1,23 @@ +# Внешние проекты + +Этот каталог хранит исходные репозитории самостоятельных приложений, +используемых как основа для изучения и дальнейшей интеграции в инструменты. +Они подключены отдельно от переносимых ядер в `c/`, `cpp/` и `python/`. + +## KDiff3 + +- Каталог: [`kdiff3`](kdiff3). +- Официальный Git: . +- Тип подключения: вложенный Git submodule с зафиксированным коммитом. +- Исходная ревизия: `49189d0` от 26 октября 2026 года. +- Лицензии и сведения об авторах сохранены в исходных `COPYING` и `LICENSES/`. +- Назначение: исходники программы сравнения и объединения файлов и каталогов + для развития инструментов SETGUI. + +При клонировании потребителя нужно инициализировать submodule рекурсивно, +чтобы получить также вложенный KDiff3. Обновление исходников выполняется +отдельно с явным изменением зафиксированной ревизии. + +Встроенная вкладка SETGUI пока использует свои панели Qt и Python `difflib`. +Добавление этого репозитория само по себе не заменяет её движок и не запускает +внешнее приложение. diff --git a/third_party/kdiff3 b/third_party/kdiff3 new file mode 160000 index 0000000..49189d0 --- /dev/null +++ b/third_party/kdiff3 @@ -0,0 +1 @@ +Subproject commit 49189d01a49b7498c878053bdb4240d63f24f0bb