Добавить поддержку GAS 167 и УМП, улучшить обмен по CAN и последовательному порту

This commit is contained in:
2026-10-11 21:23:21 +03:00
parent 3c0f6fd9e6
commit 20454ff7fa
17 changed files with 1797 additions and 37 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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-снифер; генератор работает локально без передачи в оборудование.

View File

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

View File

@@ -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 ещё не обслуживает'}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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