Небольшой квест о замене кухонного принтера заказов в ресторане на табло заказов 24" монитор с raspberryPi за вечер. Это актуально практически для любой системы erp (все современные 1С системы в торговом оборудовании поддерживают чековые принтеры, аналогично и с другими системами).
Ремарка
В ресторанах и кафе для печати заказов на кухне чаще всего используют принтеры заказов (принтеры «марок»). Это небольшие термопринтеры (родственники контрольно-кассовых машин), но без фискальных накопителей, и кнопка у них чаще всего одна — промотка ленты. Раньше термопринтеры были преимущественно связаны с системами типа FrontOffice по COM порту, но около 10 лет назад ситуация изменилась, в принтерах появилась поддержка Ethernet.
Опыт
Принтеры, которые встречались в работе производителей Штрих-М, Posiflex, Sam4s, однотипны, используют для печати протокол RAW (Протокол односторонний). У них есть небольшие веб-серверы с настройками скорости печати, указания порта, кодировки, дополнительные функциональные возможности и настройки сети. Некоторые модели имеют возможность подключения сканера штрихкодов для уведомлений о готовности блюд(пересылают штрихкод в сеть). Стоимость на текущий день для бюджетных моделей начинается от 10 т.р. и может доходить до 30 т.р на Epson. Срок жизни при интенсивной эксплуатации от пары лет. Основные причины выхода из строя — поломка отрезчика бумаги, жир (покрывает принтер снаружи и частично механизмы внутри), отказ термоголовки, высыхание пластмассы роликов и шестеренок, залитие принтеров жидкостями. Ремонт и замена элементов составляет от 50% стоимости принтера, плюс, конечно же, расходный материал — термобумага.
Задача
Итак, по согласованию с кухней и администрацией взамен очередного вышедшего из строя термопринтера был смонтирован монитор с raspberry pi 3 B c sd-картой на 2 Гб.
Основная задача не вносить изменений в FrontOffice систему, и для ПО не отличаться от принтера чеков/заказов.
ПО официантов FrontOffice Штрих-М, в качестве принтера заказов указан Штрих-600. Ранее, когда менялись российские принтеры на корейские, выяснилось, что кодовая страница, в которой передаются пакеты, — это Windows-1251 порт 9100.
Выбор и настройка ОС
В качестве мини ПК будет Raspberry Pi 3 Model B, развернем а нем легковесную систему Raspbian Stretch Lite.
Проведем небольшой тюнинг: доставим в систему менеджер окон openbox, менеджер входа в систему LightDM, настроим автологин, скроем лог загрузки.
Немного анализа
Далее построим простенький сокет-сервер, чтобы узнать, как информация кодируется в пакете, и что там вообще отправляется на термопринтер.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
sock = socket.socket()
sock.bind(('', 9100))
sock.listen(1)
while True:
conn, addr = sock.accept()
data = conn.recv(16384)
print(data)
# print(((data.rsplit('*',1)[1]).rsplit('- -',9)[0]).decode('cp1251').encode('utf8'))
# clear_data = ((data.rsplit('*',1)[1]).rsplit('- -',9)[0]).decode('cp1251').encode('utf8')
conn.close()
ПО FrontOffice отправляет данные одним пакетом в котором летит пачка спец. символов перед основной частью и после неё. Справочная информация о шрифтах и их размере кодирована символами, которых нет в кодировке utf8. После каждой строки указан перенос /r/n. Можно было написать функцию, фильтрующую спец.символы, но у нас один вечер, а в «марке» очень удачно отделено начало строкой звездочек, конец строкой символов минус. Добавим костыль, отбросим спец символы в начале и конце, декодируем в utf8. В окне консоли получим чек, как он есть при печати на «марке» из принтера.
Архитектура будущего приложения
Прикинем немного архитектуру приложения.
- Сокет-сервер, постоянно ожидающий прием.
- Веб-сервер.
- Приложение просмотра — браузер с fullscreen.
- Система обмена сообщениями между сокет-сервером и веб-сервером.
Продакшн
Первый и четвертый пункт решим, дополнив выше написанный сокет-сервер — redis — хранилищем ключ-значение, с прицелом на будущую доработку( каналы — подписки), попутно снизим износ sd-карты. И добавим сигнал — уведомление о приходе нового заказа, воспроизводить будем через hdmi на колонках монитора. Вывод звука активируем через raspi-config.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
import redis
import pygame
sock = socket.socket()
sock.bind(('', 9100))
sock.listen(1)
pygame.mixer.pre_init(frequency=44100, size=-16, channels=2, buffer=4096)
pygame.mixer.init(44100, -16, 2, 4096)
sound = pygame.mixer.Sound("icq.wav")
#print(sound.get_num_channels())
r = redis.StrictRedis(host='localhost', port=6379, db=0)
n=0
while True:
conn, addr = sock.accept()
data = conn.recv(16384)
print(((data.rsplit('*',1)[1]).rsplit('- -',9)[0]).decode('cp1251').encode('utf8'))
sound.play()
clear_data = ((data.rsplit('*',1)[1]).rsplit('- -',9)[0]).decode('cp1251').encode('utf8')
r.set('data'+str(n), clear_data)
n=n+1
conn.close()
По второму пункту накидаем веб-сервер на flask с автообновление каждые 15 секунд (пока это самый простой вариант), в таск-лист пометим socketio и очередь возможно celery или на redis. Переберем все доступные пары ключ — значение и отобразим на страничке. По клику на «марке» удалим из redis и с рабочего стола соответственно.
# -*- coding: utf-8 -*-
from flask import Flask, render_template, redirect
import os
import redis
r = redis.StrictRedis(host='localhost', port=6379, db=0)
app = Flask(__name__)
def kernel_ver():
try:
f = open(os.path.dirname(os.path.abspath(__file__)) + '/release.txt')
lines = f.readlines()
f.close()
return lines[0]
except IOError as e:
return "--"
@app.route('/')
def index():
d = {}
for item in r.keys():
d[item] = (r.get(item)).decode('utf8')
return render_template("index.html", release=kernel_ver(), di = d)
@app.route('/del/<key>')
def delstamp(key):
r.delete(key)
return redirect("http://192.168.1.80:5000/", code=302)
if __name__ == "__main__":
app.run(host='0.0.0.0')
Добавим jinja шаблон
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="refresh" content="30"/>
<title> Монитор заказов</title>
<link href="http://fonts.googleapis.com/css?family=Reenie+Beanie:regular" rel="stylesheet" type="text/css">
</head>
<body>
<ul>
{% for key in di %}
<li>
<a href="/del/{{key}}">
<!-- h2>Title #1</h2 -->
{% for item in di[key].splitlines() %}
<p>{{ item }}</p>
{% endfor %}
</a>
</li>
{% endfor %}
</ul>
</body>
</html>
Остался пункт 3, сделаем самый минимальный браузер без кнопок из 13 строк.
import sys
from PySide import QtCore, QtGui, QtWebKit
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.showFullScreen()
self.web = QtWebKit.QWebView(self)
self.web.load(QtCore.QUrl('https://127.0.0.1:5000'))
self.setCentralWidget(self.web)
app = QtGui.QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
Далее необходимо создать сервисы для запуска всех выше написанных скриптов.
Или по-быстрому их прописать в autostart файл openbox.
Автор: DarkDemon