Skip to main content

Receipt & Kitchen Printers

Setup, configuration, and integration for receipt printers and kitchen printers.

Receipt Printers

Supported Models

BrandModelWidthConnection
StarTSP100III80mmUSB, Ethernet, Bluetooth
StarTSP650II80mmEthernet, USB
StarmC-Print380mmUSB, Ethernet, Bluetooth
EpsonTM-T88VI80mmUSB, Ethernet
EpsonTM-m30II80mmUSB, Ethernet, Bluetooth
BixolonSRP-350III80mmUSB, 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(),
})
}