Автоматизация
Касса, две программы и 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С.
Ещё проекты
Похожие объекты
Автоматизация
Две кассовые программы и один COM-порт: как мы остановили синий экран
В медицинском центре касса АТОЛ подключена к компьютеру по USB и видна системе как COM-порт. С этой кассой должны работать две программы: учётная система и 1С.
Автоматизация
Почему маркетплейс не принимает ваш УПД
Наш клиент — разборка автомобилей. Продаёт запчасти, в том числе через крупный маркетплейс автозапчастей.