🖨️ esc/pos implementation in gleam
1//// Functions for connecting to and communicating with ESC/POS printers.
2////
3//// Supports both USB (device file) and network (TCP socket) connections.
4////
5//// ## Example
6////
7//// ```gleam
8//// // USB printer
9//// let assert Ok(printer) = printer.device("/dev/usb/lp0")
10////
11//// // Network printer
12//// let assert Ok(printer) = printer.connect("192.168.1.100", 9100)
13////
14//// escpos.new()
15//// |> escpos.writeln("Hello!")
16//// |> escpos.cut()
17//// |> printer.print(printer)
18////
19//// // Close network printer socket
20//// printer.disconnect(printer)
21//// ```
22
23import escpos/protocol
24import gleam/bit_array
25import gleam/result
26import mug
27import simplifile
28
29// Holds the constructed BitArray to be sent to the printer
30pub opaque type CommandBuffer {
31 CommandBuffer(data: BitArray)
32}
33
34/// A handle to a connected printer, either over USB or TCP.
35pub opaque type Printer {
36 NetworkPrinter(socket: mug.Socket)
37 UsbPrinter(device: String)
38}
39
40/// Errors that can occur when connecting to or printing with a printer.
41pub type PrinterError {
42 ConnectionFailed(mug.ConnectError)
43 DisconnectionFailed(mug.Error)
44 TransmissionError(mug.Error)
45 NetworkPrintError(mug.Error)
46 UsbPrintError(simplifile.FileError)
47 UsbDeviceError(simplifile.FileError)
48}
49
50/// Opens a USB printer by its device file path (e.g. `/dev/usb/lp0`) and writes
51/// the initialization command.
52pub fn device(path: String) -> Result(Printer, PrinterError) {
53 case simplifile.file_info(path) {
54 Ok(_) -> {
55 use _ <- result.try(
56 simplifile.write_bits(path, protocol.init)
57 |> result.map_error(UsbDeviceError),
58 )
59
60 Ok(UsbPrinter(device: path))
61 }
62 Error(err) -> Error(UsbDeviceError(err))
63 }
64}
65
66/// Connects to a network printer over TCP and sends the initialization command.
67pub fn connect(ip: String, port: Int) -> Result(Printer, PrinterError) {
68 use socket <- result.try(
69 mug.new(ip, port)
70 |> mug.timeout(milliseconds: 1000)
71 |> mug.connect()
72 |> result.map_error(ConnectionFailed),
73 )
74
75 use _ <- result.try(
76 mug.send(socket, protocol.init)
77 |> result.map_error(TransmissionError),
78 )
79
80 Ok(NetworkPrinter(socket))
81}
82
83/// Sends a command buffer to the printer.
84///
85/// For network printers this writes to the TCP socket. For USB printers
86/// this writes directly to the device file.
87pub fn print(cb: CommandBuffer, printer: Printer) -> Result(Nil, PrinterError) {
88 case printer {
89 NetworkPrinter(socket:) ->
90 mug.send(socket, cb.data)
91 |> result.map_error(NetworkPrintError)
92 UsbPrinter(device:) ->
93 simplifile.write_bits(device, cb.data)
94 |> result.map_error(UsbPrintError)
95 }
96}
97
98/// Closes the connection to a network printer. For USB printers this is a no-op.
99pub fn disconnect(printer: Printer) -> Result(Nil, PrinterError) {
100 case printer {
101 NetworkPrinter(socket:) ->
102 mug.shutdown(socket)
103 |> result.map_error(DisconnectionFailed)
104 UsbPrinter(_) -> Ok(Nil)
105 }
106}
107
108// Creates an empty CommandBuffer
109pub fn new_buffer() -> CommandBuffer {
110 CommandBuffer(<<>>)
111}
112
113// Appends BitArray to CommandBuffer
114pub fn append_to_buffer(buffer: CommandBuffer, data: BitArray) -> CommandBuffer {
115 bit_array.append(buffer.data, data)
116 |> CommandBuffer
117}
118
119/// Returns the raw BitArray from CommandBuffer
120pub fn buffer_to_bits(buffer: CommandBuffer) -> BitArray {
121 buffer.data
122}