Estrella: сервер для чеков на термопринтере Rust
Пошаговое руководство по установке и настройке Estrella — self-hosted сервера на Rust для управления термопринтером чеков.
Установка Rust
Установите Rust с помощью rustup, если он не установлен. После установки перезапустите терминал и проверьте версию Rust и Cargo.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
rustc --version
cargo --versionКлонирование репозитория Estrella
Создайте рабочую директорию и клонируйте репозиторий проекта с GitHub.
git clone https://github.com/example/estrella.git
cd estrellaУстановка зависимостей
Установите необходимые системные зависимости для вашей операционной системы. Для Ubuntu/Debian это build-essential, libudev-dev, libssl-dev и pkg-config.
sudo apt-get update
sudo apt-get install build-essential libudev-dev libssl-dev pkg-configСборка проекта
Соберите проект с помощью Cargo в release-режиме для оптимальной производительности. Бинарный файл будет находиться в target/release/estrella.
cargo build --release
./target/release/estrella --helpСоздание конфигурационного файла
Создайте файл config.toml с настройками сервера, аутентификации, принтера, логирования и очереди печати.
[server]
host = "0.0.0.0"
port = 8080
workers = 4
[printer]
type = "usb"
path = "/dev/usb/lp0"
width = 48
encoding = "cp866"
model = "epson"
autocut = true
[auth]
enabled = true
method = "bearer"
api_key = "your-secret-api-key"Настройка прав доступа к USB-принтеру
Для Linux добавьте пользователя в группы lp и dialout для доступа к USB-устройству принтера.
sudo usermod -a -G lp,dialout $USER
ls -l /dev/usb/lp*Запуск сервера
Запустите сервер с указанием конфигурационного файла. По умолчанию сервер будет доступен на порту 8080.
./target/release/estrella --config config.tomlПроверка работы сервера
Проверьте статус принтера и отправьте тестовый запрос на печать через API.
curl -X GET http://localhost:8080/status
curl -X POST http://localhost:8080/print \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{"text": "Тестовый чек", "cut": true}'Настройка systemd сервиса (опционально)
Для автоматического запуска сервера создайте systemd unit файл и включите сервис.
[Unit]
Description=Estrella Thermal Printer Server
After=network.target
[Service]
Type=simple
User=estrella
WorkingDirectory=/opt/estrella
ExecStart=/opt/estrella/target/release/estrella --config /etc/estrella/config.toml
Restart=always
[Install]
WantedBy=multi-user.targetЗапуск с Docker (опционально)
Альтернативно можно запустить сервер в Docker контейнере с пробросом USB-устройства и конфигурации.
docker run -d \
--name estrella \
-p 8080:8080 \
--device=/dev/usb/lp0 \
-v /path/to/config.toml:/etc/estrella/config.toml \
ghcr.io/example/estrella:latestRust-сервер Estrella для термопринтера чеков: полное руководство по установке и настройке
Введение
Estrella — это высокопроизводительный сервер, написанный на Rust, предназначенный для управления термопринтерами чеков. Он обеспечивает быструю и надежную печать чеков с поддержкой различных протоколов подключения. В этом руководстве мы рассмотрим процесс установки, настройки, использования и интеграции сервера Estrella с различными системами.
Этот сервер особенно востребован в торговых точках, ресторанах и других бизнесах, где требуется быстрая и надежная печать чеков. Благодаря высокому уровню абстракции, Estrella поддерживает множество моделей принтеров и предоставляет удобный API для интеграции с POS-системами. Использование Rust в качестве языка разработки обеспечивает безопасность памяти, производительность и надежность, что критически важно для систем, работающих с оборудованием в реальном времени.
Требования системы
Перед установкой убедитесь, что ваша система соответствует следующим требованиям:
- Операционная система: Linux (Ubuntu 18.04+), macOS 10.14+ или Windows 10+
- Rust 1.65 или выше
- Термопринтер, поддерживаемый через ESC/POS, CUPS или сетевой интерфейс
- Поддерживаемые модели: Epson TM-T20II, Star TSP100, CITIZEN CT-S651, ZJ-58, BIXOLON SRP-350plus, Honeywell V600, Godex EZ-3200, NCR RealPOS 80
- Для принтеров с USB-интерфейсом
- Для сетевых принтеров (Ethernet, Wi-Fi)
- USB-порт (для локального подключения) или доступная сеть (для сетевых принтеров)
- Для Linux: права доступа к устройствам (/dev/usb/lp*)
- Для Windows: доступ к принтеру через API (установлены драйверы принтера)
- Для macOS: доступ через CUPS (Common Unix Printing System)
Установка
Шаг 1: Установка Rust
Если у вас не установлен Rust, установите его с помощью rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
После установки перезапустите терминал или выполните:
source $HOME/.cargo/env
Проверьте версию Rust:
rustc --version
Убедитесь, что установлен Cargo - менеджер пакетов Rust:
cargo --version
Шаг 2: Клонирование репозитория Estrella
Создайте рабочую директорию и клонируйте репозиторий:
git clone https://github.com/example/estrella.git
cd estrella
Шаг 3: Установка зависимостей
Установите необходимые зависимости для вашей операционной системы:
Для Ubuntu/Debian:
sudo apt-get update
sudo apt-get install build-essential libudev-dev libssl-dev pkg-config
Для macOS:
brew install pkg-config openssl
Для Windows: Установите Visual Studio Build Tools и OpenSSL.
Шаг 4: Сборка проекта
Соберите проект с помощью Cargo:
cargo build --release
После сборки бинарный файл будет находиться в target/release/estrella.
Шаг 5: Проверка установки
Проверьте, что бинарный файл был создан корректно:
./target/release/estrella --help
Если вы видите справку по командам, установка прошла успешно.
Внутренние механизмы работы Estrella
Estrella построен на основе асинхронного рантайма Tokio, что обеспечивает высокую производительность и эффективное использование системных ресурсов. Сервер использует неблокирующие I/O операции для обработки множества одновременных подключений.
Архитектура сервера
- HTTP-сервер: На базе библиотеки Hyper, обеспечивает обработку входящих HTTP-запросов с поддержкой HTTP/2.
- Принтерный драйвер: Абстрактный слой, поддерживающий различные типы принтеров (USB, сеть, COM-порт) с поддержкой горячей замены.
- Кэширование: Внутренний кэш для часто используемых конфигураций и шаблонов на базе библиотеки DashMap.
- Обработка ошибок: Система обработки ошибок с детальным логированием и автоматическим восстановлением после сбоев.
- Очередь задач: Неблокирующая очередь для обработки запросов печати, обеспечивающая доставку сообщений даже при временной недоступности принтера.
- Пул соединений: Пул для эффективного управления соединениями с сетевыми принтерами.
Поддерживаемые протоколы
- ESC/POS: Протокол Epson для управления термопринтерами, включая расширенные команды.
- CPCL: Протокол Citizen для термопринтеров с поддержкой графики.
- StarPRNT: Протокол Star Micronics с поддержкой современных функций.
- ZPL: Протокол Zebra для принтеров этикеток.
- TSPL: Протокол TSC для этикеточных принтеров.
- EPL2: Протокол Zebra для старых моделей принтеров.
Потоки обработки запросов
- Приходит HTTP-запрос на печать чека.
- Сервер валидирует запрос и проверяет аутентификацию.
- Запрос помещается в очередь обработки с приоритизацией.
- Модуль форматирования преобразует данные в команды принтера.
- Пул драйверов выбирает доступный принтер для обработки запроса.
- Команда отправляется на принтер через соответствующий драйвер.
- Сервер ожидает ответа от принтера с настраиваемым таймаутом.
- После успешной обработки ответ формируется клиенту.
Производительность и оптимизация
Estrella спроектирован для высокой производительности:
- Асинхронная обработка запросов с минимальными задержками.
- Эффективное управление памятью за счет системы владения Rust.
- Оптимизированный парсинг данных для минимального потребления CPU.
- Кэширование шаблонов чеков для повторяющихся сценариев.
- Пул соединений с сетевыми принтерами для снижения накладных расходов.
Бенчмарки показывают, что Estrella способен обрабатывать до 200 запросов в секунду на стандартном железе с задержкой менее 50 мс.
Настройка
Шаг 1: Конфигурационный файл
Создайте конфигурационный файл config.toml в директории проекта:
[server]
host = "0.0.0.0"
port = 8080
workers = 4 # количество worker-потоков
timeout = 30 # таймаут в секундах
keep_alive = 75 # время поддержания соединения в секундах
max_connections = 1000 # максимальное количество одновременных соединений
[auth]
enabled = true
method = "bearer" # bearer, basic, api_key
api_key = "your-secret-api-key"
token_expiry = 3600 # срок действия токена в секундах
refresh_token = true # включить рефреш токены
[security]
enable_cors = true
allowed_origins = ["http://localhost:3000", "https://yourdomain.com"]
max_request_size = "10 MB"
enable_rate_limit = true
rate_limit = 1000 # запросов в минуту на один IP
ssl = false
ssl_cert = "/path/to/cert.pem"
ssl_key = "/path/to/key.pem"
ip_whitelist = ["192.168.1.0/24", "10.0.0.0/8"]
[printer]
type = "usb" # или "network", "serial", "bluetooth"
path = "/dev/usb/lp0" # для USB или сетевой адрес
width = 48 # ширина чека в символах
encoding = "cp866" # кодировка
model = "epson" # epson, star, citizen, zebra, bixolon, honeywell, godex
density = 15 # плотность печати (0-15)
speed = 4 # скорость печати (1-5)
thermal_mode = true # термический режим
cut_after = 3 # количество чеков после которых отрезать
paper_sensor = true # использование датчика бумаги
autocut = true # автоматическая обрезка
[printer.barcode]
type = "CODE128" # CODE128, EAN13, UPC-A, QR-Code, DataMatrix, PDF417
width = 2 # ширина штрих-кода
height = 100 # высота штрих-кода
text_position = "below" # below, above, none
barcode_width = 2
barcode_height = 100
[printer.qr]
size = 10 # размер QR-кода (1-16)
error_correction = "M" # L, M, Q, H
margin = 4 # отступы в точках
model = "2" # модель QR-кода (1-2)
[printer.image]
max_width = 512 # максимальная ширина изображения в пикселях
max_height = 1024 # максимальная высота изображения в пикселях
dithering = true # дизеринг для черно-белых изображений
[logging]
level = "info" # trace, debug, info, warn, error
file = "estrella.log"
max_size = "100 MB"
backups = 5
enable_json = false # форматировать логи в JSON
enable_syslog = false # отправлять логи в syslog
[metrics]
enabled = true
path = "/metrics" # эндпоинт для метрик Prometheus
interval = 30 # интервал сбора метрик в секундах
[cache]
enabled = true
max_size = 1000 # максимальное количество элементов в кэше
ttl = 3600 # время жизни кэша в секундах
[queue]
enabled = true
max_size = 1000 # максимальное количество элементов в очереди
retry_attempts = 3 # количество попыток повторной отправки
retry_delay = 5 # задержка между попытками в секундах
persistent = true # сохранять очередь между перезапусками (requires SQLite)
[persistent_queue]
path = "/var/lib/estrella/queue.db"
max_size = "1 GB" # максимальный размер базы данных
checkpoint_interval = 100 # количество операций между точками сохранения
Шаг 2: Настройка аутентификации
Для включения аутентификации установите [auth].enabled = true в конфигурационном файле. Estrella поддерживает три метода аутентификации:
- Bearer токен: Включите
method = "bearer"и укажитеapi_key. - HTTP Basic: Включите
method = "basic"и укажите логин и пароль. - API ключ: Включите
method = "api_key"и укажитеapi_key.
Пример запроса с аутентификацией:
curl -X POST http://localhost:8080/print \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"text": "Пример текста для печати"
}'
Для получения токена используйте эндпоинт /auth:
curl -X POST http://localhost:8080/auth \
-H "Content-Type: application/json" \
-d '{
"api_key": "your-secret-api-key"
}'
Шаг 3: Настройка безопасности
Шифрование соединений
Для настройки HTTPS:
[server]
port = 8443
ssl = true
ssl_cert = "/path/to/fullchain.pem"
ssl_key = "/path/to/privkey.pem
Защита от атак
Estrella включает встроенные механизмы защиты:
- Защита от DDoS: Ограничение количества запросов с одного IP-адреса.
- Защита от брутфорса: Ограничение попыток аутентификации.
- CORS: Ограничение доменов, с которых разрешены запросы.
- Ограничение размера запросов: Предотвращение атак типа "загрузка данных".
Настройка IP-белого списка
[security]
ip_whitelist = ["192.168.1.0/24", "10.0.0.0/8", "127.0.0.1"]
ip_blacklist = ["192.168.2.100"] # конкретные IP-адреса, которые будут заблокированы
Настройка CORS
[security]
enable_cors = true
allowed_origins = ["http://localhost:3000", "https://yourdomain.com"]
allowed_methods = ["GET", "POST", "PUT", "DELETE"]
allowed_headers = ["Content-Type", "Authorization"]
Шаг 4: Настройка принтера
Для USB-принтера:
Убедитесь, что ваш пользователь имеет доступ к USB-устройству:
sudo usermod -a -G lp $USER
sudo usermod -a -G dialout $USER
Перезагрузите систему или выйдите и войдите снова, чтобы изменения вступили в силу.
Для определения правильного пути к принтеру используйте команду:
ls -l /dev/usb/lp*
Для принтеров, которые определяются как serial устройства:
ls -l /dev/ttyUSB*
ls -l /dev/ttyACM*
Для сетевого принтера:
Укажите IP-адрес принтера в параметре path конфигурационного файла:
[printer]
type = "network"
path = "192.168.1.100:9100"
Для сетевых принтеров также можно указать таймаут подключения:
[printer.network]
timeout = 10 # таймаут в секундах
retries = 3 # количество попыток подключения
connection_pool_size = 5 # размер пула соединений
Для принтера через порт COM (Serial):
[printer]
type = "serial"
path = "/dev/ttyS0"
baud_rate = 9600
data_bits = 8
parity = "none"
stop_bits = 1
flow_control = "none"
Для Bluetooth-принтера:
[printer]
type = "bluetooth"
mac_address = "00:11:22:33:44:55"
channel = 1
timeout = 30
Шаг 5: Настройка мониторинга
Настройка метрик
Для сбора метрик Prometheus:
[metrics]
enabled = true
path = "/metrics"
interval = 15
include_labels = ["printer", "request_type"]
Настройка логирования
Для продвинутого логирования:
[logging]
level = "info"
file = "/var/log/estrella/estrella.log"
max_size = "100 MB"
backups = 5
enable_json = true
enable_syslog = true
syslog_facility = "local0"
Использование сервера
Запуск сервера
Запустите сервер с помощью следующей команды:
./target/release/estrella --config config.toml
Для запуска в режиме разработки с перезагрузкой при изменениях:
cargo watch -x "run -- --config config.toml"
По умолчанию сервер будет доступен на порту 8080. Для запуска в качестве systemd сервиса создайте файл /etc/systemd/system/estrella.service:
[Unit]
Description=Estrella Thermal Printer Server
After=network.target
[Service]
Type=simple
User=estrella
WorkingDirectory=/opt/estrella
ExecStart=/opt/estrella/target/release/estrella --config /etc/estrella/config.toml
Restart=always
RestartSec=5
Environment=RUST_LOG=info
[Install]
WantedBy=multi-user.target
Затем включите и запустите сервис:
sudo systemctl daemon-reload
sudo systemctl enable estrella
sudo systemctl start estrella
Для запуска с Docker:
docker run -d \
--name estrella \
-p 8080:8080 \
-v /path/to/config.toml:/etc/estrella/config.toml \
-v /var/log/estrella:/var/log/estrella \
ghcr.io/example/estrella:latest
Мониторинг и логирование
Estrella предоставляет подробное логирование и метрики для мониторинга работы сервера.
Логи сохраняются в указанный в конфигурационном файле. Для просмотра логов в реальном времени используйте:
tail -f estrella.log
Для просмотра системных логов:
journalctl -u estrella -f
Для мониторинга метрик используйте эндпоинт /metrics (если включен в конфигурации):
curl http://localhost:8080/metrics
API-интерфейс
Estrella предоставляет REST API для управления печатью. Все эндпоинты защищены аутентификацией, если она включена в настройках.
Аутентификация
curl -X POST http://localhost:8080/auth \
-H "Content-Type: application/json" \
-d '{
"api_key": "your-secret-api-key"
}'
Ответ:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 3600
}
Печать чека
curl -X POST http://localhost:8080/print \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-access-token" \
-d '{
"text": "Пример текста для печати",
"cut": true,
"beep": true,
"barcode": "123456789012",
"qr_code": "https://example.com",
"encoding": "cp866",
"width": 48,
"align": "left"
}'
Печать изображения
curl -X POST http://localhost:8080/print-image \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-access-token" \
-d '{
"image_base64": "iVBORw0KGgoAAAANSUhEUgAA...",
"width": 300,
"alignment": "center",
"dithering": true
}'
Печать таблицы
curl -X POST http://localhost:8080/print-table \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-access-token" \
-d '{
"headers": ["Товар", "Кол-во", "Цена"],
"rows": [
["Товар 1", "2", "100.00"],
["Товар 2", "1", "200.00"]
],
"alignment": ["left", "center", "right"],
"borders": true,
"spacing": 1
}'
Получение статуса принтера
curl -X GET http://localhost:8080/status \
-H "Authorization: Bearer your-access-token"
Ответ:
{
"status": "ready",
"temperature": 42.5,
"paper_status": "present",
"error_code": 0,
"model": "epson-tm-t20ii",
"serial_number": "12345678"
}
Отправка команды на принтер
curl -X POST http://localhost:8080/command \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-access-token" \
-d '{
"command": "\\x1B\\x40", # ESC @ - сброс принтера
"raw": true
}'
Получение списка поддерживаемых принтеров
curl -X GET http://localhost:8080/printers \
-H "Authorization: Bearer your-access-token"
Управление очередью печати
# Получение статуса очереди
curl -X GET http://localhost:8080/queue/status \
-H "Authorization: Bearer your-access-token"
# Очистка очереди
curl -X POST http://localhost:8080/queue/clear \
-H "Authorization: Bearer your-access-token"
# Приостановка обработки очереди
curl -X POST http://localhost:8080/queue/pause \
-H "Authorization: Bearer your-access-token"
# Возобновление обработки очереди
curl -X POST http://localhost:8080/queue/resume \
-H "Authorization: Bearer your-access-token"
Интеграция с POS-системами
Обзор интеграции
Estrella предоставляет гибкие возможности для интеграции с различными POS-системами. В этом разделе рассмотрим примеры интеграции на разных языках программирования.
Интеграция с Python
Установите необходимые библиотеки:
pip install requests aiohttp
Пример интеграции:
import requests
import json
import asyncio
import aiohttp
class EstrellaClient:
def __init__(self, base_url, api_key):
self.base_url = base_url
self.api_key = api_key
self.access_token = None
self.token_expiry = 0
def get_headers(self):
if self.access_token:
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.access_token}"
}
return {"Content-Type": "application/json"}
def authenticate(self):
"""Получение токена аутентификации"""
url = f"{self.base_url}/auth"
payload = {"api_key": self.api_key}
response = requests.post(url, json=payload)
response.raise_for_status()
data = response.json()
self.access_token = data["access_token"]
self.token_expiry = data["expires_in"]
return self.access_token
def ensure_authenticated(self):
"""Проверка и обновление токена при необходимости"""
current_time = time.time()
if not self.access_token or current_time > self.token_expiry:
self.authenticate()
def print_receipt(self, text, cut=True, beep=True, barcode=None, qr_code=None, **kwargs):
"""Печать чека"""
self.ensure_authenticated()
url = f"{self.base_url}/print"
payload = {
"text": text,
"cut": cut,
"beep": beep,
**kwargs
}
if barcode:
payload["barcode"] = barcode
if qr_code:
payload["qr_code"] = qr_code
response = requests.post(url, headers=self.get_headers(), json=payload)
response.raise_for_status()
return response.json()
def print_image(self, image_path=None, image_base64=None, **kwargs):
"""Печать изображения"""
self.ensure_authenticated()
url = f"{self.base_url}/print-image"
payload = kwargs
if image_path:
with open(image_path, 'rb') as f:
image_data = f.read()
payload["image_base64"] = base64.b64encode(image_data).decode('utf-8')
elif image_base64:
payload["image_base64"] = image_base64
response = requests.post(url, headers=self.get_headers(), json=payload)
response.raise_for_status()
return response.json()
def print_table(self, headers, rows, **kwargs):
"""Печать таблицы"""
self.ensure_authenticated()
url = f"{self.base_url}/print-table"
payload = {
"headers": headers,
"rows": rows,
**kwargs
}
response = requests.post(url, headers=self.get_headers(), json=payload)
response.raise_for_status()
return response.json()
def get_status(self):
"""Получение статуса принтера"""
self.ensure_authenticated()
url = f"{self.base_url}/status"
response = requests.get(url, headers=self.get_headers())
response.raise_for_status()
return response.json()
# Асинхронный клиент
class AsyncEstrellaClient:
def __init__(self, base_url, api_key):
self.base_url = base_url
self.api_key = api_key
self.access_token = None
self.token_expiry = 0
self.session = None
async def __aenter__(self):
self.session = aiohttp.ClientSession()
return self
async def __aexit__(self, exc_type, exc, tb):
if self.session:
await self.session.close()
async def authenticate(self):
"""Получение токена аутентификации"""
url = f"{self.base_url}/auth"
payload = {"api_key": self.api_key}
async with self.session.post(url, json=payload) as response:
response.raise_for_status()
data = await response.json()
self.access_token = data["access_token"]
self.token_expiry = data["expires_in"]
return self.access_token
async def ensure_authenticated(self):
"""Проверка и обновление токена при необходимости"""
current_time = time.time()
if not self.access_token or current_time > self.token_expiry:
await self.authenticate()
async def print_receipt(self, text, cut=True, beep=True, barcode=None, qr_code=None, **kwargs):
"""Асинхронная печать чека"""
await self.ensure_authenticated()
url = f"{self.base_url}/print"
payload = {
"text": text,
"cut": cut,
"beep": beep,
**kwargs
}
if barcode:
payload["barcode"] = barcode
if qr_code:
payload["qr_code"] = qr_code
async with self.session.post(url, headers=self.get_headers(), json=payload) as response:
response.raise_for_status()
return await response.json()
def get_headers(self):
if self.access_token:
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.access_token}"
}
return {"Content-Type": "application/json"}
# Пример использования
client = EstrellaClient("http://localhost:8080", "your-api-key")
receipt_text = """
КАССОВЫЙ ЧЕК
=====================
Товар 1: 100.00 руб.
Товар 2: 200.00 руб.
---------------------
ИТОГО: 300.00 руб.
=====================
Спасибо за покупку!
"""
client.print_receipt(
text=receipt_text,
barcode="123456789012",
qr_code="https://example.com/receipt/123"
)
# Получение статуса принтера
status = client.get_status()
print("Статус принтера:", status)
# Асинхронное использование
async def async_example():
async with AsyncEstrellaClient("http://localhost:8080", "your-api-key") as client:
await client.print_receipt(
text="Асинхронный чек",
cut=True
)
asyncio.run(async_example())
Интеграция с Node.js
Установите необходимые пакеты:
npm install axios axios-retry
Пример интеграции:
const axios = require('axios');
const axiosRetry = require('axios-retry');
class EstrellaClient {
constructor(baseUrl, apiKey) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
this.accessToken = null;
this.tokenExpiry = 0;
const axiosInstance = axios.create();
axiosRetry(axiosInstance, {
retries: 3,
retryDelay: (retryCount) => {
console.log(`Попытка ${retryCount}`);
return 1000;
}
});
this.axios = axiosInstance;
}
get headers() {
return {
'Content-Type': 'application/json',
...(this.accessToken && { 'Authorization': `Bearer ${this.accessToken}` })
};
}
async authenticate() {
const response = await this.axios.post(`${this.baseUrl}/auth`, {
api_key: this.apiKey
});
const { access_token, expires_in } = response.data;
this.accessToken = access_token;
this.tokenExpiry = Date.now() + (expires_in * 1000);
return access_token;
}
async ensureAuthenticated() {
if (!this.accessToken || Date.now() > this.tokenExpiry) {
await this.authenticate();
}
}
async printReceipt(text, options = {}) {
await this.ensureAuthenticated();
const payload = {
text,
cut: true,
beep: true,
...options
};
try {
const response = await this.axios.post(`${this.baseUrl}/print`, payload, {
headers: this.headers
});
return response.data;
} catch (error) {
console.error('Ошибка печати:', error.response?.data || error.message);
throw error;
}
}
async printImage(imageBase64, options = {}) {
await this.ensureAuthenticated();
const payload = {
image_base64: imageBase64,
...options
};
try {
const response = await this.axios.post(`${this.baseUrl}/print-image`, payload, {
headers: this.headers
});
return response.data;
} catch (error) {
console.error('Ошибка печати изображения:', error.response?.data || error.message);
throw error;
}
}
async getStatus() {
await this.ensureAuthenticated();
try {
const response = await this.axios.get(`${this.baseUrl}/status`, {
headers: this.headers
});
return response.data;
} catch (error) {
console.error('Ошибка получения статуса:', error.response?.data || error.message);
throw error;
}
}
async addToQueue(data, options = {}) {
await this.ensureAuthenticated();
try {
const response = await this.axios.post(`${this.baseUrl}/queue/add`, {
data,
...options
}, {
headers: this.headers
});
return response.data;
} catch (error) {
console.error('Ошибка добавления в очередь:', error.response?.data || error.message);
throw error;
}
}
}
// Пример использования
const client = new EstrellaClient('http://localhost:8080', 'your-api-key');
const receiptText = `
КАССОВЫЙ ЧЕК
=====================
Товар 1: 100.00 руб.
Товар 2: 200.00 руб.
---------------------
ИТОГО: 300.00 руб.
=====================
Спасибо за покупку!
`;
client.printReceipt(receiptText, {
barcode: '123456789012',
qr_code: 'https://example.com/receipt/123'
})
.then(() => console.log('Чек успешно отправлен на печать'))
.catch(err => console.error('Ошибка:', err));
// Получение статуса принтера
client.getStatus()
.then(status => console.log('Статус принтера:', status))
.catch(err => console.error('Ошибка:', err));
// Пример с очередью
async function printWithQueue() {
try {
const queueId = await client.addToQueue({
type: 'receipt',
text: 'Чек из очереди'
});
console.log('Задача добавлена в очередь с ID:', queueId);
// Проверка статуса задачи
setTimeout(() => {
client.getStatus()
.then(status => {
console.log('Статус принтера после добавления в очередь:', status);
});
}, 5000);
} catch (error) {
console.error('Ошибка:', error);
}
}
printWithQueue();
Интеграция с Java
Сначала добавьте зависимость в ваш проект (Maven):
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.1</version>
</dependency>
Пример интеграции:
import okhttp3.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
public class EstrellaClient {
private final OkHttpClient client;
private final String baseUrl;
private final String apiKey;
private String accessToken;
private long tokenExpiry;
private final ObjectMapper objectMapper = new ObjectMapper();
public EstrellaClient(String baseUrl, String apiKey) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
this.client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(new RetryInterceptor(3))
.build();
}
public void authenticate() throws IOException {
RequestBody body = RequestBody.create(
MediaType.parse("application/json"),
"{\"api_key\":\"" + apiKey + "\"}"
);
Request request = new Request.Builder()
.url(baseUrl + "/auth")
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response + ", body: " + response.body().string());
}
String json = response.body().string();
AuthResponse authResponse = objectMapper.readValue(json, AuthResponse.class);
this.accessToken = authResponse.getAccessToken();
this.tokenExpiry = System.currentTimeMillis() + (authResponse.getExpiresIn() * 1000L);
}
}
public void ensureAuthenticated() throws IOException {
if (accessToken == null || System.currentTimeMillis() > tokenExpiry) {
authenticate();
}
}
public PrintResponse printReceipt(String text, PrintOptions options) throws IOException {
ensureAuthenticated();
PrintRequest request = new PrintRequest();
request.setText(text);
request.setCut(options.isCut());
request.setBeep(options.isBeep());
request.setBarcode(options.getBarcode());
request.setQrCode(options.getQrCode());
request.setWidth(options.getWidth());
request.setEncoding(options.getEncoding());
request.setAlignment(options.getAlignment());
RequestBody body = RequestBody.create(
objectMapper.writeValueAsString(request),
MediaType.parse("application/json")
);
Request httpRequest = new Request.Builder()
.url(baseUrl + "/print")
.header("Authorization", "Bearer " + accessToken)
.post(body)
.build();
try (Response response = client.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response + ", body: " + response.body().string());
}
String json = response.body().string();
return objectMapper.readValue(json, PrintResponse.class);
}
}
public PrintResponse printImage(String imagePath, PrintImageOptions options) throws IOException {
ensureAuthenticated();
byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
PrintImageRequest request = new PrintImageRequest();
request.setImageBase64(base64Image);
request.setWidth(options.getWidth());
request.setAlignment(options.getAlignment());
request.setDithering(options.isDithering());
RequestBody body = RequestBody.create(
objectMapper.writeValueAsString(request),
MediaType.parse("application/json")
);
Request httpRequest = new Request.Builder()
.url(baseUrl + "/print-image")
.header("Authorization", "Bearer " + accessToken)
.post(body)
.build();
try (Response response = client.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response + ", body: " + response.body().string());
}
String json = response.body().string();
return objectMapper.readValue(json, PrintResponse.class);
}
}
public StatusResponse getStatus() throws IOException {
ensureAuthenticated();
Request request = new Request.Builder()
.url(baseUrl + "/status")
.header("Authorization", "Bearer " + accessToken)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response + ", body: " + response.body().string());
}
String json = response.body().string();
return objectMapper.readValue(json, StatusResponse.class);
}
}
// Классы для сериализации/десериализации
private static class AuthResponse {
private String accessToken;
private int expiresIn;
public String getAccessToken() { return accessToken; }
public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
public int getExpiresIn() { return expiresIn; }
public void setExpiresIn(int expiresIn) { this.expiresIn = expiresIn; }
}
private static class PrintRequest {
private String text;
private boolean cut;
private boolean beep;
private String barcode;
private String qrCode;
private int width = 48;
private String encoding = "cp866";
private String alignment = "left";
// Геттеры и сеттеры
public String getText() { return text; }
public void setText(String text) { this.text = text; }
public boolean isCut() { return cut; }
public void setCut(boolean cut) { this.cut = cut; }
public boolean isBeep() { return beep; }
public void setBeep(boolean beep) { this.beep = beep; }
public String getBarcode() { return barcode; }
public void setBarcode(String barcode) { this.barcode = barcode; }
public String getQrCode() { return qrCode; }
public void setQrCode(String qrCode) { this.qrCode = qrCode; }
public int getWidth() { return width; }
public void setWidth(int width) { this.width = width; }
public String getEncoding() { return encoding; }
public void setEncoding(String encoding) { this.encoding = encoding; }
public String getAlignment() { return alignment; }
public void setAlignment(String alignment) { this.alignment = alignment; }
}
private static class PrintOptions {
private boolean cut = true;
private boolean beep = true;
private String barcode;
private String qrCode;
private int width = 48;
private String encoding = "cp866";
private String alignment = "left";
// Геттеры и сеттеры
public boolean isCut() { return cut; }
public void setCut(boolean cut) { this.cut = cut; }
public boolean isBeep() { return beep; }
public void setBeep(boolean beep) { this.beep = beep; }
public String getBarcode() { return barcode; }
public void setBarcode(String barcode) { this.barcode = barcode; }
public String getQrCode() { return qrCode; }
public void setQrCode(String qrCode) { this.qrCode = qrCode; }
public int getWidth() { return width; }
public void setWidth(int width) { this.width = width; }
public String getEncoding() { return encoding; }
public void setEncoding(String encoding) { this.encoding = encoding; }
public String getAlignment() { return alignment; }
public void setAlignment(String alignment) { this.alignment = alignment; }
}
private static class PrintImageRequest {
private String imageBase64;
private Integer width;
private String alignment = "center";
private boolean dithering = true;
// Геттеры и сеттеры
public String getImageBase64() { return imageBase64; }
public void setImageBase64(String imageBase64) { this.imageBase64 = imageBase64; }
public Integer getWidth() { return width; }
public void setWidth(Integer width) { this.width = width; }
public String getAlignment() { return alignment; }
public void setAlignment(String alignment) { this.alignment = alignment; }
public boolean isDithering() { return dithering; }
public void setDithering(boolean dithering) { this.dithering = dithering; }
}
private static class PrintImageOptions {
private Integer width;
private String alignment = "center";
private boolean dithering = true;
// Геттеры и сеттеры
public Integer getWidth() { return width; }
public void setWidth(Integer width) { this.width = width; }
public String getAlignment() { return alignment; }
public void setAlignment(String alignment) { this.alignment = alignment; }
public boolean isDithering() { return dithering; }
public void setDithering(boolean dithering) { this.dithering = dithering; }
}
private static class PrintResponse {
private String status;
private String message;
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
}
private static class StatusResponse {
private String status;
private double temperature;
private String paperStatus;
private int errorCode;
private String model;
private String serialNumber;
// Геттеры и сеттеры
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public double getTemperature() { return temperature; }
public void setTemperature(double temperature) { this.temperature = temperature; }
public String getPaperStatus() { return paperStatus; }
public void setPaperStatus(String paperStatus) { this.paperStatus = paperStatus; }
public int getErrorCode() { return errorCode; }
public void setErrorCode(int errorCode) { this.errorCode = errorCode; }
public String getModel() { return model; }
public void setModel(String model) { this.model = model; }
public String getSerialNumber() { return serialNumber; }
public void setSerialNumber(String serialNumber) { this.serialNumber = serialNumber; }
}
// Интерцептор для повторных попыток
private static class RetryInterceptor implements Interceptor {
private final int maxRetries;
public RetryInterceptor(int maxRetries) {
this.maxRetries = maxRetries;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
int retryCount = 0;
while (true) {
try {
return chain.proceed(request);
} catch (IOException e) {
retryCount++;
if (retryCount > maxRetries) {
throw e;
}
}
}
}
}
// Пример использования
public static void main(String[] args) {
EstrellaClient client = new EstrellaClient("http://localhost:8080", "your-api-key");
String receiptText = "КАССОВЫЙ ЧЕК\n=====================\nТовар 1: 100.00 руб.\nТовар 2: 200.00 руб.\n---------------------\nИТОГО: 300.00 руб.\n=====================\nСпасибо за покупку!";
try {
PrintOptions options = new PrintOptions();
options.setBarcode("123456789012");
options.setQrCode("https://example.com/receipt/123");
PrintResponse response = client.printReceipt(receiptText, options);
System.out.println("Ответ сервера: " + response.getStatus());
StatusResponse status = client.getStatus();
System.out.println("Статус принтера: " + status.getStatus());
System.out.println("Температура: " + status.getTemperature() + "°C");
System.out.println("Статус бумаги: " + status.getPaperStatus());
} catch (IOException e) {
e.printStackTrace();
}
}
}
Интеграция с Go
Установите необходимые пакеты:
go get github.com/go-resty/resty/v2
Пример интеграции:
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"time"
"github.com/go-resty/resty/v2"
)
type EstrellaClient struct {
baseURL string
apiKey string
accessToken string
tokenExpiry time.Time
client *resty.Client
}
type AuthRequest struct {
APIKey string `json:"api_key"`
}
type AuthResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
type PrintRequest struct {
Text string `json:"text"`
Cut bool `json:"cut"`
Beep bool `json:"beep"`
Barcode string `json:"barcode,omitempty"`
QRCode string `json:"qr_code,omitempty"`
Width int `json:"width"`
Encoding string `json:"encoding"`
Align string `json:"align"`
}
type PrintImageRequest struct {
ImageBase64 string `json:"image_base64"`
Width int `json:"width"`
Align string `json:"alignment"`
Dithering bool `json:"dithering"`
}
type StatusResponse struct {
Status string `json:"status"`
Temperature float64 `json:"temperature"`
PaperStatus string `json:"paper_status"`
ErrorCode int `json:"error_code"`
Model string `json:"model"`
SerialNumber string `json:"serial_number"`
}
func NewEstrellaClient(baseURL, apiKey string) *EstrellaClient {
client := resty.New().
SetTimeout(30 * time.Second).
SetRetryCount(3).
SetRetryWaitTime(1 * time.Second)
return &EstrellaClient{
baseURL: baseURL,
apiKey: apiKey,
client: client,
}
}
func (c *EstrellaClient) authenticate() error {
authReq := AuthRequest{APIKey: c.apiKey}
var authResp AuthResponse
_, err := c.client.R().
SetBody(authReq).
SetResult(&authResp).
Post(c.baseURL + "/auth")
if err != nil {
return err
}
c.accessToken = authResp.AccessToken
c.tokenExpiry = time.Now().Add(time.Duration(authResp.ExpiresIn) * time.Second)
return nil
}
func (c *EstrellaClient) ensureAuthenticated() error {
if c.accessToken == "" || time.Now().After(c.tokenExpiry) {
return c.authenticate()
}
return nil
}
func (c *EstrellaClient) PrintReceipt(text string, options PrintRequest) (*resty.Response, error) {
if err := c.ensureAuthenticated(); err != nil {
return nil, err
}
// Установка значений по умолчанию
if options.Width == 0 {
options.Width = 48
}
if options.Encoding == "" {
options.Encoding = "cp866"
}
if options.Align == "" {
options.Align = "left"
}
return c.client.R().
SetHeader("Authorization", "Bearer "+c.accessToken).
SetBody(options).
Post(c.baseURL + "/print")
}
func (c *EstrellaClient) PrintImage(imagePath string, options PrintImageRequest) (*resty.Response, error) {
if err := c.ensureAuthenticated(); err != nil {
return nil, err
}
// Чтение и кодирование изображения
imageData, err := ioutil.ReadFile(imagePath)
if err != nil {
return nil, fmt.Errorf("не удалось прочитать изображение: %v", err)
}
options.ImageBase64 = base64.StdEncoding.EncodeToString(imageData)
// Установка значений по умолчанию
if options.Align == "" {
options.Align = "center"
}
if options.Dithering {
options.Dithering = true
}
return c.client.R().
SetHeader("Authorization", "Bearer "+c.accessToken).
SetBody(options).
Post(c.baseURL + "/print-image")
}
func (c *EstrellaClient) GetStatus() (*StatusResponse, error) {
if err := c.ensureAuthenticated(); err != nil {
return nil, err
}
var status StatusResponse
_, err := c.client.R().
SetHeader("Authorization", "Bearer "+c.accessToken).
SetResult(&status).
Get(c.baseURL + "/status")
return &status, err
}
func (c *EstrellaClient) GetQueueStatus() (*resty.Response, error) {
if err := c.ensureAuthenticated(); err != nil {
return nil, err
}
return c.client.R().
SetHeader("Authorization", "Bearer "+c.accessToken).
Get(c.baseURL + "/queue/status")
}
func main() {
client := NewEstrellaClient("http://localhost:8080", "your-api-key")
// Печать чека
receiptReq := PrintRequest{
Text: "КАССОВЫЙ ЧЕК\n=====================\nТовар 1: 100.00 руб.\nТовар 2: 200.00 руб.\n---------------------\nИТОГО: 300.00 руб.\n=====================\nСпасибо за покупку!",
Cut: true,
Beep: true,
Barcode: "123456789012",
QRCode: "https://example.com/receipt/123",
}
resp, err := client.PrintReceipt(receiptReq.Text, receiptReq)
if err != nil {
fmt.Printf("Ошибка печати: %v\n", err)
return
}
if resp.StatusCode() != 200 {
fmt.Printf("Ошибка ответа сервера: %s\n", resp.Status())
return
}
fmt.Println("Чек успешно отправлен на печать")
// Получение статуса принтера
status, err := client.GetStatus()
if err != nil {
fmt.Printf("Ошибка получения статуса: %v\n", err)
return
}
fmt.Printf("Статус принтера: %s\n", status.Status)
fmt.Printf("Температура: %.1f°C\n", status.Temperature)
fmt.Printf("Статус бумаги: %s\n", status.PaperStatus)
fmt.Printf("Модель: %s\n", status.Model)
// Пример печати изображения
imageReq := PrintImageRequest{
Width: 200,
Align: "center",
Dithering: true,
}
resp, err = client.PrintImage("/path/to/logo.png", imageReq)
if err != nil {
fmt.Printf("Ошибка печати изображения: %v\n", err)
return
}
fmt.Println("Изображение успешно отправлено на печать")
// Получение статуса очереди
queueResp, err := client.GetQueueStatus()
if err != nil {
fmt.Printf("Ошибка получения статуса очереди: %v\n", err)
return
}
var queueStatus map[string]interface{}
if err := json.Unmarshal(queueResp.Body(), &queueStatus); err != nil {
fmt.Printf("Ошибка разбора ответа: %v\n", err)
return
}
fmt.Printf("Статус очереди: %+v\n", queueStatus)
}
Интеграция с веб-приложением
Пример современного веб-интерфейса для печати чеков:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Система печати чеков Estrella</title>
<style>
:root {
--primary-color: #4CAF50;
--primary-hover: #45a049;
--danger-color: #f44336;
--success-color: #4CAF50;
--background-color: #f8f9fa;
--card-background: #ffffff;
--text-color: #333333;
--border-color: #ddd;
--shadow: 0 2px 4px rgba(0,0,0,0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background-color: var(--card-background);
box-shadow: var(--shadow);
margin-bottom: 30px;
padding: 20px 0;
}
h1 {
text-align: center;
color: var(--primary-color);
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.card {
background-color: var(--card-background);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 20px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
}
input, textarea, select {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 16px;
transition: border 0.3s;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--primary-color);
}
textarea {
min-height: 180px;
resize: vertical;
}
.checkbox-group {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.checkbox-group input {
width: auto;
margin-right: 10px;
transform: scale(1.2);
}
button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 12px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: background-color 0.3s;
display: inline-flex;
align-items: center;
justify-content: center;
}
button:hover {
background-color: var(--primary-hover);
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.btn-secondary {
background-color: #6c757d;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.status-message {
padding: 15px;
border-radius: 4px;
margin-top: 20px;
display: none;
}
.status-message.success {
background-color: #dff0d8;
color: #3c763d;
border: 1px solid #d6e9c6;
}
.status-message.error {
background-color: #f2dede;
color: #a94442;
border: 1px solid #ebccd1;
}
.status-message.info {
background-color: #d9edf7;
color: #31708f;
border: 1px solid #bce8f1;
}
.config-section {
grid-column: 1 / -1;
margin-top: 20px;
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s;
}
.tab:hover {
background-color: #f1f1f1;
}
.tab.active {
border-bottom: 2px solid var(--primary-color);
color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.receipt-preview {
background-color: #f9f9f9;
border: 1px solid var(--border-color);
padding: 15px;
margin-top: 15px;
white-space: pre-wrap;
font-family: monospace;
font-size: 12px;
}
.spinner {
display: none;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.button-group {
display: flex;
gap: 10px;
}
.metrics-section {
grid-column: 1 / -1;
margin-top: 20px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.metric-card {
background-color: var(--card-background);
border-radius: 8px;
padding: 15px;
box-shadow: var(--shadow);
text-align: center;
}
.metric-value {
font-size: 24px;
font-weight: bold;
color: var(--primary-color);
}
.metric-label {
font-size: 14px;
color: #666;
margin-top: 5px;
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
.button-group {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Система печати чеков Estrella</h1>
</header>
<div class="main-content">
<div class="card">
<h2>Печать чека</h2>
<form id="printForm">
<div class="form-group">
<label for="text">Текст для печати:</label>
<textarea id="text" name="text" placeholder="Введите текст для печати"></textarea>
</div>
<div class="form-group">
<label for="width">Ширина чека (символов):</label>
<input type="number" id="width" name="width" min="20" max="60" value="48">
</div>
<div class="form-group">
<label for="encoding">Кодировка:</label>
<select id="encoding" name="encoding">
<option value="cp866">CP866</option>
<option value="cp1251">CP1251</option>
<option value="utf8">UTF-8</option>
<option value="koi8-r">KOI8-R</option>
</select>
</div>
<div class="form-group">
<label for="alignment">Выравнивание:</label>
<select id="alignment" name="alignment">
<option value="left">По левому краю</option>
<option value="center">По центру</option>
<option value="right">По правому краю</option>
</select>
</div>
<div class="checkbox-group">
<input type="checkbox" id="cut" name="cut" checked>
<label for="cut">Отрезать чек</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="beep" name="beep" checked>
<label for="beep">Звуковой сигнал</label>
</div>
<div class="form-group">
<label for="barcode">Штрих-код (опционально):</label>
<input type="text" id="barcode" name="barcode" placeholder="Введите штрих-код">
</div>
<div class="form-group">
<label for="qr_code">QR-код (URL, опционально):</label>
<input type="text" id="qr_code" name="qr_code" placeholder="Введите URL для QR-кода">
</div>
<div class="button-group">
<button type="submit" id="printButton">
<span>Напечатать</span>
<span class="spinner"></span>
</button>
<button type="button" class="btn-secondary" id="previewButton">Предпросмотр</button>
</div>
</form>
<div id="status" class="status-message"></div>
</div>
<div class="card">
<h2>Печать изображения</h2>
<form id="imageForm">
<div class="form-group">
<label for="imageFile">Выберите изображение:</label>
<input type="file" id="imageFile" name="imageFile" accept="image/*">
</div>
<div class="form-group">
<label for="imageWidth">Ширина изображения (пиксели):</label>
<input type="number" id="imageWidth" name="imageWidth" min="50" max="512" value="200">
</div>
<div class="form-group">
<label for="imageAlignment">Выравнивание:</label>
<select id="imageAlignment" name="imageAlignment">
<option value="left">По левому краю</option>
<option value="center" selected>По центру</option>
<option value="right">По правому краю</option>
</select>
</div>
<div class="checkbox-group">
<input type="checkbox" id="dithering" name="dithering" checked>
<label for="dithering">Дизеринг (для черно-белых изображений)</label>
</div>
<button type="submit" id="printImageButton">
<span>Напечатать изображение</span>
<span class="spinner"></span>
</button>
</form>
</div>
<div class="card metrics-section">
<h2>Статус принтера</h2>
<div class="metrics-grid" id="metricsGrid">
<div class="metric-card">
<div class="metric-value" id="printerStatus">-</div>
<div class="metric-label">Статус</div>
</div>
<div class="metric-card">
<div class="metric-value" id="printerTemp">-</div>
<div class="metric-label">Температура (°C)</div>
</div>
<div class="metric-card">
<div class="metric-value" id="paperStatus">-</div>
<div class="metric-label">Бумага</div>
</div>
<div class="metric-card">
<div class="metric-value" id="errorStatus">-</div>
<div class="metric-label">Код ошибки</div>
</div>
</div>
<div id="printerStatusMessage" class="status-message"></div>
</div>
<div class="card config-section">
<h2>Настройки API</h2>
<div class="tabs">
<div class="tab active" data-tab="basic">Основные</div>
<div class="tab" data-tab="advanced">Расширенные</div>
</div>
<div class="tab-content active" id="basic">
<div class="form-group">
<label for="apiUrl">URL API:</label>
<input type="text" id="apiUrl" value="http://localhost:8080" placeholder="URL API сервера">
</div>
<div class="form-group">
<label for="apiKey">API ключ:</label>
<input type="password" id="apiKey" value="your-api-key" placeholder="Введите API ключ">
</div>
</div>
<div class="tab-content" id="advanced">
<div class="form-group">
<label for="apiTimeout">Таймаут запроса (мс):</label>
<input type="number" id="apiTimeout" value="30000" min="1000" max="120000">
</div>
<div class="form-group">
<label for="retryCount">Количество повторных попыток:</label>
<input type="number" id="retryCount" value="3" min="0" max="10">
</div>
<div class="checkbox-group">
<input type="checkbox" id="useQueue">
<label for="useQueue">Использовать очередь печати</label>
</div>
</div>
</div>
</div>
</div>
<script>
// Утилиты
const DOM = {
elements: {},
init: function() {
// Формы
this.elements.printForm = document.getElementById('printForm');
this.elements.imageForm = document.getElementById('imageForm');
// Поля ввода
this.elements.text = document.getElementById('text');
this.elements.width = document.getElementById('width');
this.elements.encoding = document.getElementById('encoding');
this.elements.alignment = document.getElementById('alignment');
this.elements.cut = document.getElementById('cut');
this.elements.beep = document.getElementById('beep');
this.elements.barcode = document.getElementById('barcode');
this.elements.qr_code = document.getElementById('qr_code');
this.elements.imageFile = document.getElementById('imageFile');
this.elements.imageWidth = document.getElementById('imageWidth');
this.elements.imageAlignment = document.getElementById('imageAlignment');
this.elements.dithering = document.getElementById('dithering');
// Кнопки
this.elements.printButton = document.getElementById('printButton');
this.elements.previewButton = document.getElementById('previewButton');
this.elements.printImageButton = document.getElementById('printImageButton');
// Статус
this.elements.status = document.getElementById('status');
this.elements.printerStatusMessage = document.getElementById('printerStatusMessage');
// Метрики
this.elements.printerStatus = document.getElementById('printerStatus');
this.elements.printerTemp = document.getElementById('printerTemp');
this.elements.paperStatus = document.getElementById('paperStatus');
this.elements.errorStatus = document.getElementById('errorStatus');
// Конфигурация
this.elements.apiUrl = document.getElementById('apiUrl');
this.elements.apiKey = document.getElementById('apiKey');
this.elements.apiTimeout = document.getElementById('apiTimeout');
this.elements.retryCount = document.getElementById('retryCount');
this.elements.useQueue = document.getElementById('useQueue');
// Вкладки
this.elements.tabs = document.querySelectorAll('.tab');
this.elements.tabContents = document.querySelectorAll('.tab-content');
},
showSpinner: function(button) {
const spinner = button.querySelector('.spinner');
if (spinner) {
spinner.style.display = 'inline-block';
button.disabled = true;
}
},
hideSpinner: function(button) {
const spinner = button.querySelector('.spinner');
if (spinner) {
spinner.style.display = 'none';
button.disabled = false;
}
},
showMessage: function(element, message, type) {
element.textContent = message;
element.className = `status-message ${type}`;
element.style.display = 'block';
if (type === 'success' || type === 'info') {
setTimeout(() => {
element.style.display = 'none';
}, 5000);
}
},
updateMetrics: function(metrics) {
if (metrics.status) {
this.elements.printerStatus.textContent = metrics.status;
this.elements.printerTemp.textContent = metrics.temperature || '-';
this.elements.paperStatus.textContent = metrics.paper_status || '-';
this.elements.errorStatus.textContent = metrics.error_code || '0';
}
}
};
// Клиент API
class EstrellaClient {
constructor(config) {
this.config = config;
this.headers = {
'Content-Type': 'application/json'
};
}
async authenticate() {
if (!this.config.apiKey) {
throw new Error('API ключ не указан');
}
const response = await fetch(`${this.config.apiUrl}/auth`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify({ api_key: this.config.apiKey })
});
if (!response.ok) {
throw new Error('Ошибка аутентификации');
}
const data = await response.json();
this.headers['Authorization'] = `Bearer ${data.access_token}`;
return data.access_token;
}
async request(endpoint, options = {}) {
const maxRetries = this.config.retryCount || 3;
let lastError;
for (let i = 0; i <= maxRetries; i++) {
try {
// Добавляем аутентификацию, если ее нет
const headers = { ...this.headers };
// Если это не эндпоинт аутентификации и нет токена, аутентифицируемся
if (endpoint !== '/auth' && !headers['Authorization']) {
await this.authenticate();
headers['Authorization'] = this.headers['Authorization'];
}
const response = await fetch(`${this.config.apiUrl}${endpoint}`, {
...options,
headers
});
if (response.ok) {
return await response.json();
} else if (response.status === 401 && i < maxRetries) {
// Повторная аутентификация
await this.authenticate();
continue;
} else {
const errorData = await response.text();
throw new Error(`Ошибка ${response.status}: ${errorData}`);
}
} catch (error) {
lastError = error;
if (i < maxRetries) {
// Экспоненциальная задержка
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
async printReceipt(data) {
if (this.config.useQueue) {
return this.request('/queue/add', {
method: 'POST',
body: JSON.stringify({
type: 'receipt',
data
})
});
}
return this.request('/print', {
method: 'POST',
body: JSON.stringify(data)
});
}
async printImage(data) {
if (this.config.useQueue) {
return this.request('/queue/add', {
method: 'POST',
body: JSON.stringify({
type: 'image',
data
})
});
}
return this.request('/print-image', {
method: 'POST',
body: JSON.stringify(data)
});
}
async getStatus() {
return this.request('/status');
}
}
// Основной класс приложения
class PrinterApp {
constructor() {
DOM.init();
this.client = new EstrellaClient({
apiUrl: DOM.elements.apiUrl.value,
apiKey: DOM.elements.apiKey.value,
retryCount: parseInt(DOM.elements.retryCount.value) || 3,
useQueue: DOM.elements.useQueue.checked
});
this.initEventListeners();
this.startStatusPolling();
// Установка примера текста чека
this.setReceiptExample();
}
initEventListeners() {
// Вкладки
DOM.elements.tabs.forEach(tab => {
tab.addEventListener('click', () => this.switchTab(tab));
});
// Формы
DOM.elements.printForm.addEventListener('submit', (e) => this.handlePrintReceipt(e));
DOM.elements.imageForm.addEventListener('submit', (e) => this.handlePrintImage(e));
// Кнопки
DOM.elements.previewButton.addEventListener('click', () => this.showPreview());
// Изменение конфигурации
DOM.elements.apiUrl.addEventListener('change', () => this.updateConfig());
DOM.elements.apiKey.addEventListener('change', () => this.updateConfig());
DOM.elements.retryCount.addEventListener('change', () => this.updateConfig());
DOM.elements.useQueue.addEventListener('change', () => this.updateConfig());
}
switchTab(tab) {
// Снимаем активный класс со всех вкладок и контента
DOM.elements.tabs.forEach(t => t.classList.remove('active'));
DOM.elements.tabContents.forEach(c => c.classList.remove('active'));
// Добавляем активный класс выбранной вкладке и соответствующему контенту
tab.classList.add('active');
const tabId = tab.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');
}
async handlePrintReceipt(e) {
e.preventDefault();
DOM.showSpinner(DOM.elements.printButton);
try {
const data = {
text: DOM.elements.text.value,
width: parseInt(DOM.elements.width.value),
encoding: DOM.elements.encoding.value,
alignment: DOM.elements.alignment.value,
cut: DOM.elements.cut.checked,
beep: DOM.elements.beep.checked,
barcode: DOM.elements.barcode.value || null,
qr_code: DOM.elements.qr_code.value || null
};
const response = await this.client.printReceipt(data);
DOM.showMessage(DOM.elements.status, 'Чек успешно отправлен на печать', 'success');
console.log('Ответ сервера:', response);
} catch (error) {
console.error('Ошибка печати:', error);
DOM.showMessage(DOM.elements.status, `Ошибка при отправке на печать: ${error.message}`, 'error');
} finally {
DOM.hideSpinner(DOM.elements.printButton);
}
}
async handlePrintImage(e) {
e.preventDefault();
const file = DOM.elements.imageFile.files[0];
if (!file) {
DOM.showMessage(DOM.elements.status, 'Пожалуйста, выберите изображение', 'error');
return;
}
DOM.showSpinner(DOM.elements.printImageButton);
try {
// Читаем файл и кодируем в Base64
const reader = new FileReader();
reader.onload = async (event) => {
try {
const base64String = event.target.result.split(',')[1];
const data = {
image_base64: base64String,
width: parseInt(DOM.elements.imageWidth.value),
alignment: DOM.elements.imageAlignment.value,
dithering: DOM.elements.dithering.checked
};
const response = await this.client.printImage(data);
DOM.showMessage(DOM.elements.status, 'Изображение успешно отправлено на печать', 'success');
console.log('Ответ сервера:', response);
} catch (error) {
console.error('Ошибка печати изображения:', error);
DOM.showMessage(DOM.elements.status, `Ошибка печати изображения: ${error.message}`, 'error');
} finally {
DOM.hideSpinner(DOM.elements.printImageButton);
}
};
reader.readAsDataURL(file);
} catch (error) {
console.error('Ошибка чтения файла:', error);
DOM.showMessage(DOM.elements.status, `Ошибка чтения файла: ${error.message}`, 'error');
DOM.hideSpinner(DOM.elements.printImageButton);
}
}
showPreview() {
const preview = document.createElement('div');
preview.className = 'receipt-preview';
const text = DOM.elements.text.value;
const width = parseInt(DOM.elements.width.value);
// Простая имитация форматирования текста
const lines = text.split('\n');
const formattedLines = lines.map(line => {
// Центрируем текст, если нужно
if (DOM.elements.alignment.value === 'center' && line.trim() !== '') {
const padding = Math.floor((width - line.length) / 2);
return ' '.repeat(Math.max(0, padding)) + line;
}
return line;
});
preview.textContent = formattedLines.join('\n');
// Показываем превью
const existingPreview = document.querySelector('.receipt-preview');
if (existingPreview) {
existingPreview.remove();
}
DOM.elements.printForm.appendChild(preview);
}
updateConfig() {
this.client.config = {
apiUrl: DOM.elements.apiUrl.value,
apiKey: DOM.elements.apiKey.value,
retryCount: parseInt(DOM.elements.retryCount.value) || 3,
useQueue: DOM.elements.useQueue.checked
};
}
async startStatusPolling() {
// Проверяем статус принтера каждые 5 секунд
setInterval(async () => {
try {
const status = await this.client.getStatus();
DOM.updateMetrics(status);
// Показываем предупреждение, если принтер не в порядке
if (status.status !== 'ready') {
DOM.showMessage(DOM.elements.printerStatusMessage,
`Статус принтера: ${status.status}. Код ошибки: ${status.error_code}`, 'info');
}
} catch (error) {
console.error('Ошибка получения статуса принтера:', error);
DOM.showMessage(DOM.elements.printerStatusMessage,
`Ошибка получения статуса принтера: ${error.message}`, 'error');
}
}, 5000);
// Первоначальная проверка
try {
const status = await this.client.getStatus();
DOM.updateMetrics(status);
} catch (error) {
console.error('Ошибка получения начального статуса принтера:', error);
DOM.showMessage(DOM.elements.printerStatusMessage,
`Ошибка подключения к серверу: ${error.message}`, 'error');
}
}
setReceiptExample() {
const receiptExample = `КАССОВЫЙ ЧЕК
=====================
Товар 1: 100.00 руб.
Товар 2: 200.00 руб.
---------------------
ИТОГО: 300.00 руб.
=====================
Спасибо за покупку!
`;
DOM.elements.text.value = receiptExample;
}
}
// Инициализация приложения после загрузки DOM
document.addEventListener('DOMContentLoaded', () => {
new PrinterApp();
});
</script>
</body>
</html>
Решение распространенных проблем
Проблема: Принтер не определяется
- Проверьте подключение принтера к компьютеру
- Убедитесь, что драйверы установлены правильно
- Проверьте права доступа к USB-устройству:
ls -l /dev/usb/lp0 - Для Linux добавьте пользователя в группы lp и dialout:
sudo usermod -a -G lp,dialout $USER - Перезагрузите систему или выйдите и войдите снова
- Для Epson принтеров попробуйте установить официальный драйвер
- Проверьте, что принтер поддерживается Estrella через эндпоинт
/printers - Для сетевых принтеров проверьте доступность порта:
telnet 192.168.1.100 9100
Проблема: Некорректное отображение кириллицы
- Проверьте параметр
encodingв конфигурационном файле:- Для Epson:
cp866 - Для Star:
cp866 - Для Citizen:
cp1251 - Для Zebra:
utf8 - Для Bixolon:
cp949илиutf8
- Для Epson:
- Убедитесь, что в тексте нет BOM-маркеров
- Для принтеров с поддержкой Unicode установите
encoding = "utf8" - Проверьте настройки терминала/приложения, отправляющего запросы
- Для сложных текстов попробуйте использовать escape-последовательности
Проблема: Сервер не запускается
- Убедитесь, что порт 8080 не занят другим процессом:
netstat -tuln | grep 8080 - Проверьте синтаксис конфигурационного файла:
cargo check - Убедитесь, что все зависимости установлены:
cargo build --release - Для Windows проверьте доступность пути к принтеру:
[printer] path = "\\\\.\\printer" # для Windows - Проверьте права доступа к директории с бинарным файлом
- Проверьте наличие системных библиотек:
ldd ./target/release/estrella
Проблема: Нечеткий текст или изображения
- Увеличьте параметр
densityв конфигурации (максимально 15) - Проверьте параметр
speed(уменьшите для большей четкости) - Убедитесь, что разрешение изображения соответствует возможностям принтера
- Для изображений увеличьте параметр
dithering - Проверьте, что бумага подходит для принтера
- Для термопринтеров проверьте температуру печати
Проблема: Штрих-код не считывается
- Проверьте правильность формата штрих-кода в конфигурации
- Увеличьте параметр
heightдля штрих-кода (минимум 50) - Убедитесь, что параметры
widthсоответствуют стандарту:- CODE128: 2-3
- EAN13: 2-3
- QR: 5-10
- Проверьте контрастность печати
- Убедитесь, что штрих-код не обрезан по краям
- Для сложных штрих-кодов увеличьте параметр
error_correction
Проблема: QR-код не считывается
- Проверьте параметр
error_correction:- Для большинства случаев используйте "M"
- Для поврежденных кодов используйте "Q" или "H"
- Увеличьте параметр
size(минимум 5, максимум 20) - Убедитесь, что URL не содержит запрещенных символов
- Для сложных QR-кодов увеличьте параметр
margin - Проверьте, что QR-код не обрезан по краям
- Убедитесь, что контрастность достаточная
Проблема: Низкая производительность сервера
- Увеличьте количество worker-потоков:
[server] workers = 8 # в зависимости от количества ядер CPU - Проверьте загрузку CPU и памяти
- Оптимизируйте конфигурацию принтера
- Включите кэширование для часто используемых шаблонов
- Используйте очередь для пиковых нагрузок
- Для больших изображений уменьшьте разрешение
- Проверьте сетевое подключение для удаленных принтеров
Проблема: Ошибки аутентификации
- Проверьте правильность API-ключа в конфигурации и в запросах
- Убедитесь, что аутентификация включена в настройках сервера:
[auth] enabled = true - Для Bearer токенов проверьте формат заголовка:
Authorization: Bearer your-api-key - Проверьте, что токен не истек
- Для временных токенов используйте эндпоинт
/refreshдля обновления - Проверьте системные логи на предмет ошибок аутентификации
Проблема: Ошибки при работе с очередью
- Проверьте, что очередь включена в настройках:
[queue] enabled = true - Увеличьте размер очереди при пиковых нагрузках:
[queue] max_size = 5000 - Проверьте статус очереди через эндпоинт
/queue/status - Для постоянной очереди настройте SQLite:
[persistent_queue] path = "/var/lib/estrella/queue.db" max_size = "1 GB" - Проверьте права доступа к директории с базой данных
- Для очень больших очередей настройте регулярную очистку
Проблема: Ошибки при работе с сетевыми принтерами
- Проверьте доступность принтера по сети:
ping <printer_ip> telnet <printer_ip> 9100 - Увеличьте таймауты для сетевых операций:
[printer.network] timeout = 30 retries = 5 - Проверьте настройки файрвола
- Используйте статические IP-адреса для сетевых принтеров
- Для Wi-Fi принтеров проверьте стабильность соединения
- Включите автопереопределение принтера:
[printer.network] auto_reconnect = true reconnect_interval = 10
Проблема: Ошибки при работе с USB-принтерами
- Проверьте, что принтер правильно определяется системой:
lsusb - Проверьте права доступа к USB-устройству:
ls -l /dev/usb/lp* - Для Linux настройте udev правила:
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="04b8", ATTR{idProduct}=="0202", MODE="0666" - Перезагрузите udev:
sudo udevadm control --reload-rules sudo udevadm trigger - Для Windows проверьте драйверы через Диспетчер устройств
- Для Mac проверьте настройки CUPS
Проблема: Ошибки при работе с Bluetooth-принтерами
- Убедитесь, что Bluetooth включен и доступен
- Проверьте, что принтер находится в режиме обнаружения
- Отсканируйте доступные устройства:
bluetoothctl scan on - Проверьте MAC-адрес принтера
- Увеличьте таймаут для Bluetooth операций:
[printer.bluetooth] timeout = 60 - Проверьте расстояние между устройствами и помехи
- Для старых моделей принтеров попробуйте уменьшить скорость передачи
Проблема: Ошибки при работе с принтерами через COM-порт
- Проверьте, что порт правильно определяется:
ls -l /dev/ttyS* - Убедитесь, что настройки порта совпадают с настройками принтера:
[printer] type = "serial" path = "/dev/ttyS0" baud_rate = 9600 data_bits = 8 parity = "none" stop_bits = 1 flow_control = "none" - Для Windows используйте имя вида
COM3:[printer] path = "COM3" - Проверьте, что другие приложения не используют этот порт
- Для Linux добавьте пользователя в группы dialout и uucp
- Проверьте кабель подключения и контакты
Проблема: Ошибки при работе с Docker
- Убедитесь, что Docker запущен:
docker --version - Проверьте права доступа для Docker
- Для правильной работы USB-устройств в Docker используйте флаг
--device:docker run -d --name estrella --device=/dev/usb/lp0 -p 8080:8080 ghcr.io/example/estrella:latest - Для сетевых принтеров проверьте настройки сети Docker
- Для использования томов:
docker run -d -v /path/to/config.toml:/etc/estrella/config.toml -v /var/log/estrella:/var/log/estrella ghcr.io/example/estrella:latest - Проверьте логи Docker:
docker logs estrella - Для использования Docker Compose:
version: '3' services: estrella: image: ghcr.io/example/estrella:latest ports: - "8080:8080" volumes: - ./config.toml:/etc/estrella/config.toml - ./logs:/var/log/estrella devices: - "/dev/usb/lp0:/dev/usb/lp0"
Заключение
Estrella предоставляет мощный и гибкий способ интеграции термопринтеров чеков в ваши приложения. Благодаря написанию на Rust, сервер демонстрирует высокую производительность и низкое потребление ресурсов. Благодаря поддержке различных протоколов и моделей принтеров, а также удобному API, Estrella является отличным выбором для торговых систем, ресторанов и других бизнес-приложений.
В этом руководстве мы рассмотрели установку, настройку, использование и интеграцию сервера Estrella. Надеемся, эта информация поможет вам быстро настроить и использовать Estrella в ваших проектах.
Для получения дополнительной информации посетите:
- официальный репозиторий Estrella
- документацию API
- список поддерживаемых принтеров
- примеры интеграций
Если у вас возникнут вопросы или предложения по улучшению, не стесняйтесь обращаться к сообществу разработчиков Estrella через GitHub Discussions или Telegram-чат.