На связи удалённо · офис завтра с 09:00

Автоматизация

Касса, две программы и 248 мегабайт в никуда

Медицинский центр. На ресепшене касса АТОЛ, подключённая к компьютеру по USB; Windows видит её как COM4.

Медицинский центр. На ресепшене касса АТОЛ, подключённая к компьютеру по USB; Windows видит её как COM4. С кассой работают две программы: медицинская учётная система и 1С. Обе хотят печатать чеки, а COM-порт в Windows может открыть только кто-то один.

Кто-то до нас решил это «по-умному»: поставил утилиту, которая расшаривает один COM-порт на несколько программ. Первое время работало. Потом компьютер начал подвисать, а раз в несколько дней падать в синий экран. Прямо во время приёма, с очередью пациентов у стойки.

Цифра, которая всё объяснила

В статистике утилиты виртуальных портов мы увидели это:

Отправлено: 247,9 МБ
Получено:   0 байт

Касса печатает чеки. Чек — это килобайты. Четверть гигабайта, ушедшая в порт, и ноль ответов означали, что программы не печатали, а без остановки кричали в порт.

Почему так

Касса работает по принципу «запрос — ответ»: программа отправляет команду и ждёт, касса отвечает, следующая команда. Это разговор двоих.

Когда порт расшарен, обе программы слышат всё: и свои ответы, и чужие. Учётная система отправляет запрос и получает ответ, который предназначался 1С. Она считает его ошибкой и повторяет свой запрос. 1С в это время делает то же самое. Каждый повтор порождает новый чужой ответ, каждый чужой ответ — новый повтор. Получается шторм, который драйвер виртуального порта в какой-то момент не выдерживает и падает. А падение драйвера в ядре — это синий экран.

Правильное решение и почему его нельзя было сделать сразу

Правильно — убрать COM-порт из уравнения. Современные кассы АТОЛ умеют работать по сети через USB (режим EthernetOverUSB): у кассы появляется свой адрес, и несколько программ подключаются к ней штатно, по TCP, как к сетевому принтеру. Но для этого нужно перенастроить медицинскую систему, а это работа её разработчика со своей очередью и сроками. Пациенты ждать не могли.

Арбитр

Поэтому мы написали небольшую программу-арбитра. Она одна владеет настоящим COM4. Каждая кассовая программа получает свой отдельный виртуальный порт, а арбитр слушает их и работает как вежливый секретарь: принял запрос от одной программы, отнёс кассе, дождался ответа, вернул ответ именно той программе, которая спрашивала. Вторая в это время ждёт своей очереди и чужих ответов не видит вообще.

Скелет логики выглядит так (рабочая версия длиннее: там журнал, переподключение и обработка служебных кадров):

import configparser, threading, time
import serial  # pyserial

cfg = configparser.ConfigParser()
cfg.read("arbiter.ini", encoding="utf-8")

KKT = serial.Serial(cfg["kkt"]["port"], int(cfg["kkt"]["baud"]), timeout=0.05)
REPLY_TIMEOUT = float(cfg["kkt"]["reply_timeout"])
kkt_lock = threading.Lock()          # касса говорит только с одним клиентом


def read_frame(port, timeout):
    """Читает один кадр целиком. frame_size() разбирает заголовок
    по полю длины из протокола ККТ и возвращает None, пока длина неизвестна."""
    deadline = time.monotonic() + timeout
    buf = bytearray()
    while time.monotonic() < deadline:
        buf += port.read(port.in_waiting or 1)
        size = frame_size(buf)
        if size and len(buf) >= size:
            return bytes(buf[:size])
    return None


def serve(client_name):
    client = serial.Serial(client_name, KKT.baudrate, timeout=0.05)
    while True:
        request = read_frame(client, timeout=60)
        if not request:
            continue
        with kkt_lock:                   # ждём, пока касса освободится
            KKT.reset_input_buffer()     # чужие хвосты нам не нужны
            KKT.write(request)
            reply = read_frame(KKT, REPLY_TIMEOUT)
        if reply:
            client.write(reply)          # ответ — только тому, кто спросил
        else:
            log(f"{client_name}: касса молчит {REPLY_TIMEOUT} с, очередь освобождена")


for name in cfg["clients"]["ports"].split(","):
    threading.Thread(target=serve, args=(name.strip(),), daemon=True).start()

Ключевое здесь — разбор кадра по полю длины. Арбитр не ждёт «паузу в потоке» и не гадает, где закончился ответ: протокол кассы сам говорит, сколько байт в кадре. И второе — сторожевой таймер: если касса не ответила вовремя, очередь освобождается, а не копится до бесконечности.

Настройки вынесены в файл, чтобы их мог поменять кто угодно, не трогая код:

[kkt]
port = COM4
baud = 115200
reply_timeout = 3

[clients]
ports = COM11, COM13

Ежедневный уход

Рядом живёт скрипт PowerShell, который раз в день проверяет, что арбитр работает, перезапускает его при необходимости и чистит старые журналы:

$arb = Get-CimInstance Win32_Process -Filter "Name = 'python.exe'" |
       Where-Object CommandLine -match 'arbiter\.py'

if (-not $arb) {
    Start-ScheduledTask -TaskName 'ComArbiter'
    Add-Content C:\ComArbiter\logs\care.log "$(Get-Date -f s) арбитр не работал, запущен"
}

Get-ChildItem C:\ComArbiter\logs\*.log |
  Where-Object LastWriteTime -lt (Get-Date).AddDays(-14) |
  Remove-Item

Обратите внимание: процесс ищется по командной строке, а не по имени. На одной машине может жить несколько Python-программ, и команда в духе taskkill /im python.exe положит их все. Мы на эти грабли уже наступали в другом проекте и теперь останавливаем процессы только по конкретному PID.

Синие экраны прекратились в день установки арбитра. Переход кассы на работу по сети остался в плане работ с разработчиком учётной системы: арбитр — хорошая заплатка, но всё-таки заплатка.

Бонус: куда пропала галочка «не печатать чек»

В той же клинике кассы вдруг перестали отключать печать бумажного чека, хотя пациентам достаточно электронного. Выяснилось, что это отдельная лицензия кассы («Доп. функционал Печать»), которая не продаётся отдельно, а входит только в подписку АТОЛ Connect ИТС (порядка 3 500 ₽ за кассу в год). Проверить, что у кассы уже есть, можно до оплаты: распечатать чек диагностики и посмотреть блок «Введённые лицензии». Руководству мы отправили короткую записку с расчётом пакетной закупки на все кассы сразу.

Что из этого вынести

Если касса «иногда зависает», почти наверняка за неё борются две программы. Расшаривание COM-порта кажется простым решением, но ломает сам принцип разговора с кассой. Либо переводите кассу в сеть, либо ставьте между программами того, кто будет следить за очередью. Подключение касс к 1С, лицензии и такие расследования — часть нашего сопровождения 1С.

Ещё проекты

Позвонить

Сайт использует cookie: для работы форм, отзывов с Яндекс.Карт и Яндекс.Метрики — чтобы понимать, что улучшить. Подробнее