Интеграция

Описание протоколов взаимодействия с матч-контроллером Кайросс

Данные от матч-контроллера могут передаваться в двух режимах:

  • по запросу (request, on-demand, pull mode)
  • подписка/прослушивание (subscribe, streaming, push mode)

Доступные протоколы и форматы данных

Протокол HTTP REST Server-Sent Events UDP Multicast NiBUS COLOSSEO
Среда Ethernet Ethernet Ethernet RS-485 RS-485
Режим запрос подписка подписка прослушивание прослушивание
Формат JSON JSON JSON бинарные данные бинарные данные

Имя хоста и адрес матч-контроллера

В некторых случаях вам может понадобиться адрес матч-контроллера в сети. Можно попробовать обратиться к нему по имени matchpad.local. Чтобы на Windows корректно работали адреса вида *.local (mDNS, Multicast DNS), нужно:

  1. Поддержка mDNS в самой системе

    Windows 10 (с недавних версий) и Windows 11 уже имеют встроенную поддержку mDNS. Адреса hostname.local должны работать без дополнительного софта, если включены сетевые службы обнаружения.

  2. Если не работает «из коробки»

    Обычно проблема в том, что на системе не установлен или отключён Bonjour (служба от Apple, используемая для mDNS).

    👉 Возможные варианты:

    • Установить Bonjour Print Services от Apple (или он автоматически ставится вместе с iTunes / iCloud).
    • Убедиться, что служба Bonjour Service (mDNSResponder.exe) запущена.
  3. Настройки сети

    • mDNS работает через UDP порт 5353 на адрес мультикаста 224.0.0.251.
    • Нужно, чтобы брандмауэр Windows и антивирусы не блокировали этот порт.
    • В свойствах сетевого адаптера должно быть включено обнаружение сети.
  4. Проверка

    После установки и запуска службы можно проверить доступность:

    ping matchpad.local
    

    или

    nslookup matchpad.local
    
  5. Посмотреть IP-адрес на матч-контроллере

    Чтобы узнать IP-адреса, выданные контроллеру, откройте настройки подключения Wi-Fi, нажав на строку состояния. При этом не обязательно подключаться к Wi-Fi — на этой странице будут перечислены все выданные адреса.

UDP Multicast

Один из простых способов получать актуальную информацию о ходе игры — подписаться на мультикаст-рассылку по протоколу UDP.

  • Адрес: 232.10.11.12
  • Порт: 55000
  • Формат данных: текстовый JSON в кодировке UTF-8

Общая структура пакета

Поле Обязательное Описание
event да Тип события (например, "tick")
data да Данные события (структура описана ниже)

Ниже приводятся типы событий и форматы данных для каждого из них.

Событие "tick" - время

  • Описание: Обновление текущего времени таймера.
  • Частота срабатывания:
    • Таймер остановлен: 1 раз в секунду
    • Таймер запущен: 10 раз в секунду
Поле Обязательное Значение Пример
kind да тип таймера:
primary - основной
secondary - дополнительный
"kind": "primary"
view да текстовое представление таймера на табло "view": "09:48.3"
isRunning да true - таймер запущен
false - таймер остановлен
"isRunning": false
isRestTimer нет true - таймер перерыва
false - игровой таймер
"isRestTimer": false
elapsed нет Прошедшее время с момента старта в мс "elapsed": 2347
timestamp да Unix-время отправки данного сообщения "timestamp": 1759405394646
name нет Имя таймера "name": "quarter"
homePenalties нет Массив удаленных команды Хозяев:
number - номер удаленного
timeLeft - оставшееся время
"homePenalties": [{ "number": "12", "timeLeft": "1:21" }]
awayPenalties нет Массив удаленных команды Гостей:
number - номер удаленного
timeLeft - оставшееся время
"awayPenalties": [{ "number": "8", "timeLeft": "0:13" }]
shortClock нет 24-секунды
value - значение
isRunning - активно
"shortClock": { "value": "18", isRunning: true }
nibusId нет NiBUS-идентификатор таймера "nibusId": 1

Событие "scoreboard" - данные на табло

  • Описание: Обновление информации на табло (счёт, фолы, тайм-ауты и т.д.).
  • Частота срабатывания: при изменении состояния, но не реже 1 раза каждые 30 секунд
Поле Обязательное Значение Пример
sportSlug да Вид спорта "sportSlug": "basketball"
home да Команда Хозяев "home": {"score":0,"fouls":0,"name":"ЦСКА","town":"Москва","teamId":"n7hitgakhv1kxhvy89ilvhdp","timeouts":0}
home.score да Очки команды Хозяев "score": 0
home.fouls да Фолы команды Хозяев "fouls": 0
home.name да Название команды Хозяев "name": "ЦСКА"
home.town да Город команды Хозяев "town": "Москва"
home.teamId нет Идентификатор команды Хозяев "teamId": "n7hitgakhv1kxhvy89ilvhdp"
home.timeouts да Тайм-ауты команды "timeouts": 0
home.lineup нет Массив номеров игроков на площадке "lineup": ["11", "14", "22", "31", "44"]
home.filedOutPlayers нет Массив номеров удаленных до конца игры "filedOutPlayers": ["33"]
home.lastServerNumber нет В волейболе номер последнего подающего игрока "lastServerNumber": "12"
home.rotationCount нет В волейболе количество ротаций от начальной расстановки "rotationCount": 5
home.substitutionsCount нет Количество замен "substitutionsCount": 3
away да Команда Гостей, аналогично команде Хозяев "away": {"score":0,"fouls":0,"name":"УНИКС","town":"Казань","teamId":"lxf3lhv3vc6byoj7q5yxqoqb","lineup":["2","4","5","12","15"],"timeouts":0}
period да Период "period": "OT"
tournament нет Название турнира "tournament": "Единая лига ВТБ"
competitionId нет Идентификатор встречи "competitionId":"abs6b7s802kzra2gy6jed4km"
setScores нет Счет по партиям "setScores": [{"home": 21, "away": 19 }, {"home": 13, "away": 21 }]

Событие "updateLogo" - обновить логотип команды на табло

С помощью REST API можно загрузить логтип команды или фото игрока по его идентификатору используя URL http://matchpad.local/media/{mediaId}

Поле Обязательное Значение Пример
team да Команда:
home - Хозяев
away - Гостей
"team": "home"
primaryColor нет Основной цвет команды в hex "primaryColor": "#e31024"
secondaryColor нет Дополнительный цвет команды в hex "secondaryColor": "#cc1b51"
textColor нет Цает текста в hex "textColor": "#ffffff"
logo нет Логотип команды "logo":{"id":"gv65tvb1tsnnghaowjfzsyis","width":143,"height":80}
logo.id да идентификатор логотипа "id": "gv65tvb1tsnnghaowjfzsyis"
logo.width нет Ширина логотипа в пикселях "width": 143
logo.height нет Высота логотипа в пикселях "height": 80

Событие "goalScoredBy" - гол забил

  • Описание: Отправляется при взятии ворот, содержит информацию об игроке, забившем гол, и при необходимости — об ассистентах. Используется для вывода сообщения на табло.
  • Виды спорта: Все, где фиксируются голы (футбол, хоккей, гандбол и т. п.).
  • Частота срабатывания: по факту события (не периодически).
Поле Обязательное Значение Пример
teamKind да Команда:
home - Хозяев
away - Гостей
"teamKind": "away"
number да Номер игрока "number": "00"
name нет Имя и Фамилия игрока "name": "Александр Иванов"
mediaId нет Идентификатор фото игрока "mediaId": "gv65tvb1tsnnghaowjfzsyis"
team нет Сведения о команде "team": { "name": "Дружба", "town": "Простоквашино", "logo": "lxf3lhv3vc6byoj7q5yxqoqb" }
team.name да Название команды "name": "Дружба"
team.town нет Родной город команды "town": "Простоквашино"
team.logo нет Идентификатор логотипа команды "logo": "gv65tvb1tsnnghaowjfzsyis"

Событие "playerStateChanged" - обновленная статистика игрока

  • Описание: Передаёт обновлённые данные по отдельному игроку: очки, фолы, количество голов, штрафы и т. д.
  • Виды спорта: Все командные виды, где ведётся индивидуальная статистика (например, баскетбол).
  • Частота срабатывания: при каждом изменении показателей игрока (например, добавлен гол, фол, очко и т. п.).
Поле Обязательное Значение Пример
team да Команда:
home - Хозяев
away - Гостей
"team": "home"
number нет Номер игрка "number": "27"
points нет Общее количество очков "points": 25
fouls нет Общее количество фолов "fouls": 2
status нет Статус игрока "status": "disqualified"

Пример UDP-клиента на python

👉 Если не приходят пакеты, попробуйте заменить IFACE IP-адресом вашего компьютера из одной сети с матч-контроллером.

# udp_client_async.py
import asyncio
import socket
import struct
import json
import sys

MCAST_GRP = "232.10.11.12"
MCAST_PORT = 55000
BUFFER_SIZE = 65536

# Укажите здесь IP вашего локального интерфейса.
# Например: "192.168.1.42" или оставьте "0.0.0.0" (INADDR_ANY).
IFACE = "0.0.0.0"

class MulticastProtocol(asyncio.DatagramProtocol):
    def connection_made(self, transport):
        self.transport = transport
        sock = transport.get_extra_info("socket")
        print("transport ready, sock:", sock.getsockname())

    def datagram_received(self, data, addr):
        try:
            message = json.loads(data.decode("utf-8"))
            event_type = message.get("event", "unknown")
            event_data = message.get("data", message)
            print(f"📩 Новое событие: {event_type} | data={event_data}")
        except Exception as e:
            print("⚠️ Ошибка парсинга/обработки пакета:", e, "raw:", data)

    def error_received(self, exc):
        print("❌ Ошибка UDP:", exc)

    def connection_lost(self, exc):
        print("❌ Соединение закрыто", exc)

async def main():
    loop = asyncio.get_running_loop()

    # Создаем сокет и настраиваем опции до передачи в asyncio
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # На macOS/новых Linux полезно разрешить REUSEPORT (если доступно)
    if hasattr(socket, "SO_REUSEPORT"):
        try:
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
        except OSError:
            pass

    # Привязываемся к порту на всех интерфейсах
    sock.bind(("", MCAST_PORT))

    # Подписываемся на мультикаст
    try:
        # Для macOS/Unix предпочтительнее 4s4s: group + local_if
        mreq = socket.inet_aton(MCAST_GRP) + socket.inet_aton(IFACE)
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
    except Exception as e:
        print("⚠️ Не удалось присоединиться к мультикаст-группе:", e)
        print("   Попробуй поставить IFACE = реальный IP интерфейса (например, ipconfig getifaddr en0).")
        # не выходим — иногда join не обязателен для теста

    sock.setblocking(False)

    transport, protocol = await loop.create_datagram_endpoint(
        lambda: MulticastProtocol(),
        sock=sock
    )

    print(f"🔌 Подключение к UDP Multicast {MCAST_GRP}:{MCAST_PORT} (IFACE={IFACE})...")

    try:
        # держим соединение бесконечно
        await asyncio.Event().wait()
    except asyncio.CancelledError:
        pass
    except KeyboardInterrupt:
        print("\n❌ Остановка клиента...")
    finally:
        transport.close()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        sys.exit(0)

Server-Sent Events (SSE)

Ещё один способ получать актуальную информацию — это подписка на события по протоколу SSE по адресу:
http://matchpad.local/api/v1/events

Вместо имени сервера matchpad.local можно использовать IP-адрес матч-контроллера.
Подписку можно проверить прямо в браузере, просто открыв указанную ссылку.

Протокол SSE идеально подходит для браузерных клиентов — например, для вывода информации о матче на HTML-странице. Именно этот механизм используется встроенным веб-сервером для отображения данных на видеотабло и SmartTV.

Пример открытой ссылки в браузере

event: scoreboard
data: {"sportSlug":"basketball","home":{"score":0,"fouls":0,"name":"ЦСКА","town":"Москва","teamId":"n7hitgakhv1kxhvy89ilvhdp","lineup":["2","8","9","11","41"],"timeouts":0},"away":{"score":0,"fouls":0,"name":"УНИКС","town":"Казань","teamId":"lxf3lhv3vc6byoj7q5yxqoqb","lineup":["2","4","5","12","15"],"timeouts":0},"period":"1","tournament":"Единая лига ВТБ","competitionId":"abs6b7s802kzra2gy6jed4km"}

event: updateLogo
data: {"team":"home","logo":{"id":"af5t865jqxyoebsot7jgil9t","width":147,"height":216},"primaryColor":"#d61a20","secondaryColor":"#003274","textColor":"#ffffff"}

event: updateLogo
data: {"team":"away","logo":{"id":"y2dny9a6jmj4zar666a0gtij","width":1820,"height":1820},"primaryColor":"#2faf6b","secondaryColor":"#d36d14","textColor":"#ffffff"}

event: tick
data: {"kind":"primary","view":"09:57.3","isRunning":false,"elapsed":2727,"timestamp":1759494388845,"name":"quarter","nibusId":1}

👉 Формат передаваемых данных полностью совпадает с форматом, описанным в разделе UDP Multicast.

Пример SSE-клиента на python

# sse_client_async.py
import asyncio
import aiohttp
import json

SSE_URL = "http://matchpad.local/api/v1/events"

async def sse_client(url):
    while True:
        try:
            print("🔌 Подключение к SSE...")
            async with aiohttp.ClientSession() as session:
                async with session.get(url) as resp:
                    if resp.status != 200:
                        raise Exception(f"Ошибка подключения: {resp.status}")

                    event_type = "message"
                    data_buffer = []

                    async for line_bytes in resp.content:
                        line = line_bytes.decode("utf-8").strip()
                        if line.startswith("event:"):
                            event_type = line[6:].strip()
                        elif line.startswith("data:"):
                            data_buffer.append(line[5:].strip())
                        elif line == "":
                            # пустая строка — конец события
                            if data_buffer:
                                raw_data = "\n".join(data_buffer)
                                try:
                                    data = json.loads(raw_data)
                                except json.JSONDecodeError:
                                    data = raw_data
                                print(f"📩 Новое событие: {event_type} | data={data}")
                            # сброс
                            event_type = "message"
                            data_buffer = []

        except Exception as e:
            print("❌ Ошибка SSE:", e)

        await asyncio.sleep(5)
        print("♻️ Переподключение...")

async def main():
    await sse_client(SSE_URL)

if __name__ == "__main__":
    asyncio.run(main())

REST API

Полная справка в формате openapi/swagger по REST API доступна при подключении к матч-контроллеру с помощью браузера по адресу http://matchpad.local/api-docs. Вместо matchapd.local можно указать IP-адрес матч-контроллера.

Протокол NiBUS

NiBUS — проприетарный промышленный протокол фирмы Ната-Инфо. Скачать описание протокола NiBUS.pdf

👉 Полное описание протокола выходит за рамки данной публикации. Здесь рассмотрены только ключевые особенности, важные для интеграции матч-контроллера.

Особенностью протокола NiBUS является использование специальных двухбайтовых маркеров (служебных последовательностей), которые передаются без паузы между байтами и используются для управления доступом к каналу RS-485. Некоторые "умные" контроллеры и преобразователи, например устройства MOXA, могут некорректно обрабатывать такие маркеры из-за особенностей внутренней буферизации и автоматического управления направлением передачи, что приводит к искажению данных (“мусору”). Для стабильной работы рекомендуется использовать трансиверы, разработанные Ната-Инфо — Siolynx2 и C22. Скорость подключения к Siolynx2 — 115200 бод, для остальных устройств — 57600 бод.

Каждый кадр данных начинается с преамбулы 0x7e, затем идут адреса получателя и отправителя, сервисные поля, но первое, что имеет значение для анализа — это размер N полученного кадра, который расположен в байте со смещением 14 байт от начала кадра. После определения размера можно вычислить окончание кадра (17 + N), рассчитать контрольную сумму и сверить её с полученными данными.

Формат кадра

Поле Смещение в байтах Значение/Фильтр
Преамбула 0 0x7e
... Адреса и сервисные поля
Размер 14 Размер кадра N
Тип кадра 15 Оставляем только со значением 0x01
Тип сервиса 16 Оставляем только со значением 0x18
ID 17 Идентификатор ID (см. ниже)
Длина nms 18 Длина данных nms M
Тип данных 19 Тип данных TYPE (см. ниже)
Данные 20..[19+M] Данные DATA
CRC [15 + N]..[15+(N+1)] Контрольная сумма (big-endian)

Стандартные типы данных

Тип Значение Размер Описание
VT_BOOL 0x0b 8 бит Значение TRUE = 1/FALSE = 0
VT_I1 0x10 8 бит Знаковый байт
VT_I2 0x02 16 бит Знаковое короткое целое
VT_I4 0x03 32 бита Знаковое целое
VT_I8 0x14 64 бита Знаковое длинное целое
VT_UI1 0x11 8 бит Байт
VT_UI2 0x12 16 бит Короткое целое
VT_UI4 0x13 32 бита Целое
VT_UI8 0x15 64 бита Длинное целое
VT_R4 0x04 32 бита Значение с плавающей точкой
VT_R8 0x05 64 бита Значение с плавающей точкой удвоенной точности
VT_LPSTR 0x1e Строка символов с терминирующим нулем
VT_DATE 0x07 80 бит Дата/время в формате BCD
DD-MM-YYYY HH:MM:SS.0mmmbW
DD – дата
MM – месяц
YYYY – год
HH – час (0..23)
MM – минуты
SS – секунды
mmm – миллисекунды
W – день недели (1..7, 1 – вс, 2 – пн, … 7 – сб)
b – зарезервировано
VT_VECTOR 0x80 Модификатор типа. Означает вектор типизированных элементов

Контрольная сумма

Циклическая контрольная сумма подсчитывается (не включает преамбулу) по стандарту CRC16-ССITT. Делитель равен 1 0001 0000 0010 0001 = 0x1021. Начальное значение 0x0000. Хранится в формате big-endian.

Детализация информационных сообщений

👉 Вся текстовая информация в сообщениях представлена кодировкой Windows-1251, бинарные данные используют порядок байт (endianness) little-endian, формат BCD означает binary-coded decimal (например 3810 хранится и передается как байт 0x38) .

Время

ID: 0x05
TYPE: VT_UI1 | VT_VECTOR (0x91)
DATA:

+0 байт - флаги:

бит название описание
0 TFM_ACTIVE таймер активизирован
1 TFM_DOTS включить точки между часами и минутами
(используется в основном для показа реального времени)
2 TFM_REST_TIMER таймер отсчитывает время перерыва
3 TFM_MILLS_IMPORTANT необходимо показать миллисекунды
4 TFM_MILLIS_10RESOLUTION разрешение миллисекунд 1/10
5 TFM_HIDDEN не отображать таймер
6 TFM_SECONDARY не отображать данный таймер на знакоместах основного времени

+1 байт - идентификатор таймера:

для баскетбола:

Значение Таймер
0x01 игровой таймер
0x02 перерыв
0x03 время до начала матча
0x04 тайм-аут
0x05 овертайм
0x06 перерыв овертайма
0x07 короткий перерыв
0x20 время атаки (24c)

для волейбола:

Значение Таймер
0x01 перерыв
0x02 тайм-аут
0x03 технический тайм-аут

для гандбола:

Значение Таймер
0x01 игровой таймер
0x02 перерыв
0x03 время до начала матча
0x04 тайм-аут
0x05 овертайм
0x06 перерыв овертайма
0x07 короткий перерыв

для мини-футбола (футзал):

Значение Таймер
0x01 игровой таймер
0x02 перерыв
0x03 время до начала матча
0x04 тайм-аут

для хоккея:

Значение Таймер
0x01 игровой таймер
0x02 перерыв
0x03 время до начала матча
0x04 тайм-аут
0x05 овертайм
0x06 перерыв овертайма

+3 байта - минуты (BCD)

+4 байта - секунды (BCD)

+5 байт - доли секунд (BCD)

Счет команды хозяев

ID: 0x06
TYPE: VT_UI2 (0x12)
DATA: +0..1 байт - значение счета

Счет команды гостей

ID: 0x07
TYPE: VT_UI2 (0x12)
DATA: +0..1 байт - значение счета

Период

ID: 0x08
TYPE: VT_UI1 (0x11)
DATA: +0 байт - значение периода

Фолы команды хозяев

ID: 0x09
TYPE: VT_UI1 (0x11)
DATA: +0 байт - значение

Фолы команды гостей

ID: 0x0a
TYPE: VT_UI1 (0x11)
DATA: +0 байт - значение

Перерывы команды хозяев

ID: 0x0e
TYPE: VT_UI1 (0x11)
DATA: +0 байт - значение

Перерывы команды гостей

ID: 0x0f
TYPE: VT_UI1 (0x11)
DATA: +0 байт - значение

Количество игроков в команде

ID: 0x10
TYPE: VT_UI1 | VT_VECTOR (0x91)
DATA:

+0 байт - команда:

Значение Команда
0x00 Хозяева
0x01 Гости
0x02 Судьи

+1..2 байт - количество игроков

Информация о игроке

ID: 0x11
TYPE: VT_UI1 | VT_VECTOR (0x91)
DATA:

+0 байт - команда:

Значение Команда
0x00 Хозяева
0x01 Гости
0x02 Судьи

+1 байт - зарезервировано

+2 байт - индекс игрока (начинается с 0)

+3 байт - номер игрока (BCD)

+4 байт - роль игрока:

Значение Роль
0x00 Основной состав
0x01 Запасной
0x02 Капитан
0x03 Тренер

+5 байт - фамилия (не более 30 знаков, терминированная 0), страна (не более 20 знаков), допинформация (терминированная 0)

Пример: Петров А.\0РОССИЯ\0\0

Статистика по игроку

ID: 0x12
TYPE: VT_UI1 | VT_VECTOR (0x91)
DATA:

+0 байт - команда:

Значение Команда
0x00 Хозяева
0x01 Гости
0x02 Судьи

+1 байт - номер игрока (BCD)

+2 байт - индекс игрока (начинается с 0)

+3 байт - суммарное число очков (BCD)

+4 байт - cуммарное число фолов (BCD)

+5 байт - статус игрока:

Бит Статус
0 Находится на площадке
1 В пакете присутствует статистика по броскам

Опционально:

+6 байт - количество 1-очковых бросков игрока (BCD)

+7 байт - количество 2-очковых бросков игрока (BCD)

+8 байт - количество 3-очковых бросков игрока (BCD)

Название команды хозяев

ID: 0x13
TYPE: VT_LPSTR (0x1e)
DATA: +0..M-1 байт - название команды, терминированное 0

Название команды гостей

ID: 0x14
TYPE: VT_LPSTR (0x1e)
DATA: +0..M-1 байт - название команды, терминированное 0

Город/страна команды хозяев

ID: 0x15
TYPE: VT_LPSTR (0x1e)
DATA: +0..M-1 байт - город/страна, терминированное 0

Город/страна команды гостей

ID: 0x16
TYPE: VT_LPSTR (0x1e)
DATA: +0..M-1 байт - город/страна, терминированное 0

Название турнира

ID: 0x17
TYPE: VT_LPSTR (0x1e)
DATA: +0..M-1 байт - название турнира, терминированное 0

Указатель владения мячом

ID: 0x18
TYPE: VT_UI1 (0x11)
DATA: +0 байт - значение

Значение Указатель
0x00 Погашен
0x01 Хозяева
0x02 Гости

Вывод информационного сообщения

ID: 0x19
TYPE: VT_UI1 | VT_VECTOR (0x91)
DATA:

+0 байт - тип сообщения:

Значение Тип
0x0b мяч забросил
0x0c фол

+1..M-1 байт - фаимлия игрока (не более 30 знаков, терминированная 0), номер игрока (не более 2 знаков), название команды (не более 20 знаков), допинформация (терминированная 0)

Пример: Петров А.\012\0ЦСКА\0\0

Переключение провайдера игры (смена вида спорта)

ID: 0x1b
TYPE: VT_UI2 (0x12)
DATA: +0..1 байт - вид спорта

Значение Вид спорта
0x0000 хоккей
0x0001 баскетбол
0x0002 волейбол
0x0003 гандбол
0x0004 футзал

Статистика по удалениям (Гандбол/Мини-футбол/Хоккей)

ID: 0x1c
TYPE: VT_UI1 | VT_VECTOR (0x91)
DATA:

+0 байт - количество строк (всего позиций 2*N, для 2х команд)

Строка 1 (хозяева)

+1 байт - флаги:

Бит Флаг
0 данная строка валидна
1 1 - время удаления активно, 0 - отложенный штраф

+2 байт - номер игрока (BCD)

+3 байт - минуты (BCD)

+4 байт - секунды (BCD)

+5 байт - зарезервировано

Всего N строк для хозяев, затем N строк для гостей: 5*(2*N) байт.

Статистика "Волейбол"

ID: 0x1d
TYPE: VT_UI1 | VT_VECTOR (0x91)
DATA:

+0 байт - счет команды хозяев в 1-партии

+1 байт - счет команды хозяев в 2-партии

+2 байт - счет команды хозяев в 3-партии

+3 байт - счет команды хозяев в 4-партии

+4 байт - счет команды хозяев в 5-партии

+5 байт - счет команды гостей в 1-партии

+6 байт - счет команды гостей в 2-партии

+7 байт - счет команды гостей в 3-партии

+8 байт - счет команды гостей в 4-партии

+9 байт - счет команды гостей в 5-партии

Количество замен в команде (Волейбол)

ID: 0x1e
TYPE: VT_UI1 | VT_VECTOR (0x91)
DATA:

+0 байт - команда:

Значение Команда
0x00 Хозяева
0x01 Гости

+1 байт - количество замен

Пример NiBUS-парсера на Python

# nibus_parser.py
import serial
import struct
import sys

# Настройки порта
SERIAL_PORT = "COM3"  # Измените на ваш порт (например, "/dev/ttyUSB0" на Linux/macOS)
BAUD_RATE = 57600  # Укажите 115200, если используете Siolynx2

# Константы протокола
PREAMBLE = 0x7E
FRAME_TYPE = 0x01
SERVICE_TYPE = 0x18

# Типы данных NiBUS
VT_TYPES = {
    0x0B: ("VT_BOOL", 1),
    0x10: ("VT_I1", 1),
    0x02: ("VT_I2", 2),
    0x03: ("VT_I4", 4),
    0x14: ("VT_I8", 8),
    0x11: ("VT_UI1", 1),
    0x12: ("VT_UI2", 2),
    0x13: ("VT_UI4", 4),
    0x15: ("VT_UI8", 8),
    0x04: ("VT_R4", 4),
    0x05: ("VT_R8", 8),
    0x1E: ("VT_LPSTR", None),  # строка переменной длины
    0x07: ("VT_DATE", 10),
    0x91: ("VT_UI1|VT_VECTOR", None),  # вектор байтов
}


def calc_crc16(data):
    """Вычисление CRC16-CCITT (полином 0x1021, начальное значение 0x0000)"""
    crc = 0x0000
    for byte in data:
        crc ^= byte << 8
        for _ in range(8):
            if crc & 0x8000:
                crc = (crc << 1) ^ 0x1021
            else:
                crc = crc << 1
            crc &= 0xFFFF
    return crc


def parse_data(data_type, data_bytes):
    """Парсинг данных в зависимости от типа"""
    if data_type not in VT_TYPES:
        return f"Unknown type 0x{data_type:02x}", data_bytes.hex()
    
    type_name, size = VT_TYPES[data_type]
    
    if data_type == 0x1E:  # VT_LPSTR - строка
        try:
            # Ищем терминирующий ноль
            end = data_bytes.find(b'\x00')
            if end == -1:
                return type_name, data_bytes.decode('windows-1251', errors='replace')
            return type_name, data_bytes[:end].decode('windows-1251', errors='replace')
        except:
            return type_name, data_bytes.hex()
    
    elif data_type == 0x91:  # VT_UI1|VT_VECTOR - массив байтов
        return type_name, ' '.join(f'{b:02x}' for b in data_bytes)
    
    elif data_type == 0x0B:  # VT_BOOL
        return type_name, bool(data_bytes[0])
    
    elif data_type in [0x11, 0x10]:  # VT_UI1, VT_I1
        fmt = 'B' if data_type == 0x11 else 'b'
        return type_name, struct.unpack(fmt, data_bytes[:1])[0]
    
    elif data_type in [0x12, 0x02]:  # VT_UI2, VT_I2
        fmt = '<H' if data_type == 0x12 else '<h'
        return type_name, struct.unpack(fmt, data_bytes[:2])[0]
    
    elif data_type in [0x13, 0x03]:  # VT_UI4, VT_I4
        fmt = '<I' if data_type == 0x13 else '<i'
        return type_name, struct.unpack(fmt, data_bytes[:4])[0]
    
    elif data_type in [0x15, 0x14]:  # VT_UI8, VT_I8
        fmt = '<Q' if data_type == 0x15 else '<q'
        return type_name, struct.unpack(fmt, data_bytes[:8])[0]
    
    elif data_type == 0x04:  # VT_R4 - float
        return type_name, struct.unpack('<f', data_bytes[:4])[0]
    
    elif data_type == 0x05:  # VT_R8 - double
        return type_name, struct.unpack('<d', data_bytes[:8])[0]
    
    elif data_type == 0x07:  # VT_DATE - BCD дата/время
        return type_name, ' '.join(f'{b:02x}' for b in data_bytes[:10])
    
    return type_name, data_bytes.hex()

def parse_nibus_frame(frame):
    """Парсинг кадра NiBUS"""
    if len(frame) < 20:
        print("⚠️ Слишком короткий кадр")
        return False
    
    # Проверка преамбулы
    if frame[0] != PREAMBLE:
        print(f"⚠️ Неверная преамбула: 0x{frame[0]:02x}")
        return False
    
    # Размер кадра
    frame_size = frame[14]
    
    if len(frame) < 15 + frame_size + 2:
        print(f"⚠️ Неполный кадр (ожидается {15 + frame_size + 2}, получено {len(frame)})")
        return False
    
    # Тип кадра и сервиса
    frame_type = frame[15]
    service_type = frame[16]
    
    if frame_type != FRAME_TYPE:
        return False  # Пропускаем без вывода
    
    if service_type != SERVICE_TYPE:
        return False  # Пропускаем без вывода
    
    # ID, длина данных, тип данных
    msg_id = frame[17]
    data_len = frame[18]
    data_type = frame[19]
    
    # Данные
    data_start = 20
    data_end = data_start + data_len
    data_bytes = frame[data_start:data_end]
    
    # CRC
    crc_start = 15 + frame_size
    crc_received = struct.unpack('<H', frame[crc_start:crc_start + 2])[0]
    
    # Проверка CRC (без преамбулы)
    crc_calculated = int.from_bytes(calc_crc16(frame[1:crc_start]).to_bytes(2, 'big'), 'little')
    
    if crc_calculated != crc_received:
        print(f"⚠️ Ошибка CRC: вычислено 0x{crc_calculated:04x}, получено 0x{crc_received:04x}")
        return False
    
    # Парсинг данных
    type_name, parsed_data = parse_data(data_type, data_bytes)
    
    # Вывод
    print(f"📦 ID: 0x{msg_id:02x} | TYPE: {type_name} (0x{data_type:02x}) | DATA: {parsed_data}")
    return True

def main():
    print(f"🔌 Подключение к {SERIAL_PORT} на скорости {BAUD_RATE} бод...")
    
    try:
        ser = serial.Serial(
            port=SERIAL_PORT,
            baudrate=BAUD_RATE,
            bytesize=serial.EIGHTBITS,
            parity=serial.PARITY_NONE,
            stopbits=serial.STOPBITS_ONE,
            timeout=1
        )
        
        print("✅ Порт открыт. Ожидание данных...")
        
        buffer = bytearray()
        
        while True:
            # Читаем данные
            if ser.in_waiting:
                chunk = ser.read(ser.in_waiting)
                buffer.extend(chunk)
            
            # Ищем преамбулу
            while len(buffer) >= 20:
                preamble_pos = buffer.find(PREAMBLE)
                
                if preamble_pos == -1:
                    # Преамбулы нет, очищаем буфер
                    buffer.clear()
                    break
                
                # Удаляем мусор до преамбулы
                if preamble_pos > 0:
                    buffer = buffer[preamble_pos:]
                
                # Проверяем, достаточно ли данных для определения размера
                if len(buffer) < 15:
                    break
                
                frame_size = buffer[14]
                total_size = 15 + frame_size + 2  # +2 для CRC
                
                # Ждём полного кадра
                if len(buffer) < total_size:
                    break
                
                # Извлекаем кадр
                frame = buffer[:total_size]
                buffer = buffer[total_size:]
                
                # Парсим кадр
                parse_nibus_frame(frame)
    
    except serial.SerialException as e:
        print(f"❌ Ошибка порта: {e}")
        sys.exit(1)
    except KeyboardInterrupt:
        print("\n❌ Остановка...")
        sys.exit(0)
    finally:
        if 'ser' in locals() and ser.is_open:
            ser.close()
            print("🔌 Порт закрыт")

if __name__ == "__main__":
    main()

COLOSSEO Universal protocol

Этот протокол используется, если вы подключаете табло или часы производства COLOSSEO.

📄 Скачать описание протокола:
COLOSSEO Universal Protocol.pdf

⚠️ Обратите внимание: нельзя одновременно использовать оба протокола — NiBUS и COLOSSEO.
Необходимо выбрать только один.