Receipt & Kitchen Printers
Setup, configuration, and integration for receipt printers and kitchen printers.
Receipt Printers
Supported Models
| Brand | Model | Width | Connection |
|---|---|---|---|
| Star | TSP100III | 80mm | USB, Ethernet, Bluetooth |
| Star | TSP650II | 80mm | Ethernet, USB |
| Star | mC-Print3 | 80mm | USB, Ethernet, Bluetooth |
| Epson | TM-T88VI | 80mm | USB, Ethernet |
| Epson | TM-m30II | 80mm | USB, Ethernet, Bluetooth |
| Bixolon | SRP-350III | 80mm | USB, Ethernet |
Configuration
# config/hardware/printers.yaml
printers:
receipt:
- id: "printer_001"
name: "Front Counter"
brand: "star"
model: "TSP100III"
connection:
type: "ethernet"
address: "192.168.1.100"
port: 9100
paper_width: 80
auto_cut: true
open_drawer: true
drawer_pin: 2
kitchen:
- id: "printer_002"
name: "Hot Line"
brand: "star"
model: "TSP650II"
connection:
type: "ethernet"
address: "192.168.1.101"
port: 9100
paper_width: 80
auto_cut: true
Star Printer Integration
// src/hardware/printers/star.rs
use star_io10::{StarDeviceDiscoveryManager, StarPrinter, StarPrintCommand};
pub struct StarPrinterService {
printers: HashMap<String, StarPrinter>,
}
impl StarPrinterService {
pub async fn discover_printers() -> Result<Vec<PrinterInfo>> {
let manager = StarDeviceDiscoveryManager::new();
let devices = manager.start_discovery(Duration::from_secs(10)).await?;
Ok(devices.into_iter().map(|d| PrinterInfo {
id: d.model_name.clone(),
name: d.identifier.clone(),
connection: d.connection_settings.clone(),
status: PrinterStatus::Online,
}).collect())
}
pub async fn print_receipt(
&self,
printer_id: &str,
receipt: &Receipt,
) -> Result<()> {
let printer = self.printers.get(printer_id)
.ok_or(Error::PrinterNotFound)?;
let mut commands = StarPrintCommand::new();
// Set alignment center for header
commands.set_alignment(Alignment::Center);
commands.set_emphasis(true);
commands.append_text(&receipt.header.business_name);
commands.set_emphasis(false);
commands.append_line_feed();
// Address
commands.set_font_size(FontSize::Small);
commands.append_text(&receipt.header.address);
commands.append_line_feed();
commands.append_text(&receipt.header.phone);
commands.append_line_feed();
commands.append_line_feed();
// Order info
commands.set_alignment(Alignment::Left);
commands.set_font_size(FontSize::Normal);
commands.append_text(&format!("Order #: {}", receipt.order_number));
commands.append_line_feed();
commands.append_text(&format!("Date: {}", receipt.date));
commands.append_line_feed();
commands.append_text(&format!("Server: {}", receipt.server));
commands.append_line_feed();
commands.append_divider('-');
// Items
for item in &receipt.items {
commands.append_two_column_text(
&format!("{} x {}", item.quantity, item.name),
&format!("${:.2}", item.total)
);
for modifier in &item.modifiers {
commands.append_text(&format!(" - {}", modifier));
commands.append_line_feed();
}
}
commands.append_divider('-');
// Totals
commands.append_two_column_text("Subtotal:", &format!("${:.2}", receipt.subtotal));
commands.append_two_column_text("Tax:", &format!("${:.2}", receipt.tax));
if let Some(tip) = receipt.tip {
commands.append_two_column_text("Tip:", &format!("${:.2}", tip));
}
commands.set_emphasis(true);
commands.append_two_column_text("Total:", &format!("${:.2}", receipt.total));
commands.set_emphasis(false);
// Payment
commands.append_line_feed();
commands.append_text(&format!("Paid by: {}", receipt.payment_method));
commands.append_line_feed();
// Footer
commands.append_line_feed();
commands.set_alignment(Alignment::Center);
commands.append_text("Thank you for dining with us!");
commands.append_line_feed();
commands.append_line_feed();
// Cut
commands.append_cut(CutType::Full);
// Open cash drawer if cash payment
if receipt.payment_method == "Cash" {
commands.append_open_cash_drawer(CashDrawerChannel::Channel1);
}
printer.print(&commands).await?;
Ok(())
}
}
Epson Printer Integration
// src/hardware/printers/epson.rs
use escpos::{printer::Printer, utils::Protocol};
pub struct EpsonPrinterService {
printers: HashMap<String, Printer>,
}
impl EpsonPrinterService {
pub fn new_network_printer(address: &str, port: u16) -> Result<Printer> {
Printer::new(
Protocol::Network {
host: address.to_string(),
port
},
None,
)
}
pub async fn print_receipt(
&self,
printer_id: &str,
receipt: &Receipt,
) -> Result<()> {
let printer = self.printers.get(printer_id)
.ok_or(Error::PrinterNotFound)?;
printer
.init()?
.align(Justification::Center)?
.bold(true)?
.text(&receipt.header.business_name)?
.bold(false)?
.feed()?
.size(0, 0)?
.text(&receipt.header.address)?
.feed()?
.text(&receipt.header.phone)?
.feed()?
.feed()?
.align(Justification::Left)?
.size(1, 1)?;
// Order details
printer
.text(&format!("Order #: {}", receipt.order_number))?
.feed()?
.text(&format!("Date: {}", receipt.date))?
.feed()?
.text("--------------------------------")?
.feed()?;
// Items
for item in &receipt.items {
printer
.text(&format!("{} x {}", item.quantity, item.name))?
.justify_two_column(&format!("${:.2}", item.total))?
.feed()?;
}
// Totals
printer
.text("--------------------------------")?
.feed()?
.justify_two_column("Subtotal:", &format!("${:.2}", receipt.subtotal))?
.feed()?
.justify_two_column("Tax:", &format!("${:.2}", receipt.tax))?
.feed()?
.bold(true)?
.justify_two_column("Total:", &format!("${:.2}", receipt.total))?
.bold(false)?
.feed()?
.feed()?
.align(Justification::Center)?
.text("Thank you!")?
.feed()?
.feed()?
.cut()?
.print()?;
Ok(())
}
}
Kitchen Printers
Configuration
# config/hardware/kitchen_printers.yaml
kitchen_printers:
- id: "kitchen_hot"
name: "Hot Line"
categories: ["entrees", "appetizers", "sides"]
connection:
type: "ethernet"
address: "192.168.1.102"
port: 9100
settings:
font_size: "large"
show_modifiers: true
show_seat_numbers: true
auto_cut: true
bell: true
copies: 1
- id: "kitchen_cold"
name: "Cold Line"
categories: ["salads", "desserts", "cold_apps"]
connection:
type: "ethernet"
address: "192.168.1.103"
port: 9100
settings:
font_size: "large"
auto_cut: true
bell: true
- id: "bar"
name: "Bar"
categories: ["drinks", "cocktails", "beer", "wine"]
connection:
type: "ethernet"
address: "192.168.1.104"
port: 9100
Kitchen Ticket Routing
// src/hardware/printers/kitchen_routing.rs
pub async fn route_order_to_printers(
ctx: &TenantContext,
order: &Order,
) -> Result<Vec<PrintJob>> {
let printers = kitchen_printer_service.get_all(ctx).await?;
let mut jobs = Vec::new();
// Group items by printer
let mut items_by_printer: HashMap<String, Vec<&OrderItem>> = HashMap::new();
for item in &order.items {
let category = item.menu_item.category.as_str();
for printer in &printers {
if printer.categories.contains(&category.to_string()) {
items_by_printer
.entry(printer.id.clone())
.or_default()
.push(item);
}
}
}
// Create print job for each printer
for (printer_id, items) in items_by_printer {
let ticket = KitchenTicket {
order_number: order.order_number.clone(),
order_type: order.order_type.clone(),
table: order.table_number.clone(),
server: order.server_name.clone(),
items: items.into_iter().map(|i| KitchenTicketItem {
quantity: i.quantity,
name: i.name.clone(),
modifiers: i.modifiers.clone(),
seat: i.seat_number,
special_instructions: i.notes.clone(),
}).collect(),
fire_time: order.fire_time,
created_at: Utc::now(),
};
let job = print_kitchen_ticket(&printer_id, &ticket).await?;
jobs.push(job);
}
Ok(jobs)
}
Kitchen Ticket Format
pub async fn print_kitchen_ticket(
printer_id: &str,
ticket: &KitchenTicket,
) -> Result<PrintJob> {
let mut commands = StarPrintCommand::new();
// Header with order type emphasis
commands.set_font_size(FontSize::Large);
commands.set_emphasis(true);
let order_type_display = match ticket.order_type {
OrderType::DineIn => format!("DINE IN - Table {}", ticket.table.unwrap_or_default()),
OrderType::Pickup => "** PICKUP **".to_string(),
OrderType::Delivery => "*** DELIVERY ***".to_string(),
OrderType::DriveThru => "**** DRIVE THRU ****".to_string(),
};
commands.append_text(&order_type_display);
commands.append_line_feed();
commands.set_emphasis(false);
// Order info
commands.set_font_size(FontSize::Normal);
commands.append_text(&format!("Order #{}", ticket.order_number));
commands.append_line_feed();
commands.append_text(&format!("Server: {}", ticket.server));
commands.append_line_feed();
commands.append_text(&format!("Time: {}", ticket.created_at.format("%H:%M")));
commands.append_line_feed();
commands.append_divider('=');
// Items - large font
commands.set_font_size(FontSize::Large);
for item in &ticket.items {
// Quantity and item name
commands.append_text(&format!("{} x {}", item.quantity, item.name.to_uppercase()));
commands.append_line_feed();
// Modifiers
commands.set_font_size(FontSize::Normal);
for modifier in &item.modifiers {
commands.append_text(&format!(" - {}", modifier));
commands.append_line_feed();
}
// Special instructions (highlighted)
if let Some(instructions) = &item.special_instructions {
commands.set_inverse(true);
commands.append_text(&format!(" ** {} **", instructions));
commands.set_inverse(false);
commands.append_line_feed();
}
// Seat number
if let Some(seat) = item.seat {
commands.append_text(&format!(" Seat {}", seat));
commands.append_line_feed();
}
commands.set_font_size(FontSize::Large);
commands.append_line_feed();
}
commands.append_divider('=');
commands.append_cut(CutType::Partial);
// Ring bell
commands.append_buzzer();
let printer = get_printer(printer_id)?;
printer.print(&commands).await?;
Ok(PrintJob {
id: Uuid::new_v4().to_string(),
printer_id: printer_id.to_string(),
status: PrintJobStatus::Completed,
created_at: Utc::now(),
})
}
Related Pages
- Payment Terminals - Card readers and payment terminals
- Peripherals - Cash drawers, customer displays
- Overview - POS Hardware overview