diff --git a/doc/setcan/protocan/GAS_ADDRESS_ALLOCATION_167.md b/doc/setcan/protocan/GAS_ADDRESS_ALLOCATION_167.md new file mode 100644 index 0000000..9d38746 --- /dev/null +++ b/doc/setcan/protocan/GAS_ADDRESS_ALLOCATION_167.md @@ -0,0 +1,97 @@ +# Сквозная нумерация GAS: распределение адресов + +Дата: **9 октября 2026 года**. + +УКСС выделен диапазон **0…11999**: общий блок 0 и блоки узлов 1…11, по **1000 ID** каждый. Соответствие технологических слов Modbus текущему профилю 167/v1 и новым ID УМП приведено в [GAS_MODBUS_MAP.md](GAS_MODBUS_MAP_167.md). Новая нумерация назначена в документации; в прошивке пока действует профиль 167/v1. + +## Диапазоны проектов + +В новой схеме числа без префикса `0x` — **десятичные**. Каждому номеру узла соответствует блок 1000 адресов с базой `номер × 1000`. + +| Проект / семейство | Первый ID | Последний ID включительно | Число адресов | Статус | +|---|---:|---:|---:|---| +| УКСС | 0 | 11999 | 12000 | Выделено: 12 блоков по 1000 ID | +| Остальные проекты | 12000 | 65535 | 53536 | Свободно для последующего распределения | + +Границы УКСС в hex: `0x0000…0x2EDF`; свободная область начинается с `0x2EE0`. + +Резерв диапазона не означает, что каждый адрес уже реализован. Адреса обозначают логические 16-битные слова; память для незадействованных ID не выделяется. Параметр шириной 32 бита занимает два последовательных слова. Служебные слова также должны входить в выделенный диапазон проекта. + +## Узлы УКСС + +Номера узлов: **1…11**; **5 и 6 — УМП**. Назначения 1…9 подтверждены в `package.h` (проект tms_periph/periph_167); служебные адреса 10/11 определены в `cntrl_adr.c` (проект tms_periph/periph_167). + +| Номер | Назначение в текущем коде | Обозначение | +|---:|---|---| +| 1 | Трансформаторный узел 1 | `adr_TRN1` | +| 2 | Трансформаторный узел 2 | `adr_TRN2` | +| 3 | Силовой узел 1 | `adr_POW1` | +| 4 | Силовой узел 2 | `adr_POW2` | +| 5 | УМП 1, LOAD | `adr_LOA1` | +| 6 | УМП 2, LOAD | `adr_LOA2` | +| 7 | Узел ENG1 | `adr_ENG1` | +| 8 | Пульт | `adr_PULT` | +| 9 | Шкаф | `adr_SHKF` | +| 10 | Универсальный адрес / адрес общей посылки | `CNTRL_ADDR_UNIVERSAL`, `ADDR_FOR_ALL` | +| 11 | Адрес терминала | `ADDR_TERMINAL` | + +Номер узла, тип платы `Desk` и ID параметра GAS — отдельные величины. Например, УМП 1 имеет `Mode = 5`, а тип обеих плат УМП равен `dsk_LOAD = 6`. Блок номера 10 зарезервирован для роли общего адреса; существующий номер 10 не идентифицирует отдельную физическую плату. + +## Назначенные блоки УКСС + +Границы включительные. Все блоки содержат по 1000 слов, пересечений нет. + +| Область | Первый ID, dec | Последний ID, dec | База, hex | +|---|---:|---:|---| +| Блок 0: общий резерв УКСС | 0 | 999 | `0x0000` | +| Трансформаторный узел 1, номер 1 | 1000 | 1999 | `0x03E8` | +| Трансформаторный узел 2, номер 2 | 2000 | 2999 | `0x07D0` | +| Силовой узел 1, номер 3 | 3000 | 3999 | `0x0BB8` | +| Силовой узел 2, номер 4 | 4000 | 4999 | `0x0FA0` | +| УМП 1, номер 5 | 5000 | 5999 | `0x1388` | +| УМП 2, номер 6 | 6000 | 6999 | `0x1770` | +| Узел ENG1, номер 7 | 7000 | 7999 | `0x1B58` | +| Пульт, номер 8 | 8000 | 8999 | `0x1F40` | +| Шкаф, номер 9 | 9000 | 9999 | `0x2328` | +| Резерв роли общего адреса, номер 10 | 10000 | 10999 | `0x2710` | +| Терминал, номер 11 | 11000 | 11999 | `0x2AF8` | + +Правило адресации: + +```text +GAS_ID = номер_узла × 1000 + смещение +номер_узла = 0…11 +смещение = 0…999 +``` + +## Технологические слова УМП + +Для штатного технологического Modbus-адреса `n = 0…127` в новой карте назначено смещение `n`: + +```text +УМП 1, номер 5: GAS_ID = 5000 + n → 5000…5127 +УМП 2, номер 6: GAS_ID = 6000 + n → 6000…6127 +``` + +| Параметр | Штатный Modbus, hex | УМП 1: новый GAS, dec | УМП 2: новый GAS, dec | +|---|---|---:|---:| +| Первое технологическое слово | `0x0000` | 5000 | 6000 | +| `DAC_go` | `0x0050` | 5080 | 6080 | +| `DAC_stop` | `0x0051` | 5081 | 6081 | +| `LOAD_CAN_TIMEOUT` | `0x0052` | 5082 | 6082 | +| `LOAD_STATUS` | `0x007C` | 5124 | 6124 | +| `LOAD_ATTEMPTS` | `0x007D` | 5125 | 6125 | +| `LastMode` | `0x007E` | 5126 | 6126 | +| `Commands` | `0x007F` | 5127 | 6127 | + +Смещения **128…999** в каждом блоке остаются резервом для расширений. Служебные параметры и новые данные в этих смещениях пока не назначены. Полная таблица всех 128 технологических слов: [GAS_MODBUS_MAP.md](GAS_MODBUS_MAP_167.md). + +## Связь с текущей прошивкой + +В существующей реализации база технологических GAS-регистров равна **`0x5000` = 20480**, а дескриптор находится на **`0x5F00` = 24320**. Это локальное окно профиля 167/v1, выбранное независимо от новой схемы сквозной нумерации. + +Текущее `0x5000` не означает `5 × 1000`: последнее вычисление даёт **5000 десятичных = `0x1388`**. Для адресации УМП 1 и УМП 2 по номеру узла потребуются разные базы, определяемые по `Mode`. + +Штатные адреса Modbus, служебные окна регистратора, паспорта, защиты и EEPROM уже имеют собственные значения. При переносе сквозной карты нужно определить соответствие GAS ↔ адрес протокола: глобальный ID может отличаться от прежнего Modbus-адреса. Это позволяет отдельно согласовать новую нумерацию и совместимость существующих клиентов. + +Диапазон УКСС и размеры блоков назначены. Перенос этих ID в обработчики прошивки и размещение служебных параметров в резерве — отдельные изменения реализации. diff --git a/doc/setcan/protocan/GAS_MODBUS_MAP_167.md b/doc/setcan/protocan/GAS_MODBUS_MAP_167.md new file mode 100644 index 0000000..13bba27 --- /dev/null +++ b/doc/setcan/protocan/GAS_MODBUS_MAP_167.md @@ -0,0 +1,288 @@ + +# GAS ↔ Modbus: карта регистров Бальзам 167 + +## Использование в SETGUI + +В `Терминал → TMS28335 → periph_28335 → rs232_periph_28335 → Работа с GAS` +доступны словарь 128 технологических слов, поиск, чтение диапазона и запись FC06. +Порт и адрес контроллера используются из общих настроек RS232/485. +Кнопка «Проверить профиль» читает восемь слов с `0x5F00`; запись разрешается +только после подтверждения GAS 167/v1, M95M02 и возможностей RTU/FC06. +Проверка сбрасывается при смене соединения/адреса, ошибке и отмене обмена. +Все 128 слов читаются двумя запросами: смещение 0, количество 124, затем +кнопкой «Прочитать 0x507C…0x507F». Таблица показывает raw и HEX без +неподтверждённых преобразований единиц. + +Машинный словарь: `python/protocan/gas_167_registers.json` проекта templates. +В общей вкладке Modbus имена и пары новых ID УМП 1/2 подставляются для FC03 +по штатным адресам и действующему окну GAS; сохранённые пользовательские +имена имеют приоритет. Обмен этой вкладки GAS реализован через RS232/485. + +Ниже сохранена карта из проекта `tms_periph/periph_167`, коммит `378e06a`. +Ссылки на исходники относятся к дереву того проекта. + +Профиль: **GAS 167/v1**. Соответствие проверено по исходникам проекта на **9 октября 2026 года**. + +УКСС выделен сквозной диапазон **0…11999** с блоком **1000 ID** на номер узла: [GAS_ADDRESS_ALLOCATION.md](GAS_ADDRESS_ALLOCATION_167.md). В полной таблице приведены текущие адреса прошивки 167/v1 и новые назначенные ID УМП 1/2. Новая нумерация пока не перенесена в прошивку. + +GAS предоставляет доступ к тем же значениям `modbus[]`, что и штатные адреса Modbus. В Modbus RTU можно обращаться как по прежнему адресу `0x0000…0x007F`, так и по GAS-адресу `0x5000…0x507F`. Это два адреса одного слова RAM; отдельной копии параметров нет. + +**GAS доступен только при `EEPROM_M95M02=1`.** В текущем `eeprom_config.h` (проект tms_periph/periph_167) по умолчанию задано `0` — сборка для AT25256B, в которой GAS-окна отсутствуют. Фактическая сборка может переопределять этот параметр. + +## Правило соответствия + +```text +Для штатного адреса n = 0…127: + GAS-адрес = 0x5000 + n + Штатный Modbus = GAS-адрес - 0x5000 + Источник значения = modbus[n] + +Пример: modbus[0x52] → штатный адрес 0x0052 (82) + → GAS-адрес 0x5052 (20562) + +Новая сквозная карта (десятичные ID): + УМП 1, номер 5: GAS_ID = 5000 + n + УМП 2, номер 6: GAS_ID = 6000 + n + modbus[0x52] → УМП 1: 5082; УМП 2: 6082 +``` + +Столбцы Modbus и GAS 167/v1 содержат **нулевые адреса из поля запроса**, без прибавления единицы и без префикса `4xxxx`. Столбцы «Новый ID УМП 1/2» содержат назначенные ID новой сквозной карты; текущая прошивка ещё не обслуживает их как новые GAS-адреса. Каждый адрес обозначает одно **16-битное слово raw**. Знаковость, единицы и масштаб зависят от параметра и типа платы `Desk`; GAS передаёт исходное слово. + +| Область | Штатный Modbus | GAS / адрес обращения через Modbus RTU | Слова | Доступ | +|---|---|---|---:|---| +| Технологические регистры | `0x0000…0x007F` | `0x5000…0x507F` | 128 | RW с исключениями LOAD | +| Информация GAS-профиля | Нет штатного аналога | `0x5F00…0x5F07` | 8 | RO | + +`RW` в документе означает, что обработчик разрешает FC06. Измерения и статусы могут затем обновляться прошивкой; разрешение записи само по себе не делает такой регистр уставкой. `RO` означает, что обработчик отклоняет запись. + +## Полное соответствие 128 технологических регистров + +Имена взяты из `package.h` (проект tms_periph/periph_167), `measure.c` (проект tms_periph/periph_167), `ADC.c` (проект tms_periph/periph_167) и `DAC.c` (проект tms_periph/periph_167). Индексы `sens_*` начинаются с нуля. Конкретное назначение каналов зависит от платы; таблица не назначает всем платам одинаковые физические каналы. + +Перекрывающиеся имена относятся к одному слову и используются в разных режимах. Например, `0x0052` входит в общий диапазон `sens_lo_edge[]`, а на LOAD служит `LOAD_CAN_TIMEOUT`. + +| Modbus, hex | Modbus, dec | GAS 167/v1, hex | GAS 167/v1, dec | Новый ID УМП 1, dec | Новый ID УМП 2, dec | Имя / источник | Доступ | +|---|---:|---|---:|---:|---:|---|---| +| `0x0000` | 0 | `0x5000` | 20480 | 5000 | 6000 | `sens_error[0]`; `InputRep0/1/2` — биты A/B/C | RW | +| `0x0001` | 1 | `0x5001` | 20481 | 5001 | 6001 | `sens_error[1]` | RW | +| `0x0002` | 2 | `0x5002` | 20482 | 5002 | 6002 | `sens_error[2]` | RW | +| `0x0003` | 3 | `0x5003` | 20483 | 5003 | 6003 | `sens_error[3]` | RW | +| `0x0004` | 4 | `0x5004` | 20484 | 5004 | 6004 | `sens_error[4]` | RW | +| `0x0005` | 5 | `0x5005` | 20485 | 5005 | 6005 | `sens_error[5]` | RW | +| `0x0006` | 6 | `0x5006` | 20486 | 5006 | 6006 | `sens_error[6]` | RW | +| `0x0007` | 7 | `0x5007` | 20487 | 5007 | 6007 | `sens_error[7]` | RW | +| `0x0008` | 8 | `0x5008` | 20488 | 5008 | 6008 | `sens_error[8]` | RW | +| `0x0009` | 9 | `0x5009` | 20489 | 5009 | 6009 | `sens_error[9]` | RW | +| `0x000A` | 10 | `0x500A` | 20490 | 5010 | 6010 | `sens_error[10]` | RW | +| `0x000B` | 11 | `0x500B` | 20491 | 5011 | 6011 | `sens_error[11]` | RW | +| `0x000C` | 12 | `0x500C` | 20492 | 5012 | 6012 | `sens_error[12]` | RW | +| `0x000D` | 13 | `0x500D` | 20493 | 5013 | 6013 | `sens_error[13]` | RW | +| `0x000E` | 14 | `0x500E` | 20494 | 5014 | 6014 | `sens_error[14]` | RW | +| `0x000F` | 15 | `0x500F` | 20495 | 5015 | 6015 | `sens_error[15]` | RW | +| `0x0010` | 16 | `0x5010` | 20496 | 5016 | 6016 | `sens_error[16]`; `Inputs` | RW | +| `0x0011` | 17 | `0x5011` | 20497 | 5017 | 6017 | `sens_error[17]` | RW | +| `0x0012` | 18 | `0x5012` | 20498 | 5018 | 6018 | `sens_error[18]` | RW | +| `0x0013` | 19 | `0x5013` | 20499 | 5019 | 6019 | `sens_error[19]` | RW | +| `0x0014` | 20 | `0x5014` | 20500 | 5020 | 6020 | `sens_error[20]` | RW | +| `0x0015` | 21 | `0x5015` | 20501 | 5021 | 6021 | `sens_error[21]` | RW | +| `0x0016` | 22 | `0x5016` | 20502 | 5022 | 6022 | `sens_error[22]`; `Buttons` | RW | +| `0x0017` | 23 | `0x5017` | 20503 | 5023 | 6023 | `sens_error[23]`; `Jumpers` | RW | +| `0x0018` | 24 | `0x5018` | 20504 | 5024 | 6024 | `sens_data[0]` | RW | +| `0x0019` | 25 | `0x5019` | 20505 | 5025 | 6025 | `sens_data[1]` | RW | +| `0x001A` | 26 | `0x501A` | 20506 | 5026 | 6026 | `sens_data[2]` | RW | +| `0x001B` | 27 | `0x501B` | 20507 | 5027 | 6027 | `sens_data[3]` | RW | +| `0x001C` | 28 | `0x501C` | 20508 | 5028 | 6028 | `sens_data[4]`; LOAD: расчётный выход 4–20 мА, raw ×10 | RW | +| `0x001D` | 29 | `0x501D` | 20509 | 5029 | 6029 | `sens_data[5]`; LOAD: измеренный вход 4–20 мА, raw ×10 | RW | +| `0x001E` | 30 | `0x501E` | 20510 | 5030 | 6030 | `sens_data[6]` | RW | +| `0x001F` | 31 | `0x501F` | 20511 | 5031 | 6031 | `sens_data[7]` | RW | +| `0x0020` | 32 | `0x5020` | 20512 | 5032 | 6032 | `sens_data[8]` | RW | +| `0x0021` | 33 | `0x5021` | 20513 | 5033 | 6033 | `sens_data[9]` | RW | +| `0x0022` | 34 | `0x5022` | 20514 | 5034 | 6034 | `sens_data[10]` | RW | +| `0x0023` | 35 | `0x5023` | 20515 | 5035 | 6035 | `sens_data[11]` | RW | +| `0x0024` | 36 | `0x5024` | 20516 | 5036 | 6036 | `sens_data[12]` | RW | +| `0x0025` | 37 | `0x5025` | 20517 | 5037 | 6037 | `sens_data[13]` | RW | +| `0x0026` | 38 | `0x5026` | 20518 | 5038 | 6038 | `sens_data[14]` | RW | +| `0x0027` | 39 | `0x5027` | 20519 | 5039 | 6039 | `sens_data[15]` | RW | +| `0x0028` | 40 | `0x5028` | 20520 | 5040 | 6040 | `sens_data[16]` | RW | +| `0x0029` | 41 | `0x5029` | 20521 | 5041 | 6041 | `sens_data[17]` | RW | +| `0x002A` | 42 | `0x502A` | 20522 | 5042 | 6042 | `sens_data[18]` | RW | +| `0x002B` | 43 | `0x502B` | 20523 | 5043 | 6043 | `sens_data[19]` | RW | +| `0x002C` | 44 | `0x502C` | 20524 | 5044 | 6044 | `sens_data[20]` | RW | +| `0x002D` | 45 | `0x502D` | 20525 | 5045 | 6045 | `sens_data[21]` | RW | +| `0x002E` | 46 | `0x502E` | 20526 | 5046 | 6046 | `sens_data[22]` | RW | +| `0x002F` | 47 | `0x502F` | 20527 | 5047 | 6047 | `sens_data[23]` | RW | +| `0x0030` | 48 | `0x5030` | 20528 | 5048 | 6048 | `sens_hi_edge[0]` | RW | +| `0x0031` | 49 | `0x5031` | 20529 | 5049 | 6049 | `sens_hi_edge[1]` | RW | +| `0x0032` | 50 | `0x5032` | 20530 | 5050 | 6050 | `sens_hi_edge[2]` | RW | +| `0x0033` | 51 | `0x5033` | 20531 | 5051 | 6051 | `sens_hi_edge[3]` | RW | +| `0x0034` | 52 | `0x5034` | 20532 | 5052 | 6052 | `sens_hi_edge[4]` | RW | +| `0x0035` | 53 | `0x5035` | 20533 | 5053 | 6053 | `sens_hi_edge[5]` | RW | +| `0x0036` | 54 | `0x5036` | 20534 | 5054 | 6054 | `sens_hi_edge[6]` | RW | +| `0x0037` | 55 | `0x5037` | 20535 | 5055 | 6055 | `sens_hi_edge[7]` | RW | +| `0x0038` | 56 | `0x5038` | 20536 | 5056 | 6056 | `sens_hi_edge[8]` | RW | +| `0x0039` | 57 | `0x5039` | 20537 | 5057 | 6057 | `sens_hi_edge[9]` | RW | +| `0x003A` | 58 | `0x503A` | 20538 | 5058 | 6058 | `sens_hi_edge[10]` | RW | +| `0x003B` | 59 | `0x503B` | 20539 | 5059 | 6059 | `sens_hi_edge[11]` | RW | +| `0x003C` | 60 | `0x503C` | 20540 | 5060 | 6060 | `sens_hi_edge[12]` | RW | +| `0x003D` | 61 | `0x503D` | 20541 | 5061 | 6061 | `sens_hi_edge[13]` | RW | +| `0x003E` | 62 | `0x503E` | 20542 | 5062 | 6062 | `sens_hi_edge[14]` | RW | +| `0x003F` | 63 | `0x503F` | 20543 | 5063 | 6063 | `sens_hi_edge[15]` | RW | +| `0x0040` | 64 | `0x5040` | 20544 | 5064 | 6064 | `sens_hi_edge[16]` | RW | +| `0x0041` | 65 | `0x5041` | 20545 | 5065 | 6065 | `sens_hi_edge[17]` | RW | +| `0x0042` | 66 | `0x5042` | 20546 | 5066 | 6066 | `sens_hi_edge[18]` | RW | +| `0x0043` | 67 | `0x5043` | 20547 | 5067 | 6067 | `sens_hi_edge[19]` | RW | +| `0x0044` | 68 | `0x5044` | 20548 | 5068 | 6068 | `sens_hi_edge[20]` | RW | +| `0x0045` | 69 | `0x5045` | 20549 | 5069 | 6069 | `sens_hi_edge[21]` | RW | +| `0x0046` | 70 | `0x5046` | 20550 | 5070 | 6070 | `sens_hi_edge[22]` | RW | +| `0x0047` | 71 | `0x5047` | 20551 | 5071 | 6071 | `sens_hi_edge[23]` | RW | +| `0x0048` | 72 | `0x5048` | 20552 | 5072 | 6072 | `sens_lo_edge[0]` | RW | +| `0x0049` | 73 | `0x5049` | 20553 | 5073 | 6073 | `sens_lo_edge[1]` | RW | +| `0x004A` | 74 | `0x504A` | 20554 | 5074 | 6074 | `sens_lo_edge[2]` | RW | +| `0x004B` | 75 | `0x504B` | 20555 | 5075 | 6075 | `sens_lo_edge[3]` | RW | +| `0x004C` | 76 | `0x504C` | 20556 | 5076 | 6076 | `sens_lo_edge[4]` | RW | +| `0x004D` | 77 | `0x504D` | 20557 | 5077 | 6077 | `sens_lo_edge[5]` | RW | +| `0x004E` | 78 | `0x504E` | 20558 | 5078 | 6078 | `sens_lo_edge[6]` | RW | +| `0x004F` | 79 | `0x504F` | 20559 | 5079 | 6079 | `sens_lo_edge[7]` | RW | +| `0x0050` | 80 | `0x5050` | 20560 | 5080 | 6080 | `sens_lo_edge[8]`; LOAD: `DAC_go` — параметр начала рампы | RW | +| `0x0051` | 81 | `0x5051` | 20561 | 5081 | 6081 | `sens_lo_edge[9]`; LOAD: `DAC_stop` — параметр конца рампы | RW | +| `0x0052` | 82 | `0x5052` | 20562 | 5082 | 6082 | `sens_lo_edge[10]`; LOAD: `LOAD_CAN_TIMEOUT`, мс | RW; LOAD: 1…60000 | +| `0x0053` | 83 | `0x5053` | 20563 | 5083 | 6083 | `sens_lo_edge[11]` | RW | +| `0x0054` | 84 | `0x5054` | 20564 | 5084 | 6084 | `sens_lo_edge[12]` | RW | +| `0x0055` | 85 | `0x5055` | 20565 | 5085 | 6085 | `sens_lo_edge[13]` | RW | +| `0x0056` | 86 | `0x5056` | 20566 | 5086 | 6086 | `sens_lo_edge[14]` | RW | +| `0x0057` | 87 | `0x5057` | 20567 | 5087 | 6087 | `sens_lo_edge[15]` | RW | +| `0x0058` | 88 | `0x5058` | 20568 | 5088 | 6088 | `sens_lo_edge[16]` | RW | +| `0x0059` | 89 | `0x5059` | 20569 | 5089 | 6089 | `sens_lo_edge[17]` | RW | +| `0x005A` | 90 | `0x505A` | 20570 | 5090 | 6090 | `sens_lo_edge[18]` | RW | +| `0x005B` | 91 | `0x505B` | 20571 | 5091 | 6091 | `sens_lo_edge[19]` | RW | +| `0x005C` | 92 | `0x505C` | 20572 | 5092 | 6092 | `sens_lo_edge[20]` | RW | +| `0x005D` | 93 | `0x505D` | 20573 | 5093 | 6093 | `sens_lo_edge[21]` | RW | +| `0x005E` | 94 | `0x505E` | 20574 | 5094 | 6094 | `sens_lo_edge[22]` | RW | +| `0x005F` | 95 | `0x505F` | 20575 | 5095 | 6095 | `sens_lo_edge[23]` | RW | +| `0x0060` | 96 | `0x5060` | 20576 | 5096 | 6096 | `Cancount[m_FAST]` — пауза быстрых посылок CAN | RW | +| `0x0061` | 97 | `0x5061` | 20577 | 5097 | 6097 | `Cancount[m_SLOW]` — пауза медленных посылок CAN | RW | +| `0x0062` | 98 | `0x5062` | 20578 | 5098 | 6098 | `Brightness` / `Bright[0]` — яркость | RW | +| `0x0063` | 99 | `0x5063` | 20579 | 5099 | 6099 | `Owncount` / `Bright[1]`; LOAD: исходное значение АЦП | RW | +| `0x0064` | 100 | `0x5064` | 20580 | 5100 | 6100 | `Bright[2]`; `Current_count`: исходное значение канала 0 | RW | +| `0x0065` | 101 | `0x5065` | 20581 | 5101 | 6101 | `Bright[3]`; `Current_count`: исходное значение канала 1 | RW | +| `0x0066` | 102 | `0x5066` | 20582 | 5102 | 6102 | `Bright[4]`; `Current_count`: исходное значение канала 2 | RW | +| `0x0067` | 103 | `0x5067` | 20583 | 5103 | 6103 | `Bright[5]`; `Current_count`: исходное значение канала 3 | RW | +| `0x0068` | 104 | `0x5068` | 20584 | 5104 | 6104 | `lev_count[]`, запись из `Current_count`; назначение по режиму | RW | +| `0x0069` | 105 | `0x5069` | 20585 | 5105 | 6105 | `lev_count[]`, запись из `Current_count`; назначение по режиму | RW | +| `0x006A` | 106 | `0x506A` | 20586 | 5106 | 6106 | `lev_count[]`, запись из `Current_count`; назначение по режиму | RW | +| `0x006B` | 107 | `0x506B` | 20587 | 5107 | 6107 | `lev_count[]`, запись из `Current_count`; назначение по режиму | RW | +| `0x006C` | 108 | `0x506C` | 20588 | 5108 | 6108 | `lev_count[]`, запись из `Current_count`; назначение по режиму | RW | +| `0x006D` | 109 | `0x506D` | 20589 | 5109 | 6109 | `lev_count[]`, запись из `Current_count`; назначение по режиму | RW | +| `0x006E` | 110 | `0x506E` | 20590 | 5110 | 6110 | `DAC_20` — калибровка входа 20 мА | RW | +| `0x006F` | 111 | `0x506F` | 20591 | 5111 | 6111 | `DAC_04` — калибровка входа 4 мА | RW | +| `0x0070` | 112 | `0x5070` | 20592 | 5112 | 6112 | `Zero_lev[0]` — нулевой уровень канала тока 0 | RW | +| `0x0071` | 113 | `0x5071` | 20593 | 5113 | 6113 | `Zero_lev[1]` — нулевой уровень канала тока 1 | RW | +| `0x0072` | 114 | `0x5072` | 20594 | 5114 | 6114 | `Zero_lev[2]` — нулевой уровень канала тока 2 | RW | +| `0x0073` | 115 | `0x5073` | 20595 | 5115 | 6115 | `Zero_lev[3]` — нулевой уровень канала тока 3 | RW | +| `0x0074` | 116 | `0x5074` | 20596 | 5116 | 6116 | `Caliber[0]`: `K380_1` / `KPOW_1` | RW | +| `0x0075` | 117 | `0x5075` | 20597 | 5117 | 6117 | `Caliber[1]`: `K380_2` / `KPOW_2` | RW | +| `0x0076` | 118 | `0x5076` | 20598 | 5118 | 6118 | `Caliber[2]`: `KPOW_3` | RW | +| `0x0077` | 119 | `0x5077` | 20599 | 5119 | 6119 | `Caliber[3]`: `KPOW_4` | RW | +| `0x0078` | 120 | `0x5078` | 20600 | 5120 | 6120 | `Caliber[4]` / `TCaliber[0]`; LOAD: `DAC_max()` | RW | +| `0x0079` | 121 | `0x5079` | 20601 | 5121 | 6121 | `Caliber[5]` / `TCaliber[1]`; LOAD: `DAC_min()` | RW | +| `0x007A` | 122 | `0x507A` | 20602 | 5122 | 6122 | `Caliber[6]` / `TCaliber[2]`; LOAD: `DAC_cal()` | RW | +| `0x007B` | 123 | `0x507B` | 20603 | 5123 | 6123 | `Caliber[7]` / `TCaliber[3]`; LOAD: текущий 12-битный код ЦАП | RW | +| `0x007C` | 124 | `0x507C` | 20604 | 5124 | 6124 | `NormaU` = `Caliber[8]`; LOAD: `LOAD_STATUS` | RO для LOAD; иначе RW | +| `0x007D` | 125 | `0x507D` | 20605 | 5125 | 6125 | `DeltaU` = `Caliber[9]`; LOAD: `LOAD_ATTEMPTS` | RO для LOAD; иначе RW | +| `0x007E` | 126 | `0x507E` | 20606 | 5126 | 6126 | `LastMode` | RW | +| `0x007F` | 127 | `0x507F` | 20607 | 5127 | 6127 | `Commands` — битовая маска команд | RW | + +Слово `modbus[128]` в массиве используется для CRC сохраняемых параметров и **не входит** в эту карту. `0x5080` не является допустимым GAS-адресом. + +## Информация GAS-профиля + +Эти слова читаются через FC03 непосредственно по адресам `0x5F00…0x5F07`. Для них нет соответствующих штатных регистров `0x0000…0x007F`. + +| GAS = адрес Modbus, hex | Адрес, dec | Значение | Назначение | +|---|---:|---|---| +| `0x5F00` | 24320 | `0x4741` | Сигнатура `GA` | +| `0x5F01` | 24321 | `0x5331` | Сигнатура `S1` | +| `0x5F02` | 24322 | `1` | Версия контракта GAS | +| `0x5F03` | 24323 | `167` | Профиль устройства | +| `0x5F04` | 24324 | `0x5000` | База технологических GAS-регистров | +| `0x5F05` | 24325 | `128` | Число технологических регистров | +| `0x5F06` | 24326 | `0x0007` | Возможности: бит 0 — RTU, бит 1 — служебный CAN, бит 2 — FC06 | +| `0x5F07` | 24327 | `1` | Аппаратный вариант EEPROM M95M02 | + +Все восемь слов — **RO**. В сборке AT25256B GAS-адреса, включая этот блок, отклоняются с кодом недопустимого адреса. + +## Маски команд и состояния LOAD + +Регистр `Commands`: штатный Modbus **`0x007F`**, GAS **`0x507F`**. Операция FC06 записывает всё слово; обработчик вызывает `ump_log_rs()`. Смысл и выполнение команд определяет прошивка. + +| Бит | Маска | Имя в исходниках | +|---:|---|---| +| 0 | `0x0001` | `cTestLamp` | +| 1 | `0x0002` | `cDefParam` | +| 2 | `0x0004` | `cSaveParam` | +| 3 | `0x0008` | `cLoadParam` | +| 4 | `0x0010` | `cTermoCal` | +| 5 | `0x0020` | `cKoefCalc` | +| 6 | `0x0040` | `cUMPreset` | +| 7 | `0x0080` | `cUMPstart` | +| 8 | `0x0100` | `cInitDac` | +| 9 | `0x0200` | `cCalibrDac` | +| 10 | `0x0400` | `cSecretBt` | +| 11 | `0x0800` | `cLiteFire` | +| 12 | `0x1000` | `cRawMeat` | +| 13 | `0x2000` | Имя команды в package.h не задано | +| 14 | `0x4000` | Имя команды в package.h не задано | +| 15 | `0x8000` | `cReset` | + +Для `Desk == dsk_LOAD` регистр `LOAD_STATUS` (`0x007C` / `0x507C`) формируется в `DAC.c` (проект tms_periph/periph_167): + +| Бит | Маска | Смысл | +|---:|---|---| +| 0 | `0x0001` | Состояние `LOAD_FAULT`: ошибка после пяти попыток пуска | +| 1 | `0x0002` | Нет свежего напряжения ЗПТ | +| 2 | `0x0004` | Состояние `LOAD_RELOAD`: сброс перед повторным пуском | +| 3 | `0x0008` | Состояние `LOAD_RUN`: работа рампы | +| 4 | `0x0010` | Имитация потери ЗПТ: приём напряжения из CAN игнорируется | +| 5 | `0x0020` | Имитация 0 В для защиты; реальные значения CAN в логгере сохраняются | + +`LOAD_ATTEMPTS` (`0x007D` / `0x507D`) содержит число попыток пуска. Оба слова доступны только для чтения на LOAD. + +## Запросы и ограничения + +| Операция | Штатные адреса `0x0000…0x007F` | GAS-адреса | +|---|---|---| +| FC03 | 1…125 слов в пределах 128 регистров | 1…124 слова; весь блок внутри одного GAS-региона | +| FC06 | Одно 16-битное слово | Одно 16-битное слово; ответ сохраняет GAS-адрес | +| FC04 / FC16 | Не используются этими обработчиками для данной карты | GAS-provider поддерживает функции 3 и 6 | +| LOAD: запись `LOAD_STATUS`, `LOAD_ATTEMPTS` | Код 2 | Код 2 | +| LOAD: запись `LOAD_CAN_TIMEOUT` вне 1…60000 мс | Код 3 | Код 3 | + +GAS-provider возвращает код 1 для неподдерживаемой функции, код 2 для недопустимого адреса, пересечения границы или записи RO-блока; код 3 — для количества слов 0 или >124 и недопустимого LOAD-timeout. В RTU запросы к этой карте маршрутизируются обработчиками FC03/FC06. + +В RTU данные одного слова передаются старшим байтом вперёд. Формат ответа GAS-расширения отличается от исторического ответа низких адресов: у GAS нет дополнительных хвостовых нулей. + +Примеры ниже задают функцию, адрес и количество/значение; это параметры запроса, без адреса устройства и CRC: + +| Действие | Штатный Modbus | Через GAS | +|---|---|---| +| Прочитать информацию профиля | Нет аналога | `FC03(0x5F00, 8)` | +| Прочитать слово 0 | `FC03(0x0000, 1)` | `FC03(0x5000, 1)` | +| Прочитать все 128 слов | `FC03(0x0000, 125)` + `FC03(0x007D, 3)` | `FC03(0x5000, 124)` + `FC03(0x507C, 4)` | +| Прочитать timeout LOAD | `FC03(0x0052, 1)` | `FC03(0x5052, 1)` | +| Задать timeout LOAD = 1000 мс | `FC06(0x0052, 1000)` | `FC06(0x5052, 1000)` | +| Прочитать статус и попытки LOAD | `FC03(0x007C, 2)` | `FC03(0x507C, 2)` | + +Два запроса на чтение всей карты дают два отдельных снимка. FC06 меняет RAM; сохранение параметров в EEPROM выполняется штатным механизмом прошивки. + +Эти же GAS-адреса доступны через **служебный CAN УМП** с маркером `0x1F00`, token и функцией 3/6. CAN-чтение принимает чётное количество **2…124 слова**; каждый фрагмент ответа содержит два слова. Формат запроса приведён в `GAS_167_M95M02.md` проекта tms_periph/periph_167. + +Другие расширения Modbus — регистратор `0x1000…0x117B`, паспорт `0x1200…0x120B`, защита `0x1300…0x1307` и EEPROM `0x8000…0xFFFF` — имеют собственные обработчики. Формула `GAS = 0x5000 + Modbus` применяется только к штатным **128 технологическим словам**. + +## Источники + +- `gas167.h` (проект tms_periph/periph_167) — базы, размеры, версия и лимит GAS. +- `gas167.c` (проект tms_periph/periph_167) — прямое соответствие `modbus[]`, информация профиля и ограничения записи. +- `package.h` (проект tms_periph/periph_167) — группы регистров, имена и маски команд. +- `message.c` (проект tms_periph/periph_167) — FC03/FC06, инициализация и сохранение параметров. +- `measure.c` (проект tms_periph/periph_167), `ADC.c` (проект tms_periph/periph_167), `DAC.c` (проект tms_periph/periph_167) — значения измерений и особенности LOAD. +- `GAS_167_M95M02.md` проекта tms_periph/periph_167 — включение GAS, форматы транспорта и проверки реализации. diff --git a/doc/setcan/protocan/PROTOCOL.md b/doc/setcan/protocan/PROTOCOL.md index aea4627..a770cf6 100644 --- a/doc/setcan/protocan/PROTOCOL.md +++ b/doc/setcan/protocan/PROTOCOL.md @@ -112,3 +112,78 @@ CAN ID = 0x13590702 Этот пример соответствует `ENTER_BOOT`, `SessionID=7`. Машинные варианты находятся в [examples/test-vectors.json](examples/test-vectors.json). + +## GAS: сквозная карта УКСС и профиль 167/v1 + +УКСС назначены десятичные ID **0…11999**, блоки по 1000 слов: +`GAS_ID = номер_узла × 1000 + смещение`. Блок 0 — общий резерв; +узлы 1…11 и все границы описаны в +[GAS_ADDRESS_ALLOCATION_167.md](GAS_ADDRESS_ALLOCATION_167.md). +Диапазон 12000…65535 свободен для последующего распределения. + +УМП 1 (узел 5) назначены ID 5000…5127, УМП 2 (узел 6) — +6000…6127. Смещения 128…999 зарезервированы. Полный словарь 128 слов, +ограничения LOAD, биты Commands/LOAD_STATUS и соответствие Modbus приведены в +[GAS_MODBUS_MAP_167.md](GAS_MODBUS_MAP_167.md). + +Новые ID пока назначены только в документации. Действующий профиль 167/v1 +использует Modbus 0x0000…0x007F и окно GAS 0x5000…0x507F; +например, LOAD_CAN_TIMEOUT: Modbus 0x0052, GAS 0x5052, +новые ID 5082/6082. Десятичный ID 5000 не равен hex-адресу 0x5000. +Служебный CAN УМП с маркером 0x1F00 — отдельный транспорт; +его пакет нельзя интерпретировать как обычный payload ProtoCAN GAS. + +Словарь и lookup находятся в `python/protocan/gas_167.py` и +`gas_167_registers.json` проекта templates. Парсер обычных кадров GAS +показывает назначенные имена, узел, старые адреса и статус для запросов +и каждого слова ответа; неизвестные и резервные адреса сохраняются raw. +Общая сводка `Decoded.summary` содержит имя каждого назначенного параметра, +УМП 1/2, десятичный ID, hex-адрес и значение в dec/hex. Поэтому имена видны +непосредственно в колонке «Расшифровка» всех клиентов общего декодера: +онлайн-монитор, трассировка, фильтр кадров, ручной разбор и DevBoard_V1. +Полный текст длинной строки доступен во всплывающей подсказке. +Тип сообщения отображается как `GAS (General Address Space)`. +Обычные демо-потоки шины и DevBoard также включают назначенные адреса УМП. +Низкие Modbus-адреса и старое окно не определяют профиль прибора автоматически. +DATA обычного ProtoCAN GAS — до четырёх u16 little-endian; Modbus RTU +передаёт каждое слово старшим байтом вперёд. GUI использует этот же словарь +через совместимый импорт, без собственной реализации карты. + +### Демо УМП в SETGUI + +Общий список «Протокол» позволяет выбрать `ProtoCAN / GAS`, +`УМП / Balsam 167 (legacy CAN)`, карту `GAS 167/v1` отдельно для УМП 1/2 +или `УМП / ProtoCAN GAS — новые ID 5000/6000`. +Выбор перерисовывает уже полученные кадры. Для старой карты номер УМП +выбирается оператором и обозначается «выбран вручную»; низкие адреса +0…127 и окно 0x5000…0x507F сами по себе номер узла не кодируют. +Этот режим накладывает словарь на слова ProtoCAN GAS/Modbus Holding; +он не является декодером служебных CAN-пакетов с маркером 0x1F00. +Профиль Balsam принимает только собственный формат и показывает +несоответствие для чужих кадров. В сводке Commands показаны активные +биты с названиями команд; это не подтверждение их выполнения. + +В режиме «Авто» legacy-ID Balsam проверяются первыми. Для корректного +ProtoCAN GAS с целыми словами все адреса кадра сверяются с назначенными +диапазонами УМП. Если все они принадлежат одному узлу, поля Device Name +и Msg Type показывают УМП 1/2; основание выбора записывается в подробности +«Автоопределение». Это распознавание назначенной карты, а не подтверждение +её поддержки прошивкой. Для старых/неизвестных адресов, смешанных диапазонов +и некорректного payload автоматический выбор УМП не выполняется; +сводка сообщает «профиль/узел не определены». Номер Device в ProtoCAN сам +по себе не считается номером УМП. «Демо УМП» запускается с режимом «Авто». + +Открыть `CAN → Шина CAN / мост → Настройки → Демо УМП`. +Генератор `python/set_devices/bus_demo.py:UmpDemoSource` моделирует два +узла с демонстрационными адресами `Device Type=3`, `Device=5/6`. +Он передаёт PULSE раз в секунду и четыре GAS-слова каждого узла каждые +250 мс: ID 5000…5127 и 6000…6127, полный обход за 8 секунд. +Это имитация назначенной карты, а не эмуляция текущего транспорта 167/v1. + +Кадры проходят обычное кадрирование AA 55, CRC и общий парсер ProtoCAN. +Их можно смотреть в онлайн-мониторе, трассировке и адресном пространстве; +в буфере сессии и его CSV источник обозначен `DEMO УМП`. +Значения синтетические, в формате raw u16. LOAD_STATUS показывает RUN +(бит 3), LOAD_ATTEMPTS — 0, LOAD_CAN_TIMEOUT — 1000 мс. +Кнопка «Стоп демо УМП» останавливает поток. Для запуска нужно отключить +COM-порт и CAN-снифер; генератор работает локально без передачи в оборудование. diff --git a/python/protocan/balsam.py b/python/protocan/balsam.py index 8b7fef8..0ce27ab 100644 --- a/python/protocan/balsam.py +++ b/python/protocan/balsam.py @@ -62,7 +62,7 @@ class BalsamFrame: "%s=0x%04X (%d)" % (item.name or "R%04X" % item.address, item.value, item.signed_value) + (" — " + self.command_description(item.value) - if not self.from_device and item.address == 0x7F else "") + if item.address == 0x7F else "") for item in self.registers ) or "нет отмеченных регистров" return "BALZAM · %s · %s · %s" % (self.route, direction, values) diff --git a/python/protocan/gas_167.py b/python/protocan/gas_167.py new file mode 100644 index 0000000..1f44341 --- /dev/null +++ b/python/protocan/gas_167.py @@ -0,0 +1,51 @@ +"""GAS 167/v1 dictionary and assigned UKSS IDs; values remain raw u16.""" +import json +from pathlib import Path + +REGISTERS = tuple(json.loads(Path(__file__).with_name('gas_167_registers.json').read_text(encoding='utf-8'))['registers']) +BY_ADDRESS = {entry['address']: entry for entry in REGISTERS} +BY_ASSIGNED_ID = {entry[key]: (node, entry) for node, key in ((5, 'ump1_id'), (6, 'ump2_id')) for entry in REGISTERS} + +COMMAND_NAMES = ( + 'cTestLamp — проверка ламп', 'cDefParam — параметры по умолчанию', + 'cSaveParam — сохранение параметров', 'cLoadParam — загрузка параметров', + 'cTermoCal — термокалибровка', 'cKoefCalc — расчёт коэффициентов', + 'cUMPreset — останов заряда', 'cUMPstart — пуск заряда', + 'cInitDac — инициализация ЦАП', 'cCalibrDac — калибровка ЦАП', + 'cSecretBt', 'cLiteFire', 'cRawMeat', 'бит 13 (не назначен)', + 'бит 14 (не назначен)', 'cReset — сброс', +) + + +def describe_word(offset, value): + """Describe active bits using the selected LOAD register map.""" + if offset == 127: + return ', '.join(name for bit, name in enumerate(COMMAND_NAMES) + if value & (1 << bit)) or 'нет активных команд' + if offset == 124: + names = ('LOAD_FAULT — ошибка пуска', 'нет свежего напряжения ЗПТ', + 'LOAD_RELOAD — сброс перед пуском', 'LOAD_RUN — работа рампы', + 'имитация потери ЗПТ', 'имитация 0 В') + return ', '.join(names[bit] if bit < len(names) else 'бит %d' % bit + for bit in range(16) if value & (1 << bit)) or 'нет активных флагов LOAD' + return '' + + +def metadata(address): + """Metadata for the explicitly selected legacy Modbus profile.""" + index = address if 0 <= address < 128 else address - 0x5000 + if not 0 <= index < 128: + return {} + entry = REGISTERS[index] + return {'name': entry['description'], 'gas_id': f"{entry['ump1_id']} / {entry['ump2_id']}"} + + +def assigned_metadata(address): + """Only assigned global IDs; reserved offsets have no parameter name.""" + match = BY_ASSIGNED_ID.get(address) + if match is None: + return {} + node, entry = match + return {'node': node, 'name': entry['description'], 'access': entry['access'], + 'modbus': entry['modbus'], 'legacy_address': entry['address'], + 'status': 'назначен; прошивка 167/v1 ещё не обслуживает'} diff --git a/python/protocan/gas_167_registers.json b/python/protocan/gas_167_registers.json new file mode 100644 index 0000000..d0b03be --- /dev/null +++ b/python/protocan/gas_167_registers.json @@ -0,0 +1,1030 @@ +{ + "profile": "167/v1", + "source_commit": "378e06a", + "registers": [ + { + "modbus": 0, + "address": 20480, + "ump1_id": 5000, + "ump2_id": 6000, + "description": "sens_error[0]; InputRep0/1/2 — биты A/B/C", + "access": "RW" + }, + { + "modbus": 1, + "address": 20481, + "ump1_id": 5001, + "ump2_id": 6001, + "description": "sens_error[1]", + "access": "RW" + }, + { + "modbus": 2, + "address": 20482, + "ump1_id": 5002, + "ump2_id": 6002, + "description": "sens_error[2]", + "access": "RW" + }, + { + "modbus": 3, + "address": 20483, + "ump1_id": 5003, + "ump2_id": 6003, + "description": "sens_error[3]", + "access": "RW" + }, + { + "modbus": 4, + "address": 20484, + "ump1_id": 5004, + "ump2_id": 6004, + "description": "sens_error[4]", + "access": "RW" + }, + { + "modbus": 5, + "address": 20485, + "ump1_id": 5005, + "ump2_id": 6005, + "description": "sens_error[5]", + "access": "RW" + }, + { + "modbus": 6, + "address": 20486, + "ump1_id": 5006, + "ump2_id": 6006, + "description": "sens_error[6]", + "access": "RW" + }, + { + "modbus": 7, + "address": 20487, + "ump1_id": 5007, + "ump2_id": 6007, + "description": "sens_error[7]", + "access": "RW" + }, + { + "modbus": 8, + "address": 20488, + "ump1_id": 5008, + "ump2_id": 6008, + "description": "sens_error[8]", + "access": "RW" + }, + { + "modbus": 9, + "address": 20489, + "ump1_id": 5009, + "ump2_id": 6009, + "description": "sens_error[9]", + "access": "RW" + }, + { + "modbus": 10, + "address": 20490, + "ump1_id": 5010, + "ump2_id": 6010, + "description": "sens_error[10]", + "access": "RW" + }, + { + "modbus": 11, + "address": 20491, + "ump1_id": 5011, + "ump2_id": 6011, + "description": "sens_error[11]", + "access": "RW" + }, + { + "modbus": 12, + "address": 20492, + "ump1_id": 5012, + "ump2_id": 6012, + "description": "sens_error[12]", + "access": "RW" + }, + { + "modbus": 13, + "address": 20493, + "ump1_id": 5013, + "ump2_id": 6013, + "description": "sens_error[13]", + "access": "RW" + }, + { + "modbus": 14, + "address": 20494, + "ump1_id": 5014, + "ump2_id": 6014, + "description": "sens_error[14]", + "access": "RW" + }, + { + "modbus": 15, + "address": 20495, + "ump1_id": 5015, + "ump2_id": 6015, + "description": "sens_error[15]", + "access": "RW" + }, + { + "modbus": 16, + "address": 20496, + "ump1_id": 5016, + "ump2_id": 6016, + "description": "sens_error[16]; Inputs", + "access": "RW" + }, + { + "modbus": 17, + "address": 20497, + "ump1_id": 5017, + "ump2_id": 6017, + "description": "sens_error[17]", + "access": "RW" + }, + { + "modbus": 18, + "address": 20498, + "ump1_id": 5018, + "ump2_id": 6018, + "description": "sens_error[18]", + "access": "RW" + }, + { + "modbus": 19, + "address": 20499, + "ump1_id": 5019, + "ump2_id": 6019, + "description": "sens_error[19]", + "access": "RW" + }, + { + "modbus": 20, + "address": 20500, + "ump1_id": 5020, + "ump2_id": 6020, + "description": "sens_error[20]", + "access": "RW" + }, + { + "modbus": 21, + "address": 20501, + "ump1_id": 5021, + "ump2_id": 6021, + "description": "sens_error[21]", + "access": "RW" + }, + { + "modbus": 22, + "address": 20502, + "ump1_id": 5022, + "ump2_id": 6022, + "description": "sens_error[22]; Buttons", + "access": "RW" + }, + { + "modbus": 23, + "address": 20503, + "ump1_id": 5023, + "ump2_id": 6023, + "description": "sens_error[23]; Jumpers", + "access": "RW" + }, + { + "modbus": 24, + "address": 20504, + "ump1_id": 5024, + "ump2_id": 6024, + "description": "sens_data[0]", + "access": "RW" + }, + { + "modbus": 25, + "address": 20505, + "ump1_id": 5025, + "ump2_id": 6025, + "description": "sens_data[1]", + "access": "RW" + }, + { + "modbus": 26, + "address": 20506, + "ump1_id": 5026, + "ump2_id": 6026, + "description": "sens_data[2]", + "access": "RW" + }, + { + "modbus": 27, + "address": 20507, + "ump1_id": 5027, + "ump2_id": 6027, + "description": "sens_data[3]", + "access": "RW" + }, + { + "modbus": 28, + "address": 20508, + "ump1_id": 5028, + "ump2_id": 6028, + "description": "sens_data[4]; LOAD: расчётный выход 4–20 мА, raw ×10", + "access": "RW" + }, + { + "modbus": 29, + "address": 20509, + "ump1_id": 5029, + "ump2_id": 6029, + "description": "sens_data[5]; LOAD: измеренный вход 4–20 мА, raw ×10", + "access": "RW" + }, + { + "modbus": 30, + "address": 20510, + "ump1_id": 5030, + "ump2_id": 6030, + "description": "sens_data[6]", + "access": "RW" + }, + { + "modbus": 31, + "address": 20511, + "ump1_id": 5031, + "ump2_id": 6031, + "description": "sens_data[7]", + "access": "RW" + }, + { + "modbus": 32, + "address": 20512, + "ump1_id": 5032, + "ump2_id": 6032, + "description": "sens_data[8]", + "access": "RW" + }, + { + "modbus": 33, + "address": 20513, + "ump1_id": 5033, + "ump2_id": 6033, + "description": "sens_data[9]", + "access": "RW" + }, + { + "modbus": 34, + "address": 20514, + "ump1_id": 5034, + "ump2_id": 6034, + "description": "sens_data[10]", + "access": "RW" + }, + { + "modbus": 35, + "address": 20515, + "ump1_id": 5035, + "ump2_id": 6035, + "description": "sens_data[11]", + "access": "RW" + }, + { + "modbus": 36, + "address": 20516, + "ump1_id": 5036, + "ump2_id": 6036, + "description": "sens_data[12]", + "access": "RW" + }, + { + "modbus": 37, + "address": 20517, + "ump1_id": 5037, + "ump2_id": 6037, + "description": "sens_data[13]", + "access": "RW" + }, + { + "modbus": 38, + "address": 20518, + "ump1_id": 5038, + "ump2_id": 6038, + "description": "sens_data[14]", + "access": "RW" + }, + { + "modbus": 39, + "address": 20519, + "ump1_id": 5039, + "ump2_id": 6039, + "description": "sens_data[15]", + "access": "RW" + }, + { + "modbus": 40, + "address": 20520, + "ump1_id": 5040, + "ump2_id": 6040, + "description": "sens_data[16]", + "access": "RW" + }, + { + "modbus": 41, + "address": 20521, + "ump1_id": 5041, + "ump2_id": 6041, + "description": "sens_data[17]", + "access": "RW" + }, + { + "modbus": 42, + "address": 20522, + "ump1_id": 5042, + "ump2_id": 6042, + "description": "sens_data[18]", + "access": "RW" + }, + { + "modbus": 43, + "address": 20523, + "ump1_id": 5043, + "ump2_id": 6043, + "description": "sens_data[19]", + "access": "RW" + }, + { + "modbus": 44, + "address": 20524, + "ump1_id": 5044, + "ump2_id": 6044, + "description": "sens_data[20]", + "access": "RW" + }, + { + "modbus": 45, + "address": 20525, + "ump1_id": 5045, + "ump2_id": 6045, + "description": "sens_data[21]", + "access": "RW" + }, + { + "modbus": 46, + "address": 20526, + "ump1_id": 5046, + "ump2_id": 6046, + "description": "sens_data[22]", + "access": "RW" + }, + { + "modbus": 47, + "address": 20527, + "ump1_id": 5047, + "ump2_id": 6047, + "description": "sens_data[23]", + "access": "RW" + }, + { + "modbus": 48, + "address": 20528, + "ump1_id": 5048, + "ump2_id": 6048, + "description": "sens_hi_edge[0]", + "access": "RW" + }, + { + "modbus": 49, + "address": 20529, + "ump1_id": 5049, + "ump2_id": 6049, + "description": "sens_hi_edge[1]", + "access": "RW" + }, + { + "modbus": 50, + "address": 20530, + "ump1_id": 5050, + "ump2_id": 6050, + "description": "sens_hi_edge[2]", + "access": "RW" + }, + { + "modbus": 51, + "address": 20531, + "ump1_id": 5051, + "ump2_id": 6051, + "description": "sens_hi_edge[3]", + "access": "RW" + }, + { + "modbus": 52, + "address": 20532, + "ump1_id": 5052, + "ump2_id": 6052, + "description": "sens_hi_edge[4]", + "access": "RW" + }, + { + "modbus": 53, + "address": 20533, + "ump1_id": 5053, + "ump2_id": 6053, + "description": "sens_hi_edge[5]", + "access": "RW" + }, + { + "modbus": 54, + "address": 20534, + "ump1_id": 5054, + "ump2_id": 6054, + "description": "sens_hi_edge[6]", + "access": "RW" + }, + { + "modbus": 55, + "address": 20535, + "ump1_id": 5055, + "ump2_id": 6055, + "description": "sens_hi_edge[7]", + "access": "RW" + }, + { + "modbus": 56, + "address": 20536, + "ump1_id": 5056, + "ump2_id": 6056, + "description": "sens_hi_edge[8]", + "access": "RW" + }, + { + "modbus": 57, + "address": 20537, + "ump1_id": 5057, + "ump2_id": 6057, + "description": "sens_hi_edge[9]", + "access": "RW" + }, + { + "modbus": 58, + "address": 20538, + "ump1_id": 5058, + "ump2_id": 6058, + "description": "sens_hi_edge[10]", + "access": "RW" + }, + { + "modbus": 59, + "address": 20539, + "ump1_id": 5059, + "ump2_id": 6059, + "description": "sens_hi_edge[11]", + "access": "RW" + }, + { + "modbus": 60, + "address": 20540, + "ump1_id": 5060, + "ump2_id": 6060, + "description": "sens_hi_edge[12]", + "access": "RW" + }, + { + "modbus": 61, + "address": 20541, + "ump1_id": 5061, + "ump2_id": 6061, + "description": "sens_hi_edge[13]", + "access": "RW" + }, + { + "modbus": 62, + "address": 20542, + "ump1_id": 5062, + "ump2_id": 6062, + "description": "sens_hi_edge[14]", + "access": "RW" + }, + { + "modbus": 63, + "address": 20543, + "ump1_id": 5063, + "ump2_id": 6063, + "description": "sens_hi_edge[15]", + "access": "RW" + }, + { + "modbus": 64, + "address": 20544, + "ump1_id": 5064, + "ump2_id": 6064, + "description": "sens_hi_edge[16]", + "access": "RW" + }, + { + "modbus": 65, + "address": 20545, + "ump1_id": 5065, + "ump2_id": 6065, + "description": "sens_hi_edge[17]", + "access": "RW" + }, + { + "modbus": 66, + "address": 20546, + "ump1_id": 5066, + "ump2_id": 6066, + "description": "sens_hi_edge[18]", + "access": "RW" + }, + { + "modbus": 67, + "address": 20547, + "ump1_id": 5067, + "ump2_id": 6067, + "description": "sens_hi_edge[19]", + "access": "RW" + }, + { + "modbus": 68, + "address": 20548, + "ump1_id": 5068, + "ump2_id": 6068, + "description": "sens_hi_edge[20]", + "access": "RW" + }, + { + "modbus": 69, + "address": 20549, + "ump1_id": 5069, + "ump2_id": 6069, + "description": "sens_hi_edge[21]", + "access": "RW" + }, + { + "modbus": 70, + "address": 20550, + "ump1_id": 5070, + "ump2_id": 6070, + "description": "sens_hi_edge[22]", + "access": "RW" + }, + { + "modbus": 71, + "address": 20551, + "ump1_id": 5071, + "ump2_id": 6071, + "description": "sens_hi_edge[23]", + "access": "RW" + }, + { + "modbus": 72, + "address": 20552, + "ump1_id": 5072, + "ump2_id": 6072, + "description": "sens_lo_edge[0]", + "access": "RW" + }, + { + "modbus": 73, + "address": 20553, + "ump1_id": 5073, + "ump2_id": 6073, + "description": "sens_lo_edge[1]", + "access": "RW" + }, + { + "modbus": 74, + "address": 20554, + "ump1_id": 5074, + "ump2_id": 6074, + "description": "sens_lo_edge[2]", + "access": "RW" + }, + { + "modbus": 75, + "address": 20555, + "ump1_id": 5075, + "ump2_id": 6075, + "description": "sens_lo_edge[3]", + "access": "RW" + }, + { + "modbus": 76, + "address": 20556, + "ump1_id": 5076, + "ump2_id": 6076, + "description": "sens_lo_edge[4]", + "access": "RW" + }, + { + "modbus": 77, + "address": 20557, + "ump1_id": 5077, + "ump2_id": 6077, + "description": "sens_lo_edge[5]", + "access": "RW" + }, + { + "modbus": 78, + "address": 20558, + "ump1_id": 5078, + "ump2_id": 6078, + "description": "sens_lo_edge[6]", + "access": "RW" + }, + { + "modbus": 79, + "address": 20559, + "ump1_id": 5079, + "ump2_id": 6079, + "description": "sens_lo_edge[7]", + "access": "RW" + }, + { + "modbus": 80, + "address": 20560, + "ump1_id": 5080, + "ump2_id": 6080, + "description": "sens_lo_edge[8]; LOAD: DAC_go — параметр начала рампы", + "access": "RW" + }, + { + "modbus": 81, + "address": 20561, + "ump1_id": 5081, + "ump2_id": 6081, + "description": "sens_lo_edge[9]; LOAD: DAC_stop — параметр конца рампы", + "access": "RW" + }, + { + "modbus": 82, + "address": 20562, + "ump1_id": 5082, + "ump2_id": 6082, + "description": "sens_lo_edge[10]; LOAD: LOAD_CAN_TIMEOUT, мс", + "access": "RW; LOAD: 1…60000" + }, + { + "modbus": 83, + "address": 20563, + "ump1_id": 5083, + "ump2_id": 6083, + "description": "sens_lo_edge[11]", + "access": "RW" + }, + { + "modbus": 84, + "address": 20564, + "ump1_id": 5084, + "ump2_id": 6084, + "description": "sens_lo_edge[12]", + "access": "RW" + }, + { + "modbus": 85, + "address": 20565, + "ump1_id": 5085, + "ump2_id": 6085, + "description": "sens_lo_edge[13]", + "access": "RW" + }, + { + "modbus": 86, + "address": 20566, + "ump1_id": 5086, + "ump2_id": 6086, + "description": "sens_lo_edge[14]", + "access": "RW" + }, + { + "modbus": 87, + "address": 20567, + "ump1_id": 5087, + "ump2_id": 6087, + "description": "sens_lo_edge[15]", + "access": "RW" + }, + { + "modbus": 88, + "address": 20568, + "ump1_id": 5088, + "ump2_id": 6088, + "description": "sens_lo_edge[16]", + "access": "RW" + }, + { + "modbus": 89, + "address": 20569, + "ump1_id": 5089, + "ump2_id": 6089, + "description": "sens_lo_edge[17]", + "access": "RW" + }, + { + "modbus": 90, + "address": 20570, + "ump1_id": 5090, + "ump2_id": 6090, + "description": "sens_lo_edge[18]", + "access": "RW" + }, + { + "modbus": 91, + "address": 20571, + "ump1_id": 5091, + "ump2_id": 6091, + "description": "sens_lo_edge[19]", + "access": "RW" + }, + { + "modbus": 92, + "address": 20572, + "ump1_id": 5092, + "ump2_id": 6092, + "description": "sens_lo_edge[20]", + "access": "RW" + }, + { + "modbus": 93, + "address": 20573, + "ump1_id": 5093, + "ump2_id": 6093, + "description": "sens_lo_edge[21]", + "access": "RW" + }, + { + "modbus": 94, + "address": 20574, + "ump1_id": 5094, + "ump2_id": 6094, + "description": "sens_lo_edge[22]", + "access": "RW" + }, + { + "modbus": 95, + "address": 20575, + "ump1_id": 5095, + "ump2_id": 6095, + "description": "sens_lo_edge[23]", + "access": "RW" + }, + { + "modbus": 96, + "address": 20576, + "ump1_id": 5096, + "ump2_id": 6096, + "description": "Cancount[m_FAST] — пауза быстрых посылок CAN", + "access": "RW" + }, + { + "modbus": 97, + "address": 20577, + "ump1_id": 5097, + "ump2_id": 6097, + "description": "Cancount[m_SLOW] — пауза медленных посылок CAN", + "access": "RW" + }, + { + "modbus": 98, + "address": 20578, + "ump1_id": 5098, + "ump2_id": 6098, + "description": "Brightness / Bright[0] — яркость", + "access": "RW" + }, + { + "modbus": 99, + "address": 20579, + "ump1_id": 5099, + "ump2_id": 6099, + "description": "Owncount / Bright[1]; LOAD: исходное значение АЦП", + "access": "RW" + }, + { + "modbus": 100, + "address": 20580, + "ump1_id": 5100, + "ump2_id": 6100, + "description": "Bright[2]; Current_count: исходное значение канала 0", + "access": "RW" + }, + { + "modbus": 101, + "address": 20581, + "ump1_id": 5101, + "ump2_id": 6101, + "description": "Bright[3]; Current_count: исходное значение канала 1", + "access": "RW" + }, + { + "modbus": 102, + "address": 20582, + "ump1_id": 5102, + "ump2_id": 6102, + "description": "Bright[4]; Current_count: исходное значение канала 2", + "access": "RW" + }, + { + "modbus": 103, + "address": 20583, + "ump1_id": 5103, + "ump2_id": 6103, + "description": "Bright[5]; Current_count: исходное значение канала 3", + "access": "RW" + }, + { + "modbus": 104, + "address": 20584, + "ump1_id": 5104, + "ump2_id": 6104, + "description": "lev_count[], запись из Current_count; назначение по режиму", + "access": "RW" + }, + { + "modbus": 105, + "address": 20585, + "ump1_id": 5105, + "ump2_id": 6105, + "description": "lev_count[], запись из Current_count; назначение по режиму", + "access": "RW" + }, + { + "modbus": 106, + "address": 20586, + "ump1_id": 5106, + "ump2_id": 6106, + "description": "lev_count[], запись из Current_count; назначение по режиму", + "access": "RW" + }, + { + "modbus": 107, + "address": 20587, + "ump1_id": 5107, + "ump2_id": 6107, + "description": "lev_count[], запись из Current_count; назначение по режиму", + "access": "RW" + }, + { + "modbus": 108, + "address": 20588, + "ump1_id": 5108, + "ump2_id": 6108, + "description": "lev_count[], запись из Current_count; назначение по режиму", + "access": "RW" + }, + { + "modbus": 109, + "address": 20589, + "ump1_id": 5109, + "ump2_id": 6109, + "description": "lev_count[], запись из Current_count; назначение по режиму", + "access": "RW" + }, + { + "modbus": 110, + "address": 20590, + "ump1_id": 5110, + "ump2_id": 6110, + "description": "DAC_20 — калибровка входа 20 мА", + "access": "RW" + }, + { + "modbus": 111, + "address": 20591, + "ump1_id": 5111, + "ump2_id": 6111, + "description": "DAC_04 — калибровка входа 4 мА", + "access": "RW" + }, + { + "modbus": 112, + "address": 20592, + "ump1_id": 5112, + "ump2_id": 6112, + "description": "Zero_lev[0] — нулевой уровень канала тока 0", + "access": "RW" + }, + { + "modbus": 113, + "address": 20593, + "ump1_id": 5113, + "ump2_id": 6113, + "description": "Zero_lev[1] — нулевой уровень канала тока 1", + "access": "RW" + }, + { + "modbus": 114, + "address": 20594, + "ump1_id": 5114, + "ump2_id": 6114, + "description": "Zero_lev[2] — нулевой уровень канала тока 2", + "access": "RW" + }, + { + "modbus": 115, + "address": 20595, + "ump1_id": 5115, + "ump2_id": 6115, + "description": "Zero_lev[3] — нулевой уровень канала тока 3", + "access": "RW" + }, + { + "modbus": 116, + "address": 20596, + "ump1_id": 5116, + "ump2_id": 6116, + "description": "Caliber[0]: K380_1 / KPOW_1", + "access": "RW" + }, + { + "modbus": 117, + "address": 20597, + "ump1_id": 5117, + "ump2_id": 6117, + "description": "Caliber[1]: K380_2 / KPOW_2", + "access": "RW" + }, + { + "modbus": 118, + "address": 20598, + "ump1_id": 5118, + "ump2_id": 6118, + "description": "Caliber[2]: KPOW_3", + "access": "RW" + }, + { + "modbus": 119, + "address": 20599, + "ump1_id": 5119, + "ump2_id": 6119, + "description": "Caliber[3]: KPOW_4", + "access": "RW" + }, + { + "modbus": 120, + "address": 20600, + "ump1_id": 5120, + "ump2_id": 6120, + "description": "Caliber[4] / TCaliber[0]; LOAD: DAC_max()", + "access": "RW" + }, + { + "modbus": 121, + "address": 20601, + "ump1_id": 5121, + "ump2_id": 6121, + "description": "Caliber[5] / TCaliber[1]; LOAD: DAC_min()", + "access": "RW" + }, + { + "modbus": 122, + "address": 20602, + "ump1_id": 5122, + "ump2_id": 6122, + "description": "Caliber[6] / TCaliber[2]; LOAD: DAC_cal()", + "access": "RW" + }, + { + "modbus": 123, + "address": 20603, + "ump1_id": 5123, + "ump2_id": 6123, + "description": "Caliber[7] / TCaliber[3]; LOAD: текущий 12-битный код ЦАП", + "access": "RW" + }, + { + "modbus": 124, + "address": 20604, + "ump1_id": 5124, + "ump2_id": 6124, + "description": "NormaU = Caliber[8]; LOAD: LOAD_STATUS", + "access": "RO для LOAD; иначе RW" + }, + { + "modbus": 125, + "address": 20605, + "ump1_id": 5125, + "ump2_id": 6125, + "description": "DeltaU = Caliber[9]; LOAD: LOAD_ATTEMPTS", + "access": "RO для LOAD; иначе RW" + }, + { + "modbus": 126, + "address": 20606, + "ump1_id": 5126, + "ump2_id": 6126, + "description": "LastMode", + "access": "RW" + }, + { + "modbus": 127, + "address": 20607, + "ump1_id": 5127, + "ump2_id": 6127, + "description": "Commands — битовая маска команд", + "access": "RW" + } + ] +} diff --git a/python/protocan/protocan.py b/python/protocan/protocan.py index 5a31d62..74d832a 100644 --- a/python/protocan/protocan.py +++ b/python/protocan/protocan.py @@ -17,6 +17,8 @@ from dataclasses import dataclass, field from enum import IntEnum from typing import List, Optional +from .gas_167 import assigned_metadata, describe_word + try: from .native import get_native_core _SHARED_PROTOCOL = get_native_core() @@ -114,7 +116,7 @@ MSGTYPE_RU = { MsgType.BROADCAST: "Broadcast", MsgType.DISCRETE: "Discrete", MsgType.ANALOG: "Analog", - MsgType.GENERAL_ADDRESS_SPACE: "General Address Space", + MsgType.GENERAL_ADDRESS_SPACE: "GAS (General Address Space)", MsgType.MODBUS_COIL: "Modbus Coil", MsgType.MODBUS_DISCRETE: "Modbus Discrete", MsgType.MODBUS_HOLDING: "Modbus Holding", @@ -622,11 +624,49 @@ def _decode_gas(res: Decoded) -> None: return for adr, v in res.registers: res.fields.append((" Reg 0x%04X" % adr, "0x%04X (%d)" % (v, v))) + _append_gas_metadata(res, adr) + if not vals: + _append_gas_metadata(res, start) if vals: res.summary = "GAS: %d рег. с 0x%04X = %s" % ( len(vals), start, " ".join("%04X" % v for v in vals)) else: res.summary = "GAS: запрос по адресу 0x%04X" % start + # Every monitor, manual decoder and export uses this shared summary. + # Keep names in the summary as well as in the expandable field list. + addresses = res.registers if vals else [(start, None)] + if any(assigned_metadata(address) for address, _value in addresses): + words = [] + for address, value in addresses: + entry = assigned_metadata(address) + label = "ID %d (0x%04X)" % (address, address) + if entry: + label = "УМП %d · %s · %s" % ( + entry['node'] - 4, label, entry['name']) + else: + label += " · имя не назначено" + if value is not None: + label += " = %d [0x%04X]" % (value, value) + description = describe_word(entry['modbus'], value) if entry else '' + if description: + label += " — " + description + res.fields.append((" GAS ID %d / биты" % address, description)) + words.append(label) + res.summary = ("GAS: " if vals else "GAS запрос: ") + "; ".join(words) + + +def _append_gas_metadata(res: Decoded, address: int) -> None: + entry = assigned_metadata(address) + if not entry: + return + label = " GAS ID %d" % address + res.fields += [ + (label, "УМП %d — %s" % (entry['node'] - 4, entry['name'])), + (label + " / статус", entry['status']), + (label + " / Modbus 167/v1", "0x%04X" % entry['modbus']), + (label + " / GAS 167/v1", "0x%04X" % entry['legacy_address']), + (label + " / доступ", entry['access']), + ] def _decode_modbus_bit(res: Decoded) -> None: diff --git a/python/pyproject.toml b/python/pyproject.toml index 37f1664..4486f96 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -19,4 +19,4 @@ include = ["firmware_image*", "altera_logic*", "logic_analyzer*", "set_devices*" [tool.setuptools.package-data] set_devices = ["gas_logger_maps/*.json"] -protocan = ["native/*.dll", "native/*.so", "native/*.dylib"] +protocan = ["*.json", "native/*.dll", "native/*.so", "native/*.dylib"] diff --git a/python/pysetterminal/rawprotocol.py b/python/pysetterminal/rawprotocol.py index 4738e79..249bf28 100644 --- a/python/pysetterminal/rawprotocol.py +++ b/python/pysetterminal/rawprotocol.py @@ -206,10 +206,16 @@ class RawProtocolWidget(QWidget): def init_ui(self): layout = QVBoxLayout() + layout.setProperty("fixedPanelLayout", True) + layout.setContentsMargins(4, 4, 4, 4) + layout.setSpacing(4) # Группа для ПРЯМОГО ДОСТУПА К ПАМЯТИ (PEEK/POKE) mem_group = QGroupBox("Прямой доступ к памяти (PEEK/POKE)") + mem_group.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) mem_layout = QGridLayout() + mem_layout.setContentsMargins(6, 6, 6, 6) + mem_layout.setSpacing(4) mem_layout.addWidget(QLabel("Адрес памяти (hex):"), 0, 0) self.mem_addr_edit = QLineEdit("2000") @@ -239,6 +245,9 @@ class RawProtocolWidget(QWidget): # Группа для RAW отправки raw_group = QGroupBox("RAW отправка/чтение") raw_layout = QVBoxLayout() + raw_layout.setProperty("fixedPanelLayout", True) + raw_layout.setContentsMargins(6, 6, 6, 6) + raw_layout.setSpacing(4) self.raw_send_edit = QLineEdit() self.raw_send_edit.setPlaceholderText("Введите hex данные для отправки") @@ -246,6 +255,7 @@ class RawProtocolWidget(QWidget): raw_layout.addWidget(self.raw_send_edit) hbox = QHBoxLayout() + hbox.setSpacing(4) self.raw_send_btn = QPushButton("Отправить RAW") self.raw_send_btn.clicked.connect(self.send_raw_data) hbox.addWidget(self.raw_send_btn) @@ -260,16 +270,19 @@ class RawProtocolWidget(QWidget): self.raw_output = QTextEdit() self.raw_output.setReadOnly(True) - self.raw_output.setMaximumHeight(100) - raw_layout.addWidget(self.raw_output) + self.raw_output.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + raw_layout.addWidget(self.raw_output, 1) self.raw_clear_btn = QPushButton("Очистить логи") self.raw_clear_btn.clicked.connect(self.clear_logs) raw_layout.addWidget(self.raw_clear_btn, 0, Qt.AlignLeft) + for button in (self.mem_write_btn, self.mem_read_btn, self.raw_send_btn, + self.raw_read_btn, self.raw_clear_btn): + button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed) raw_group.setLayout(raw_layout) - layout.addWidget(raw_group) + layout.addWidget(raw_group, 1) self.setLayout(layout) def memory_write(self): diff --git a/python/pysetterminal/serialbus.py b/python/pysetterminal/serialbus.py index 27dc9c3..bd7499a 100644 --- a/python/pysetterminal/serialbus.py +++ b/python/pysetterminal/serialbus.py @@ -403,24 +403,38 @@ class SerialTab(QWidget): button.setFixedHeight(30) button.setStyleSheet("QPushButton { padding: 3px 10px; }") - # Прокручиваемая область для кнопок макросов - self.macros_scroll_area = QScrollArea() - self.macros_scroll_area.setWidgetResizable(True) - # Устанавливаем фиксированную высоту, чтобы вместить 3-4 строки макросов - self.macros_scroll_area.setMinimumHeight(120) # Высота для 3-4 строк - - self.macros_scroll_content = QWidget() - self.macros_scroll_layout = QGridLayout() - self.macros_scroll_content.setLayout(self.macros_scroll_layout) - self.macros_scroll_area.setWidget(self.macros_scroll_content) - self.macros_scroll_layout.setAlignment(Qt.AlignTop | Qt.AlignLeft) - self.macros_scroll_layout.setContentsMargins(6, 6, 6, 6) - self.macros_scroll_layout.setHorizontalSpacing(8) - self.macros_scroll_layout.setVerticalSpacing(6) - macros_layout.addWidget(self.macros_scroll_area) + self.macros_splitter = QSplitter(Qt.Orientation.Horizontal) + self.macros_splitter.setObjectName("macrosSplitter") + self.macros_splitter.setChildrenCollapsible(False) + self.macros_splitter.setHandleWidth(8) + self.macros_splitter.setToolTip("Перетащите разделитель, чтобы изменить ширину панелей") + self.macros_scroll_areas = [] + self.macros_scroll_layouts = [] + for side in ("Левая", "Правая"): + area = QScrollArea() + area.setAccessibleName(f"{side} панель макросов") + area.setWidgetResizable(True) + area.setMinimumSize(120, 120) + content = QWidget() + grid = QGridLayout(content) + grid.setAlignment(Qt.AlignTop | Qt.AlignLeft) + grid.setContentsMargins(6, 6, 6, 6) + grid.setVerticalSpacing(6) + area.setWidget(content) + self.macros_splitter.addWidget(area) + self.macros_scroll_areas.append(area) + self.macros_scroll_layouts.append(grid) + self.macros_splitter.setStretchFactor(0, 1) + self.macros_splitter.setStretchFactor(1, 1) + self.macros_splitter.setSizes([1, 1]) + # Keep the existing attributes available to terminal integrations. + self.macros_scroll_area = self.macros_scroll_areas[0] + self.macros_scroll_content = self.macros_scroll_area.widget() + self.macros_scroll_layout = self.macros_scroll_layouts[0] + macros_layout.addWidget(self.macros_splitter, 1) macros_group.setLayout(macros_layout) - layout.addWidget(macros_group) + layout.addWidget(macros_group, 1) # --- ВНЕ ГРУППЫ: Результаты чтения --- results_layout = QGridLayout() @@ -447,7 +461,6 @@ class SerialTab(QWidget): results_frame.setLayout(results_layout) layout.addWidget(results_frame) - layout.addStretch() self.setLayout(layout) # После создания интерфейса создаем кнопки для загруженных макросов @@ -590,7 +603,7 @@ class SerialTab(QWidget): container_layout.addWidget(delete_btn) container.setLayout(container_layout) - self.macros_scroll_layout.addWidget(container, row, col, 1, 1) # span изменен на 1 + self.macros_scroll_layouts[col].addWidget(container, row, 0) self.macro_buttons.append(container) @@ -680,7 +693,7 @@ class SerialTab(QWidget): def _set_macro_controls(self, running): self.stop_macro_btn.setEnabled(running) for widget in (self.bus_write_btn, self.bus_read_btn, self.create_macro_btn, - self.macros_scroll_area, self.raw_widget): + self.macros_splitter, self.raw_widget): widget.setEnabled(not running) def _next_macro_command(self, generation): @@ -748,10 +761,11 @@ class SerialTab(QWidget): def rebuild_macros_layout(self): """Перестроить layout макросов""" # Очищаем layout - for i in reversed(range(self.macros_scroll_layout.count())): - item = self.macros_scroll_layout.itemAt(i) - if item and item.widget(): - item.widget().setParent(None) + for grid in self.macros_scroll_layouts: + while grid.count(): + item = grid.takeAt(0) + if item.widget(): + item.widget().deleteLater() # Очищаем список кнопок self.macro_buttons.clear() diff --git a/python/set_devices/balsam_can.py b/python/set_devices/balsam_can.py index 53b7574..c0274ce 100644 --- a/python/set_devices/balsam_can.py +++ b/python/set_devices/balsam_can.py @@ -53,8 +53,8 @@ def decode(raw_id: int, data: bytes, *, ide: bool = True, "0x%04X %s" % (item.address, item.name or "регистр"), "0x%04X (%d)" % (item.value, item.signed_value), )) - if not frame.from_device and item.address == 0x7F: - result.fields.append(("Команды ПМ", frame.command_description(item.value))) + if item.address == 0x7F: + result.fields.append(("Активные биты Commands", frame.command_description(item.value))) if frame.present_mask == 0: result.warnings.append("В адресном слове не отмечен ни один регистр") return result diff --git a/python/set_devices/bus_demo.py b/python/set_devices/bus_demo.py index ee81014..92f1025 100644 --- a/python/set_devices/bus_demo.py +++ b/python/set_devices/bus_demo.py @@ -13,15 +13,59 @@ from __future__ import annotations import random +import math from datetime import datetime from . import can_transport as tr from protocan import protocan as pc +from protocan.gas_167 import REGISTERS #: Типы сообщений, которые генератор выдаёт по кругу случайно. KINDS = ("pulse", "analog", "modbus", "gas", "status", "error", "discrete") +class UmpDemoSource: + """Two simulated UMP nodes using the assigned global GAS map. + + Call next_frames every 250 ms: four words per node per tick and a + heartbeat every second. Device Type=3, Device=5/6 are demo addresses, + not discovery results or a claim about firmware 167/v1 support. + """ + + def __init__(self) -> None: + self._tick = 0 + self._seq = 0 + + def _frame(self, node: int, kind: int, body: int, data: bytes) -> bytes: + self._seq = (self._seq + 1) & 0xFF + identifier = pc.ProtoCanId.build(1, 1, 3, node, kind, body) + return tr.Frame(seq=self._seq, flags=tr.FLAG_IDE, + can_id=identifier, data=data).encode() + + def next_frames(self) -> list[bytes]: + """Return encoded bridge frames; one full map takes eight seconds.""" + packets = [] + # Show LOAD state immediately, then walk the entire dictionary. + start = (124 + self._tick * 4) % len(REGISTERS) + for node, key in ((5, "ump1_id"), (6, "ump2_id")): + if self._tick % 4 == 0: + packets.append(self._frame( + node, pc.MsgType.PULSE, 0, + bytes([(self._tick // 4) & 0xFF]))) + values = [] + for entry in REGISTERS[start:start + 4]: + offset = entry["modbus"] + value = int(2000 + 700 * math.sin(self._tick / 12 + offset / 8 + node)) + command = 0x80 if (self._tick // 32) % 2 == 0 else 0x40 + value = {82: 1000, 124: 8, 125: 0, 126: node, 127: command}.get(offset, value) + values.append(value.to_bytes(2, "little")) + packets.append(self._frame( + node, pc.MsgType.GENERAL_ADDRESS_SPACE, + REGISTERS[start][key], b"".join(values))) + self._tick += 1 + return packets + + class BusDemoSource: """@brief Источник демонстрационных кадров шины. @@ -58,11 +102,15 @@ class BusDemoSource: data = b"".join(self._rng.randint(0, 0xFFFF).to_bytes(2, "little") for _ in range(cnt)) elif kind == "gas": - adr = self._rng.randint(0, 0x400) + # Exercise the shared named map in the ordinary demo too. + dev_type, dev_id = 3, self._rng.choice((5, 6)) + key = "ump1_id" if dev_id == 5 else "ump2_id" + count = self._rng.randint(1, 4) + adr = REGISTERS[self._rng.randrange(len(REGISTERS) - count + 1)][key] cid = pc.ProtoCanId.build(0, 1, dev_type, dev_id, pc.MsgType.GENERAL_ADDRESS_SPACE, adr) data = b"".join(self._rng.randint(0, 0xFFFF).to_bytes(2, "little") - for _ in range(self._rng.randint(1, 4))) + for _ in range(count)) elif kind == "status": now = datetime.now() cid = pc.ProtoCanId.build(1, 1, dev_type, dev_id, pc.MsgType.BROADCAST, diff --git a/python/set_devices/can485_board.py b/python/set_devices/can485_board.py index 27f829e..53ad30c 100644 --- a/python/set_devices/can485_board.py +++ b/python/set_devices/can485_board.py @@ -32,6 +32,8 @@ from collections import deque from dataclasses import dataclass, field from itertools import chain from typing import Iterable +from protocan import protocan as pc +from protocan.gas_167 import REGISTERS #: Распространённые скорости UART для подключения платы и RS485-приёмника. UART_BAUDS = ( @@ -596,7 +598,14 @@ class BoardDemoSource: """ #: Идентификаторы, по которым генератор идёт по кругу. - IDS = ((True, 0x18FEE000), (True, 0x0CF00400), (False, 0x123), (False, 0x7DF)) + IDS = ( + (True, 0x18FEE000), (True, 0x0CF00400), (False, 0x123), (False, 0x7DF), + # Assigned UMP map, simulated to demonstrate the common GAS decoder. + (True, pc.ProtoCanId.build(1, 1, 3, 5, pc.MsgType.GENERAL_ADDRESS_SPACE, + REGISTERS[124]['ump1_id'])), + (True, pc.ProtoCanId.build(1, 1, 3, 6, pc.MsgType.GENERAL_ADDRESS_SPACE, + REGISTERS[124]['ump2_id'])), + ) def __init__(self, rng: random.Random | None = None) -> None: self._rng = rng or random.Random() diff --git a/python/set_devices/can_bridge.py b/python/set_devices/can_bridge.py index 2ac0add..a6d9be9 100644 --- a/python/set_devices/can_bridge.py +++ b/python/set_devices/can_bridge.py @@ -16,6 +16,7 @@ from __future__ import annotations from . import can_transport as tr from protocan import protocan as pc +from protocan.gas_167 import assigned_metadata from . import balsam_can #: Расшифровка поля LEC регистра CAN_ESR. @@ -64,8 +65,27 @@ def decode_frame(frame: tr.Frame) -> pc.Decoded: decoded_balsam = balsam_can.decode( frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr) if decoded_balsam is not None: + decoded_balsam.fields.append(("Автоопределение", "Balsam 167: идентификатор legacy CAN")) return decoded_balsam - return pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr) + result = pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr) + if (frame.ide and not frame.rtr + and result.id.msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE + and len(frame.data) <= 8 and len(frame.data) % 2 == 0 + and result.registers is not None): + addresses = [address for address, _value in result.registers] or [result.id.body] + entries = [assigned_metadata(address) for address in addresses] + if all(entries) and len({entry['node'] for entry in entries}) == 1: + node = entries[0]['node'] - 4 + result.protocol = "УМП / ProtoCAN GAS (назначенные ID)" + result.device_label = "УМП %d" % node + result.message_label = "GAS · УМП %d" % node + result.fields.append(("Автоопределение", + "УМП %d: все GAS ID входят в назначенную карту; поддержка прошивкой не подтверждена" % node)) + return result + result.fields.append(("Автоопределение", "Разбор ProtoCAN; профиль прибора не определён")) + if result.id.msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE: + result.summary += " · профиль/узел не определены — выберите протокол вручную" + return result dec = pc.Decoded(id=pc.ProtoCanId.parse(frame.can_id), dlc=len(frame.data), data=frame.data, ide=frame.ide, rtr=frame.rtr) diff --git a/python/set_devices/can_monitor.py b/python/set_devices/can_monitor.py index c76cda1..70ad7b1 100644 --- a/python/set_devices/can_monitor.py +++ b/python/set_devices/can_monitor.py @@ -10,14 +10,33 @@ from .can_bridge import decode_frame PROTOCOLS = (("auto", "Авто (ProtoCAN / Balsam)"), ("devboard", "DevBoard_V1 / SETCAN"), - ("deif", "DEIF XDi-192 / CANopen")) + ("deif", "DEIF XDi-192 / CANopen"), + ("protocan", "ProtoCAN / GAS"), + ("ump_balsam", "УМП / Balsam 167 (legacy CAN)"), + ("ump1_gas167", "УМП 1 / ProtoCAN — карта GAS 167/v1"), + ("ump2_gas167", "УМП 2 / ProtoCAN — карта GAS 167/v1"), + ("ump_gas", "УМП / ProtoCAN GAS — новые ID 5000/6000")) def decode_protocol(frame, protocol="auto"): if protocol == "deif": from .deif_xdi import decode return decode(frame) - if protocol == "devboard": + if protocol == "ump_balsam": + from .balsam_can import decode + result = decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr) + if result is not None: + return result + result = pc.Decoded(pc.ProtoCanId.parse(frame.can_id), len(frame.data), + bytes(frame.data), ide=frame.ide, rtr=frame.rtr, + protocol="BALZAM 167", message_label="Не Balsam 167") + result.summary = "УМП / Balsam 167: CAN ID или формат кадра не соответствует протоколу" + result.warnings.append(result.summary) + return result + if protocol in ("ump1_gas167", "ump2_gas167"): + from .ump_can import decode_gas167 + return decode_gas167(frame, node=1 if protocol == "ump1_gas167" else 2) + if protocol in ("devboard", "protocan", "ump_gas"): return pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr) return decode_frame(frame) diff --git a/python/set_devices/qt_ports/can_monitor.py b/python/set_devices/qt_ports/can_monitor.py index 311c350..6c13b5c 100644 --- a/python/set_devices/qt_ports/can_monitor.py +++ b/python/set_devices/qt_ports/can_monitor.py @@ -22,6 +22,12 @@ class MonitorControls(QWidget): self.protocol = QComboBox() for key, label in PROTOCOLS: self.protocol.addItem(label, key) + self.protocol.setMinimumContentsLength(28) + self.protocol.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) + self.protocol.setToolTip( + "Выберите формат CAN вручную. УМП / Balsam — legacy CAN; " + "карта 167/v1 — имена слов 0…127 и 0x5000…0x507F в ProtoCAN; " + "новые ID — назначенные диапазоны 5000…5127 и 6000…6127.") self.protocol.setCurrentIndex(self.protocol.findData(default)) layout.addWidget(QLabel("Протокол")) layout.addWidget(self.protocol) diff --git a/python/set_devices/ump_can.py b/python/set_devices/ump_can.py new file mode 100644 index 0000000..10cd4bb --- /dev/null +++ b/python/set_devices/ump_can.py @@ -0,0 +1,50 @@ +"""Explicit interpretation of ProtoCAN registers with the UMP 167/v1 map. + +This is a manually selected dictionary, not detection of the legacy service +CAN packet format or a claim that a particular firmware implements ProtoCAN. +""" +from protocan import protocan as pc +from protocan.gas_167 import REGISTERS, describe_word + + +def decode_gas167(frame, node): + result = pc.decode(frame.can_id, frame.data, ide=frame.ide, rtr=frame.rtr) + result.device_label = "УМП %d (выбран вручную)" % node + result.protocol = "УМП %d / ProtoCAN GAS 167/v1" % node + if not frame.ide or frame.rtr or frame.is_error: + return result + if result.id.msg_type not in (pc.MsgType.GENERAL_ADDRESS_SPACE, + pc.MsgType.MODBUS_HOLDING): + return result + if result.id.msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE: + start = result.id.body + else: + start, _count = pc.split_modbus(result.id.body) + words = result.registers + if words is None: + return result + labels = [] + for address, value in words or [(start, None)]: + offset = address if 0 <= address < 128 else address - 0x5000 + label = "0x%04X" % address + if 0 <= offset < len(REGISTERS): + entry = REGISTERS[offset] + label += " · " + entry['description'] + result.fields += [ + (" УМП 167/v1 0x%04X" % address, entry['description']), + (" Назначенные ID УМП 1/2", "%d / %d" % (entry['ump1_id'], entry['ump2_id'])), + (" Доступ", entry['access']), + ] + else: + label += " · вне карты 167/v1" + if value is not None: + label += " = %d [0x%04X]" % (value, value) + description = describe_word(offset, value) + if description: + label += " — " + description + result.fields.append((" Биты 0x%04X" % address, description)) + labels.append(label) + result.summary = "УМП %d · GAS 167/v1%s: %s" % ( + node, " запрос" if not words else "", "; ".join(labels)) + result.fields.append((" Карта", "167/v1 выбрана вручную")) + return result