diff --git a/README.md b/README.md index 176921b..1c63547 100644 --- a/README.md +++ b/README.md @@ -20,18 +20,18 @@ gleam add escpos@1 The `escpos/document` module provides a high-level declarative API: ```gleam -import escpos/document.{bold, cut, justify, styled, writeln, Center} +import escpos/document.{bold, cut, justify, styled, text_line, Center} import escpos/printer pub fn main() { let assert Ok(printer) = printer.connect("192.168.1.100", 9100) - document.build([ + document.render([ styled([justify(Center), bold()], [ - writeln("Receipt"), + text_line("Receipt"), ]), - writeln("Item 1 ... $5.00"), - writeln("Item 2 ... $3.50"), + text_line("Item 1 ... $5.00"), + text_line("Item 2 ... $3.50"), cut(), ]) |> printer.print(printer) @@ -51,11 +51,11 @@ pub fn main() { escpos.new() |> escpos.reset() - |> escpos.set_align(Center) - |> escpos.set_bold(True) + |> escpos.align(Center) + |> escpos.bold(True) |> escpos.writeln("Receipt") - |> escpos.set_bold(False) - |> escpos.set_align(Left) + |> escpos.bold(False) + |> escpos.align(Left) |> escpos.writeln("Item 1 ... $5.00") |> escpos.writeln("Item 2 ... $3.50") |> escpos.line_feed(3) diff --git a/dev/escpos_dev.gleam b/dev/escpos_dev.gleam index 2961ed8..3fe45e3 100644 --- a/dev/escpos_dev.gleam +++ b/dev/escpos_dev.gleam @@ -13,15 +13,18 @@ pub fn main() { imgpgm // |> image.dither_ign |> image.dither_bayer4x4(0) - // |> image.dither_bayer2x2(0) + // |> image.dither_bayer2x2(0) - let assert Ok(printer) = printer.connect("10.219.160.62", 9100) + // let assert Ok(printer) = printer.connect("10.219.160.62", 9100) + let assert Ok(printer) = printer.device("/dev/usb/lp0") - escpos.new() - |> escpos.reset - |> escpos.image(imgpgm) - |> escpos.image(imgpbm) - |> escpos.line_feed(3) - |> escpos.cut - |> printer.print(printer) + let assert Ok(_) = + escpos.new() + |> escpos.reset + |> escpos.image(imgpgm) + |> escpos.image(imgpbm) + |> escpos.line_feed(3) + |> escpos.cut + |> printer.print(printer) + // printer.disconnect(printer) } diff --git a/src/escpos.gleam b/src/escpos.gleam index 26c9f12..a13ab81 100644 --- a/src/escpos.gleam +++ b/src/escpos.gleam @@ -12,11 +12,11 @@ //// //// escpos.new() //// |> escpos.reset() -//// |> escpos.set_align(Center) -//// |> escpos.set_bold(True) +//// |> escpos.align(Center) +//// |> escpos.bold(True) //// |> escpos.writeln("Receipt") -//// |> escpos.set_bold(False) -//// |> escpos.set_align(Left) +//// |> escpos.bold(False) +//// |> escpos.align(Left) //// |> escpos.writeln("Item 1 ... $5.00") //// |> escpos.cut() //// |> printer.print(my_printer) @@ -53,42 +53,42 @@ pub fn writeln(cb: CommandBuffer, text: String) -> CommandBuffer { } /// Enables or disables bold text. -pub fn set_bold(cb: CommandBuffer, b: Bool) -> CommandBuffer { +pub fn bold(cb: CommandBuffer, b: Bool) -> CommandBuffer { append(cb, protocol.bold(b)) } /// Enables or disables underlined text. -pub fn set_underline(cb: CommandBuffer, b: Bool) -> CommandBuffer { +pub fn underline(cb: CommandBuffer, b: Bool) -> CommandBuffer { append(cb, protocol.underline(b)) } /// Enables or disables double-strike text. -pub fn set_double_strike(cb: CommandBuffer, b: Bool) -> CommandBuffer { +pub fn double_strike(cb: CommandBuffer, b: Bool) -> CommandBuffer { append(cb, protocol.double_strike(b)) } /// Enables or disables reverse (white on black) text. -pub fn set_reverse(cb: CommandBuffer, b: Bool) -> CommandBuffer { +pub fn reverse(cb: CommandBuffer, b: Bool) -> CommandBuffer { append(cb, protocol.reverse(b)) } /// Enables or disables upside-down text. -pub fn set_upside_down(cb: CommandBuffer, b: Bool) -> CommandBuffer { +pub fn upside_down(cb: CommandBuffer, b: Bool) -> CommandBuffer { append(cb, protocol.upside_down(b)) } /// Enables or disables character smoothing. -pub fn set_smooth(cb: CommandBuffer, b: Bool) -> CommandBuffer { +pub fn smooth(cb: CommandBuffer, b: Bool) -> CommandBuffer { append(cb, protocol.smooth(b)) } /// Enables or disables 180-degree rotation. -pub fn set_flip(cb: CommandBuffer, b: Bool) -> CommandBuffer { +pub fn flip(cb: CommandBuffer, b: Bool) -> CommandBuffer { append(cb, protocol.flip(b)) } /// Sets the printer font. -pub fn set_font(cb: CommandBuffer, font: Font) -> CommandBuffer { +pub fn font(cb: CommandBuffer, font: Font) -> CommandBuffer { append(cb, protocol.font(font)) } @@ -98,13 +98,13 @@ pub fn reset_font(cb: CommandBuffer) -> CommandBuffer { } /// Sets text alignment (Left, Center, or Right). -pub fn set_align(cb: CommandBuffer, justify: Justify) -> CommandBuffer { +pub fn align(cb: CommandBuffer, justify: Justify) -> CommandBuffer { ensure_new_line(cb) |> append(protocol.justify(justify)) } /// Sets text size multiplier (1-8 for width and height). -pub fn set_text_size( +pub fn text_size( cb: CommandBuffer, width: Int, height: Int, @@ -169,8 +169,8 @@ pub fn image(cb: CommandBuffer, image: image.PrintableImage) -> CommandBuffer { |> append(protocol.print_graphics_buffer()) } -/// Appends raw bytes to the buffer for custom commands. -pub fn custom(cb: CommandBuffer, data: BitArray) -> CommandBuffer { +/// Appends raw bytes to the buffer. +pub fn raw(cb: CommandBuffer, data: BitArray) -> CommandBuffer { append(cb, data) } diff --git a/src/escpos/document.gleam b/src/escpos/document.gleam index 8e87671..7560d54 100644 --- a/src/escpos/document.gleam +++ b/src/escpos/document.gleam @@ -6,13 +6,13 @@ //// ## Example //// //// ```gleam -//// import escpos/document.{bold, cut, justify, styled, writeln, Center} +//// import escpos/document.{bold, cut, justify, styled, text_line, Center} //// -//// document.build([ +//// document.render([ //// styled([justify(Center), bold()], [ -//// writeln("Receipt"), +//// text_line("Receipt"), //// ]), -//// writeln("Item 1 ... $5.00"), +//// text_line("Item 1 ... $5.00"), //// cut(), //// ]) //// ``` @@ -35,13 +35,13 @@ pub type Font = /// A command representing a print operation or content. pub opaque type Command { - Write(String) - Writeln(String) + Text(String) + TextLine(String) LineFeed(Int) Cut(protocol.Cut) Image(image: image.PrintableImage) Styled(modifiers: Set(Modifier), commands: List(Command)) - Custom(BitArray) + Raw(BitArray) } /// A style modifier that affects how text is rendered. @@ -65,7 +65,7 @@ pub opaque type AST { DoLineFeed(Int) DoCut(protocol.Cut) DoImage(image.PrintableImage) - DoCustom(BitArray) + DoRaw(BitArray) SetBold(Bool) SetUnderline(Bool) SetDoubleStrike(Bool) @@ -117,7 +117,7 @@ fn ensure_new_line(state: State, acc: List(AST)) -> #(List(AST), State) { } /// Compiles a list of commands into a binary command buffer ready for printing. -pub fn build(document: List(Command)) -> CommandBuffer { +pub fn render(document: List(Command)) -> CommandBuffer { upside_down_pass(document) |> build_ast |> compile_ast @@ -131,13 +131,13 @@ pub fn styled(modifiers: List(Modifier), commands: List(Command)) -> Command { } /// Prints text without a trailing newline. -pub fn write(text: String) -> Command { - Write(text) +pub fn text(text: String) -> Command { + Text(text) } /// Prints text followed by a newline. -pub fn writeln(text: String) -> Command { - Writeln(text) +pub fn text_line(text: String) -> Command { + TextLine(text) } /// Advances the paper by the specified number of lines. @@ -159,7 +159,7 @@ pub fn new_line() -> Command { /// let assert Ok(img) = image.from_pgm(raw_pgm) /// let img = image.dither_bayer4x4(img, 0) /// -/// document.build([ +/// document.render([ /// image(img), /// cut(), /// ]) @@ -179,8 +179,8 @@ pub fn partial_cut() -> Command { } /// Sends raw ESC/POS bytes to the printer. -pub fn custom(bytes: BitArray) -> Command { - Custom(bytes) +pub fn raw(bytes: BitArray) -> Command { + Raw(bytes) } /// Bold text modifier. @@ -310,13 +310,13 @@ fn do_build_ast( [DoLineFeed(n), ..acc], State(..state, new_line: True), ) - Write(x) -> + Text(x) -> do_build_ast( rest, [DoWrite(x), ..acc], State(..state, new_line: False), ) - Writeln(x) -> { + TextLine(x) -> { let new_acc = list.prepend(acc, DoWrite(x)) |> list.prepend(DoLineFeed(1)) do_build_ast(rest, new_acc, State(..state, new_line: True)) @@ -329,10 +329,10 @@ fn do_build_ast( State(..state, new_line: True), ) } - Custom(b) -> + Raw(b) -> do_build_ast( rest, - [DoCustom(b), ..acc], + [DoRaw(b), ..acc], State(..state, new_line: False), ) } @@ -512,7 +512,7 @@ fn do_compile_ast(ast: List(AST), acc: BitArray) -> BitArray { DoLineFeed(n) -> do_compile_ast(rest, bit_array.append(acc, protocol.line_feed(n))) DoCut(c) -> do_compile_ast(rest, bit_array.append(acc, protocol.cut(c))) - DoCustom(b) -> do_compile_ast(rest, bit_array.append(acc, b)) + DoRaw(b) -> do_compile_ast(rest, bit_array.append(acc, b)) SetBold(on) -> do_compile_ast(rest, bit_array.append(acc, protocol.bold(on))) SetUnderline(on) -> diff --git a/src/escpos/printer.gleam b/src/escpos/printer.gleam index 91124c2..c644ae5 100644 --- a/src/escpos/printer.gleam +++ b/src/escpos/printer.gleam @@ -1,3 +1,25 @@ +//// Functions for connecting to and communicating with ESC/POS printers. +//// +//// Supports both USB (device file) and network (TCP socket) connections. +//// +//// ## Example +//// +//// ```gleam +//// // USB printer +//// let assert Ok(printer) = printer.device("/dev/usb/lp0") +//// +//// // Network printer +//// let assert Ok(printer) = printer.connect("192.168.1.100", 9100) +//// +//// escpos.new() +//// |> escpos.writeln("Hello!") +//// |> escpos.cut() +//// |> printer.print(printer) +//// +//// // Close network printer socket +//// printer.disconnect(printer) +//// ``` + import escpos/protocol import gleam/result import mug @@ -8,11 +30,13 @@ pub type CommandBuffer { CommandBuffer(data: BitArray) } +/// A handle to a connected printer, either over USB or TCP. pub opaque type Printer { NetworkPrinter(socket: mug.Socket) UsbPrinter(device: String) } +/// Errors that can occur when connecting to or printing with a printer. pub opaque type PrinterError { ConnectionFailed(mug.ConnectError) DisconnectionFailed(mug.Error) @@ -20,21 +44,29 @@ pub opaque type PrinterError { NetworkPrintError(mug.Error) UsbPrintError(simplifile.FileError) UsbDeviceError(simplifile.FileError) - UsbDeviceNotFound } +/// Opens a USB printer by its device file path (e.g. `/dev/usb/lp0`) and writes +/// the initialization command. pub fn device(path: String) -> Result(Printer, PrinterError) { - case simplifile.is_file(path) { - Ok(True) -> Ok(UsbPrinter(device: path)) - Ok(False) -> Error(UsbDeviceNotFound) + case simplifile.file_info(path) { + Ok(_) -> { + use _ <- result.try( + simplifile.write_bits(path, protocol.init) + |> result.map_error(UsbDeviceError), + ) + + Ok(UsbPrinter(device: path)) + } Error(err) -> Error(UsbDeviceError(err)) } } +/// Connects to a network printer over TCP and sends the initialization command. pub fn connect(ip: String, port: Int) -> Result(Printer, PrinterError) { use socket <- result.try( mug.new(ip, port) - |> mug.timeout(milliseconds: 500) + |> mug.timeout(milliseconds: 1000) |> mug.connect() |> result.map_error(ConnectionFailed), ) @@ -47,7 +79,10 @@ pub fn connect(ip: String, port: Int) -> Result(Printer, PrinterError) { Ok(NetworkPrinter(socket)) } -/// Sends the CommandBuffer to the printer +/// Sends a command buffer to the printer. +/// +/// For network printers this writes to the TCP socket. For USB printers +/// this writes directly to the device file. pub fn print(cb: CommandBuffer, printer: Printer) -> Result(Nil, PrinterError) { case printer { NetworkPrinter(socket:) -> @@ -59,6 +94,7 @@ pub fn print(cb: CommandBuffer, printer: Printer) -> Result(Nil, PrinterError) { } } +/// Closes the connection to a network printer. For USB printers this is a no-op. pub fn disconnect(printer: Printer) -> Result(Nil, PrinterError) { case printer { NetworkPrinter(socket:) -> diff --git a/src/escpos/protocol.gleam b/src/escpos/protocol.gleam index 62bc71e..8ed7584 100644 --- a/src/escpos/protocol.gleam +++ b/src/escpos/protocol.gleam @@ -1,17 +1,27 @@ +//// Low-level ESC/POS command encoding. +//// +//// Each function returns a `BitArray` containing the raw bytes for a single +//// ESC/POS command. These are used internally by the `escpos` module to +//// build command buffers. + import gleam/bit_array import gleam/int +/// Text justification mode. pub type Justify { Left Center Right } +/// Paper cut mode. pub type Cut { Partial Full } +/// Built-in printer font. Available fonts vary by printer model; +/// FontA and FontB are the most widely supported. pub type Font { FontA FontB @@ -22,18 +32,19 @@ pub type Font { SpecialFontB } -/// most printers only support Monochrome +/// Image tone mode. Most printers only support `Monochrome`. pub type ImageTone { Monochrome MultipleTone } +/// Image scaling factor for the graphics buffer. pub type ImageScale { Scale1x Scale2x } -/// most printers only support Color1 (Black) +/// Print color selection. Most printers only support `Color1` (black). pub type PrintColor { Color1 Color2 @@ -45,10 +56,13 @@ const esc = 27 const gs = 29 +/// Initialize printer command (`ESC @`). pub const init = <> +/// Line feed byte (`LF`). pub const lf = <<10>> +/// Paper cut command (`GS V`). pub fn cut(cut: Cut) -> BitArray { case cut { Full -> <> @@ -56,6 +70,7 @@ pub fn cut(cut: Cut) -> BitArray { } } +/// Feeds the given number of lines, clamped to 1–255 (`ESC d`). pub fn line_feed(lines: Int) -> BitArray { case lines { l if l < 2 -> <> @@ -64,7 +79,8 @@ pub fn line_feed(lines: Int) -> BitArray { } } -/// requires to be on a new line to take effect +/// Sets text justification (`ESC a`). Must be at the start of a line +/// to take effect. pub fn justify(justify: Justify) -> BitArray { case justify { Left -> <> @@ -73,6 +89,7 @@ pub fn justify(justify: Justify) -> BitArray { } } +/// Enables or disables bold text (`ESC E`). pub fn bold(on: Bool) -> BitArray { case on { True -> <> @@ -80,6 +97,7 @@ pub fn bold(on: Bool) -> BitArray { } } +/// Enables or disables underlined text (`ESC -`). pub fn underline(on: Bool) -> BitArray { case on { True -> <> @@ -87,6 +105,7 @@ pub fn underline(on: Bool) -> BitArray { } } +/// Enables or disables double-strike text (`ESC G`). pub fn double_strike(on: Bool) -> BitArray { case on { True -> <> @@ -94,6 +113,7 @@ pub fn double_strike(on: Bool) -> BitArray { } } +/// Enables or disables reverse (white on black) printing (`GS B`). pub fn reverse(on: Bool) -> BitArray { case on { True -> <> @@ -101,6 +121,7 @@ pub fn reverse(on: Bool) -> BitArray { } } +/// Enables or disables upside-down printing (`ESC {`). pub fn upside_down(on: Bool) -> BitArray { case on { True -> <> @@ -108,6 +129,7 @@ pub fn upside_down(on: Bool) -> BitArray { } } +/// Enables or disables character smoothing (`GS b`). pub fn smooth(on: Bool) -> BitArray { case on { True -> <> @@ -115,6 +137,7 @@ pub fn smooth(on: Bool) -> BitArray { } } +/// Enables or disables 180-degree rotation (`ESC V`). pub fn flip(on: Bool) -> BitArray { case on { True -> <> @@ -122,6 +145,7 @@ pub fn flip(on: Bool) -> BitArray { } } +/// Selects a built-in printer font (`ESC M`). pub fn font(font: Font) -> BitArray { case font { FontA -> <> @@ -134,13 +158,15 @@ pub fn font(font: Font) -> BitArray { } } +/// Sets character width and height 1–8 (`GS !`). pub fn character_size(width: Int, height: Int) -> BitArray { let w = int.clamp(width, min: 1, max: 8) |> int.subtract(1) let h = int.clamp(height, min: 1, max: 8) |> int.subtract(1) <> } -/// `gs ( L fn=112` +/// Stores raster image data into the printer's graphics buffer +/// (`GS ( L`, fn=112). pub fn image_to_graphics_buffer( data: BitArray, width: Int, @@ -209,7 +235,7 @@ pub fn image_to_graphics_buffer( >> } -/// `gs ( L fn=50` +/// Prints the contents of the graphics buffer (`GS ( L`, fn=50). pub fn print_graphics_buffer() -> BitArray { <> } diff --git a/test/escpos_test.gleam b/test/escpos_test.gleam index 847a752..f71b521 100644 --- a/test/escpos_test.gleam +++ b/test/escpos_test.gleam @@ -14,33 +14,33 @@ fn setup_printer() -> Result(printer.Printer, printer.PrinterError) { pub fn upside_down_test() { let input = [ - document.write("Hello, World!"), + document.text("Hello, World!"), document.styled([document.upside_down()], [ - document.write("Hello"), - document.write("Australia!"), + document.text("Hello"), + document.text("Australia!"), document.styled([document.bold()], [ - document.write("Hello"), - document.write("Joe!"), + document.text("Hello"), + document.text("Joe!"), document.styled([document.upside_down()], [ - document.write("Hello"), - document.write("New Zealand!"), + document.text("Hello"), + document.text("New Zealand!"), ]), ]), ]), ] let result = [ - document.write("Hello, World!"), + document.text("Hello, World!"), document.styled([document.upside_down()], [ document.styled([document.bold()], [ document.styled([document.upside_down()], [ - document.write("Hello"), - document.write("New Zealand!"), + document.text("Hello"), + document.text("New Zealand!"), ]), - document.write("Joe!"), - document.write("Hello"), + document.text("Joe!"), + document.text("Hello"), ]), - document.write("Australia!"), - document.write("Hello"), + document.text("Australia!"), + document.text("Hello"), ]), ] assert document.upside_down_pass(input) == result @@ -66,21 +66,21 @@ pub fn imp_print_test() { let assert Ok(Nil) = test_print(printer, "font B", fn(b) { - escpos.set_font(b, protocol.FontB) + escpos.font(b, protocol.FontB) |> escpos.write("Hello, World!") |> escpos.reset_font }) let assert Ok(Nil) = test_print(printer, "font C", fn(b) { - escpos.set_font(b, protocol.FontC) + escpos.font(b, protocol.FontC) |> escpos.write("Hello, World!") |> escpos.reset_font }) let assert Ok(Nil) = test_print(printer, "large text size", fn(b) { - escpos.set_text_size(b, 3, 3) + escpos.text_size(b, 3, 3) |> escpos.write("Hello, World!") |> escpos.reset_text_size }) @@ -96,20 +96,20 @@ pub fn decl_print_test() { let assert Ok(printer) = setup_printer() let assert Ok(Nil) = - document.build([ - document.writeln("hello"), + document.render([ + document.text_line("hello"), document.styled([document.bold()], [ - document.writeln("world"), + document.text_line("world"), document.styled([document.justify(protocol.Center)], [ - document.writeln("center and bold"), + document.text_line("center and bold"), ]), ]), document.styled([document.justify(protocol.Right)], [ - document.writeln("right"), + document.text_line("right"), ]), document.styled([document.upside_down()], [ - document.write("Hello"), - document.write("Australia!"), + document.text("Hello"), + document.text("Australia!"), ]), document.line_feed(3), document.cut(),