python

Монитор заказов вместо кухонного термопринтера

  • суббота, 4 августа 2018 г. в 00:22:06
https://habr.com/post/418961/
  • Разработка под Linux
  • Разработка на Raspberry Pi
  • Периферия
  • Python


Монитор заказов

Небольшой квест о замене кухонного принтера заказов в ресторане на табло заказов 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. В окне консоли получим чек, как он есть при печати на «марке» из принтера.

Архитектура будущего приложения


Прикинем немного архитектуру приложения.

  1. Сокет-сервер, постоянно ожидающий прием.
  2. Веб-сервер.
  3. Приложение просмотра — браузер с fullscreen.
  4. Система обмена сообщениями между сокет-сервером и веб-сервером.

Продакшн


Первый и четвертый пункт решим, дополнив выше написанный сокет-сервер — 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('http://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.

Результат
Кухонный монитор заказов