🖨️ esc/pos implementation in gleam

refactor: api tweaks

okk.moe 3d06102b 7feaf208

verified
+155 -90
+9 -9
README.md
··· 20 20 The `escpos/document` module provides a high-level declarative API: 21 21 22 22 ```gleam 23 - import escpos/document.{bold, cut, justify, styled, writeln, Center} 23 + import escpos/document.{bold, cut, justify, styled, text_line, Center} 24 24 import escpos/printer 25 25 26 26 pub fn main() { 27 27 let assert Ok(printer) = printer.connect("192.168.1.100", 9100) 28 28 29 - document.build([ 29 + document.render([ 30 30 styled([justify(Center), bold()], [ 31 - writeln("Receipt"), 31 + text_line("Receipt"), 32 32 ]), 33 - writeln("Item 1 ... $5.00"), 34 - writeln("Item 2 ... $3.50"), 33 + text_line("Item 1 ... $5.00"), 34 + text_line("Item 2 ... $3.50"), 35 35 cut(), 36 36 ]) 37 37 |> printer.print(printer) ··· 51 51 52 52 escpos.new() 53 53 |> escpos.reset() 54 - |> escpos.set_align(Center) 55 - |> escpos.set_bold(True) 54 + |> escpos.align(Center) 55 + |> escpos.bold(True) 56 56 |> escpos.writeln("Receipt") 57 - |> escpos.set_bold(False) 58 - |> escpos.set_align(Left) 57 + |> escpos.bold(False) 58 + |> escpos.align(Left) 59 59 |> escpos.writeln("Item 1 ... $5.00") 60 60 |> escpos.writeln("Item 2 ... $3.50") 61 61 |> escpos.line_feed(3)
+12 -9
dev/escpos_dev.gleam
··· 13 13 imgpgm 14 14 // |> image.dither_ign 15 15 |> image.dither_bayer4x4(0) 16 - // |> image.dither_bayer2x2(0) 16 + // |> image.dither_bayer2x2(0) 17 17 18 - let assert Ok(printer) = printer.connect("10.219.160.62", 9100) 18 + // let assert Ok(printer) = printer.connect("10.219.160.62", 9100) 19 + let assert Ok(printer) = printer.device("/dev/usb/lp0") 19 20 20 - escpos.new() 21 - |> escpos.reset 22 - |> escpos.image(imgpgm) 23 - |> escpos.image(imgpbm) 24 - |> escpos.line_feed(3) 25 - |> escpos.cut 26 - |> printer.print(printer) 21 + let assert Ok(_) = 22 + escpos.new() 23 + |> escpos.reset 24 + |> escpos.image(imgpgm) 25 + |> escpos.image(imgpbm) 26 + |> escpos.line_feed(3) 27 + |> escpos.cut 28 + |> printer.print(printer) 29 + // printer.disconnect(printer) 27 30 }
+16 -16
src/escpos.gleam
··· 12 12 //// 13 13 //// escpos.new() 14 14 //// |> escpos.reset() 15 - //// |> escpos.set_align(Center) 16 - //// |> escpos.set_bold(True) 15 + //// |> escpos.align(Center) 16 + //// |> escpos.bold(True) 17 17 //// |> escpos.writeln("Receipt") 18 - //// |> escpos.set_bold(False) 19 - //// |> escpos.set_align(Left) 18 + //// |> escpos.bold(False) 19 + //// |> escpos.align(Left) 20 20 //// |> escpos.writeln("Item 1 ... $5.00") 21 21 //// |> escpos.cut() 22 22 //// |> printer.print(my_printer) ··· 53 53 } 54 54 55 55 /// Enables or disables bold text. 56 - pub fn set_bold(cb: CommandBuffer, b: Bool) -> CommandBuffer { 56 + pub fn bold(cb: CommandBuffer, b: Bool) -> CommandBuffer { 57 57 append(cb, protocol.bold(b)) 58 58 } 59 59 60 60 /// Enables or disables underlined text. 61 - pub fn set_underline(cb: CommandBuffer, b: Bool) -> CommandBuffer { 61 + pub fn underline(cb: CommandBuffer, b: Bool) -> CommandBuffer { 62 62 append(cb, protocol.underline(b)) 63 63 } 64 64 65 65 /// Enables or disables double-strike text. 66 - pub fn set_double_strike(cb: CommandBuffer, b: Bool) -> CommandBuffer { 66 + pub fn double_strike(cb: CommandBuffer, b: Bool) -> CommandBuffer { 67 67 append(cb, protocol.double_strike(b)) 68 68 } 69 69 70 70 /// Enables or disables reverse (white on black) text. 71 - pub fn set_reverse(cb: CommandBuffer, b: Bool) -> CommandBuffer { 71 + pub fn reverse(cb: CommandBuffer, b: Bool) -> CommandBuffer { 72 72 append(cb, protocol.reverse(b)) 73 73 } 74 74 75 75 /// Enables or disables upside-down text. 76 - pub fn set_upside_down(cb: CommandBuffer, b: Bool) -> CommandBuffer { 76 + pub fn upside_down(cb: CommandBuffer, b: Bool) -> CommandBuffer { 77 77 append(cb, protocol.upside_down(b)) 78 78 } 79 79 80 80 /// Enables or disables character smoothing. 81 - pub fn set_smooth(cb: CommandBuffer, b: Bool) -> CommandBuffer { 81 + pub fn smooth(cb: CommandBuffer, b: Bool) -> CommandBuffer { 82 82 append(cb, protocol.smooth(b)) 83 83 } 84 84 85 85 /// Enables or disables 180-degree rotation. 86 - pub fn set_flip(cb: CommandBuffer, b: Bool) -> CommandBuffer { 86 + pub fn flip(cb: CommandBuffer, b: Bool) -> CommandBuffer { 87 87 append(cb, protocol.flip(b)) 88 88 } 89 89 90 90 /// Sets the printer font. 91 - pub fn set_font(cb: CommandBuffer, font: Font) -> CommandBuffer { 91 + pub fn font(cb: CommandBuffer, font: Font) -> CommandBuffer { 92 92 append(cb, protocol.font(font)) 93 93 } 94 94 ··· 98 98 } 99 99 100 100 /// Sets text alignment (Left, Center, or Right). 101 - pub fn set_align(cb: CommandBuffer, justify: Justify) -> CommandBuffer { 101 + pub fn align(cb: CommandBuffer, justify: Justify) -> CommandBuffer { 102 102 ensure_new_line(cb) 103 103 |> append(protocol.justify(justify)) 104 104 } 105 105 106 106 /// Sets text size multiplier (1-8 for width and height). 107 - pub fn set_text_size( 107 + pub fn text_size( 108 108 cb: CommandBuffer, 109 109 width: Int, 110 110 height: Int, ··· 169 169 |> append(protocol.print_graphics_buffer()) 170 170 } 171 171 172 - /// Appends raw bytes to the buffer for custom commands. 173 - pub fn custom(cb: CommandBuffer, data: BitArray) -> CommandBuffer { 172 + /// Appends raw bytes to the buffer. 173 + pub fn raw(cb: CommandBuffer, data: BitArray) -> CommandBuffer { 174 174 append(cb, data) 175 175 } 176 176
+21 -21
src/escpos/document.gleam
··· 6 6 //// ## Example 7 7 //// 8 8 //// ```gleam 9 - //// import escpos/document.{bold, cut, justify, styled, writeln, Center} 9 + //// import escpos/document.{bold, cut, justify, styled, text_line, Center} 10 10 //// 11 - //// document.build([ 11 + //// document.render([ 12 12 //// styled([justify(Center), bold()], [ 13 - //// writeln("Receipt"), 13 + //// text_line("Receipt"), 14 14 //// ]), 15 - //// writeln("Item 1 ... $5.00"), 15 + //// text_line("Item 1 ... $5.00"), 16 16 //// cut(), 17 17 //// ]) 18 18 //// ``` ··· 35 35 36 36 /// A command representing a print operation or content. 37 37 pub opaque type Command { 38 - Write(String) 39 - Writeln(String) 38 + Text(String) 39 + TextLine(String) 40 40 LineFeed(Int) 41 41 Cut(protocol.Cut) 42 42 Image(image: image.PrintableImage) 43 43 Styled(modifiers: Set(Modifier), commands: List(Command)) 44 - Custom(BitArray) 44 + Raw(BitArray) 45 45 } 46 46 47 47 /// A style modifier that affects how text is rendered. ··· 65 65 DoLineFeed(Int) 66 66 DoCut(protocol.Cut) 67 67 DoImage(image.PrintableImage) 68 - DoCustom(BitArray) 68 + DoRaw(BitArray) 69 69 SetBold(Bool) 70 70 SetUnderline(Bool) 71 71 SetDoubleStrike(Bool) ··· 117 117 } 118 118 119 119 /// Compiles a list of commands into a binary command buffer ready for printing. 120 - pub fn build(document: List(Command)) -> CommandBuffer { 120 + pub fn render(document: List(Command)) -> CommandBuffer { 121 121 upside_down_pass(document) 122 122 |> build_ast 123 123 |> compile_ast ··· 131 131 } 132 132 133 133 /// Prints text without a trailing newline. 134 - pub fn write(text: String) -> Command { 135 - Write(text) 134 + pub fn text(text: String) -> Command { 135 + Text(text) 136 136 } 137 137 138 138 /// Prints text followed by a newline. 139 - pub fn writeln(text: String) -> Command { 140 - Writeln(text) 139 + pub fn text_line(text: String) -> Command { 140 + TextLine(text) 141 141 } 142 142 143 143 /// Advances the paper by the specified number of lines. ··· 159 159 /// let assert Ok(img) = image.from_pgm(raw_pgm) 160 160 /// let img = image.dither_bayer4x4(img, 0) 161 161 /// 162 - /// document.build([ 162 + /// document.render([ 163 163 /// image(img), 164 164 /// cut(), 165 165 /// ]) ··· 179 179 } 180 180 181 181 /// Sends raw ESC/POS bytes to the printer. 182 - pub fn custom(bytes: BitArray) -> Command { 183 - Custom(bytes) 182 + pub fn raw(bytes: BitArray) -> Command { 183 + Raw(bytes) 184 184 } 185 185 186 186 /// Bold text modifier. ··· 310 310 [DoLineFeed(n), ..acc], 311 311 State(..state, new_line: True), 312 312 ) 313 - Write(x) -> 313 + Text(x) -> 314 314 do_build_ast( 315 315 rest, 316 316 [DoWrite(x), ..acc], 317 317 State(..state, new_line: False), 318 318 ) 319 - Writeln(x) -> { 319 + TextLine(x) -> { 320 320 let new_acc = 321 321 list.prepend(acc, DoWrite(x)) |> list.prepend(DoLineFeed(1)) 322 322 do_build_ast(rest, new_acc, State(..state, new_line: True)) ··· 329 329 State(..state, new_line: True), 330 330 ) 331 331 } 332 - Custom(b) -> 332 + Raw(b) -> 333 333 do_build_ast( 334 334 rest, 335 - [DoCustom(b), ..acc], 335 + [DoRaw(b), ..acc], 336 336 State(..state, new_line: False), 337 337 ) 338 338 } ··· 512 512 DoLineFeed(n) -> 513 513 do_compile_ast(rest, bit_array.append(acc, protocol.line_feed(n))) 514 514 DoCut(c) -> do_compile_ast(rest, bit_array.append(acc, protocol.cut(c))) 515 - DoCustom(b) -> do_compile_ast(rest, bit_array.append(acc, b)) 515 + DoRaw(b) -> do_compile_ast(rest, bit_array.append(acc, b)) 516 516 SetBold(on) -> 517 517 do_compile_ast(rest, bit_array.append(acc, protocol.bold(on))) 518 518 SetUnderline(on) ->
+42 -6
src/escpos/printer.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 + 1 23 import escpos/protocol 2 24 import gleam/result 3 25 import mug ··· 8 30 CommandBuffer(data: BitArray) 9 31 } 10 32 33 + /// A handle to a connected printer, either over USB or TCP. 11 34 pub opaque type Printer { 12 35 NetworkPrinter(socket: mug.Socket) 13 36 UsbPrinter(device: String) 14 37 } 15 38 39 + /// Errors that can occur when connecting to or printing with a printer. 16 40 pub opaque type PrinterError { 17 41 ConnectionFailed(mug.ConnectError) 18 42 DisconnectionFailed(mug.Error) ··· 20 44 NetworkPrintError(mug.Error) 21 45 UsbPrintError(simplifile.FileError) 22 46 UsbDeviceError(simplifile.FileError) 23 - UsbDeviceNotFound 24 47 } 25 48 49 + /// Opens a USB printer by its device file path (e.g. `/dev/usb/lp0`) and writes 50 + /// the initialization command. 26 51 pub fn device(path: String) -> Result(Printer, PrinterError) { 27 - case simplifile.is_file(path) { 28 - Ok(True) -> Ok(UsbPrinter(device: path)) 29 - Ok(False) -> Error(UsbDeviceNotFound) 52 + case simplifile.file_info(path) { 53 + Ok(_) -> { 54 + use _ <- result.try( 55 + simplifile.write_bits(path, protocol.init) 56 + |> result.map_error(UsbDeviceError), 57 + ) 58 + 59 + Ok(UsbPrinter(device: path)) 60 + } 30 61 Error(err) -> Error(UsbDeviceError(err)) 31 62 } 32 63 } 33 64 65 + /// Connects to a network printer over TCP and sends the initialization command. 34 66 pub fn connect(ip: String, port: Int) -> Result(Printer, PrinterError) { 35 67 use socket <- result.try( 36 68 mug.new(ip, port) 37 - |> mug.timeout(milliseconds: 500) 69 + |> mug.timeout(milliseconds: 1000) 38 70 |> mug.connect() 39 71 |> result.map_error(ConnectionFailed), 40 72 ) ··· 47 79 Ok(NetworkPrinter(socket)) 48 80 } 49 81 50 - /// Sends the CommandBuffer to the printer 82 + /// Sends a command buffer to the printer. 83 + /// 84 + /// For network printers this writes to the TCP socket. For USB printers 85 + /// this writes directly to the device file. 51 86 pub fn print(cb: CommandBuffer, printer: Printer) -> Result(Nil, PrinterError) { 52 87 case printer { 53 88 NetworkPrinter(socket:) -> ··· 59 94 } 60 95 } 61 96 97 + /// Closes the connection to a network printer. For USB printers this is a no-op. 62 98 pub fn disconnect(printer: Printer) -> Result(Nil, PrinterError) { 63 99 case printer { 64 100 NetworkPrinter(socket:) ->
+31 -5
src/escpos/protocol.gleam
··· 1 + //// Low-level ESC/POS command encoding. 2 + //// 3 + //// Each function returns a `BitArray` containing the raw bytes for a single 4 + //// ESC/POS command. These are used internally by the `escpos` module to 5 + //// build command buffers. 6 + 1 7 import gleam/bit_array 2 8 import gleam/int 3 9 10 + /// Text justification mode. 4 11 pub type Justify { 5 12 Left 6 13 Center 7 14 Right 8 15 } 9 16 17 + /// Paper cut mode. 10 18 pub type Cut { 11 19 Partial 12 20 Full 13 21 } 14 22 23 + /// Built-in printer font. Available fonts vary by printer model; 24 + /// FontA and FontB are the most widely supported. 15 25 pub type Font { 16 26 FontA 17 27 FontB ··· 22 32 SpecialFontB 23 33 } 24 34 25 - /// most printers only support Monochrome 35 + /// Image tone mode. Most printers only support `Monochrome`. 26 36 pub type ImageTone { 27 37 Monochrome 28 38 MultipleTone 29 39 } 30 40 41 + /// Image scaling factor for the graphics buffer. 31 42 pub type ImageScale { 32 43 Scale1x 33 44 Scale2x 34 45 } 35 46 36 - /// most printers only support Color1 (Black) 47 + /// Print color selection. Most printers only support `Color1` (black). 37 48 pub type PrintColor { 38 49 Color1 39 50 Color2 ··· 45 56 46 57 const gs = 29 47 58 59 + /// Initialize printer command (`ESC @`). 48 60 pub const init = <<esc, "@">> 49 61 62 + /// Line feed byte (`LF`). 50 63 pub const lf = <<10>> 51 64 65 + /// Paper cut command (`GS V`). 52 66 pub fn cut(cut: Cut) -> BitArray { 53 67 case cut { 54 68 Full -> <<gs, "V", 0>> ··· 56 70 } 57 71 } 58 72 73 + /// Feeds the given number of lines, clamped to 1–255 (`ESC d`). 59 74 pub fn line_feed(lines: Int) -> BitArray { 60 75 case lines { 61 76 l if l < 2 -> <<esc, "d", 1>> ··· 64 79 } 65 80 } 66 81 67 - /// requires to be on a new line to take effect 82 + /// Sets text justification (`ESC a`). Must be at the start of a line 83 + /// to take effect. 68 84 pub fn justify(justify: Justify) -> BitArray { 69 85 case justify { 70 86 Left -> <<esc, "a", 0>> ··· 73 89 } 74 90 } 75 91 92 + /// Enables or disables bold text (`ESC E`). 76 93 pub fn bold(on: Bool) -> BitArray { 77 94 case on { 78 95 True -> <<esc, "E", 1>> ··· 80 97 } 81 98 } 82 99 100 + /// Enables or disables underlined text (`ESC -`). 83 101 pub fn underline(on: Bool) -> BitArray { 84 102 case on { 85 103 True -> <<esc, "-", 1>> ··· 87 105 } 88 106 } 89 107 108 + /// Enables or disables double-strike text (`ESC G`). 90 109 pub fn double_strike(on: Bool) -> BitArray { 91 110 case on { 92 111 True -> <<esc, "G", 1>> ··· 94 113 } 95 114 } 96 115 116 + /// Enables or disables reverse (white on black) printing (`GS B`). 97 117 pub fn reverse(on: Bool) -> BitArray { 98 118 case on { 99 119 True -> <<gs, "B", 1>> ··· 101 121 } 102 122 } 103 123 124 + /// Enables or disables upside-down printing (`ESC {`). 104 125 pub fn upside_down(on: Bool) -> BitArray { 105 126 case on { 106 127 True -> <<esc, "{", 1>> ··· 108 129 } 109 130 } 110 131 132 + /// Enables or disables character smoothing (`GS b`). 111 133 pub fn smooth(on: Bool) -> BitArray { 112 134 case on { 113 135 True -> <<gs, "b", 1>> ··· 115 137 } 116 138 } 117 139 140 + /// Enables or disables 180-degree rotation (`ESC V`). 118 141 pub fn flip(on: Bool) -> BitArray { 119 142 case on { 120 143 True -> <<esc, "V", 1>> ··· 122 145 } 123 146 } 124 147 148 + /// Selects a built-in printer font (`ESC M`). 125 149 pub fn font(font: Font) -> BitArray { 126 150 case font { 127 151 FontA -> <<esc, "M", 0>> ··· 134 158 } 135 159 } 136 160 161 + /// Sets character width and height 1–8 (`GS !`). 137 162 pub fn character_size(width: Int, height: Int) -> BitArray { 138 163 let w = int.clamp(width, min: 1, max: 8) |> int.subtract(1) 139 164 let h = int.clamp(height, min: 1, max: 8) |> int.subtract(1) 140 165 <<gs, "!", 0:1, w:3, 0:1, h:3>> 141 166 } 142 167 143 - /// `gs ( L fn=112` 168 + /// Stores raster image data into the printer's graphics buffer 169 + /// (`GS ( L`, fn=112). 144 170 pub fn image_to_graphics_buffer( 145 171 data: BitArray, 146 172 width: Int, ··· 209 235 >> 210 236 } 211 237 212 - /// `gs ( L fn=50` 238 + /// Prints the contents of the graphics buffer (`GS ( L`, fn=50). 213 239 pub fn print_graphics_buffer() -> BitArray { 214 240 <<gs, "(", "L", 2, 0, 48, 50>> 215 241 }
+24 -24
test/escpos_test.gleam
··· 14 14 15 15 pub fn upside_down_test() { 16 16 let input = [ 17 - document.write("Hello, World!"), 17 + document.text("Hello, World!"), 18 18 document.styled([document.upside_down()], [ 19 - document.write("Hello"), 20 - document.write("Australia!"), 19 + document.text("Hello"), 20 + document.text("Australia!"), 21 21 document.styled([document.bold()], [ 22 - document.write("Hello"), 23 - document.write("Joe!"), 22 + document.text("Hello"), 23 + document.text("Joe!"), 24 24 document.styled([document.upside_down()], [ 25 - document.write("Hello"), 26 - document.write("New Zealand!"), 25 + document.text("Hello"), 26 + document.text("New Zealand!"), 27 27 ]), 28 28 ]), 29 29 ]), 30 30 ] 31 31 let result = [ 32 - document.write("Hello, World!"), 32 + document.text("Hello, World!"), 33 33 document.styled([document.upside_down()], [ 34 34 document.styled([document.bold()], [ 35 35 document.styled([document.upside_down()], [ 36 - document.write("Hello"), 37 - document.write("New Zealand!"), 36 + document.text("Hello"), 37 + document.text("New Zealand!"), 38 38 ]), 39 - document.write("Joe!"), 40 - document.write("Hello"), 39 + document.text("Joe!"), 40 + document.text("Hello"), 41 41 ]), 42 - document.write("Australia!"), 43 - document.write("Hello"), 42 + document.text("Australia!"), 43 + document.text("Hello"), 44 44 ]), 45 45 ] 46 46 assert document.upside_down_pass(input) == result ··· 66 66 67 67 let assert Ok(Nil) = 68 68 test_print(printer, "font B", fn(b) { 69 - escpos.set_font(b, protocol.FontB) 69 + escpos.font(b, protocol.FontB) 70 70 |> escpos.write("Hello, World!") 71 71 |> escpos.reset_font 72 72 }) 73 73 74 74 let assert Ok(Nil) = 75 75 test_print(printer, "font C", fn(b) { 76 - escpos.set_font(b, protocol.FontC) 76 + escpos.font(b, protocol.FontC) 77 77 |> escpos.write("Hello, World!") 78 78 |> escpos.reset_font 79 79 }) 80 80 81 81 let assert Ok(Nil) = 82 82 test_print(printer, "large text size", fn(b) { 83 - escpos.set_text_size(b, 3, 3) 83 + escpos.text_size(b, 3, 3) 84 84 |> escpos.write("Hello, World!") 85 85 |> escpos.reset_text_size 86 86 }) ··· 96 96 let assert Ok(printer) = setup_printer() 97 97 98 98 let assert Ok(Nil) = 99 - document.build([ 100 - document.writeln("hello"), 99 + document.render([ 100 + document.text_line("hello"), 101 101 document.styled([document.bold()], [ 102 - document.writeln("world"), 102 + document.text_line("world"), 103 103 document.styled([document.justify(protocol.Center)], [ 104 - document.writeln("center and bold"), 104 + document.text_line("center and bold"), 105 105 ]), 106 106 ]), 107 107 document.styled([document.justify(protocol.Right)], [ 108 - document.writeln("right"), 108 + document.text_line("right"), 109 109 ]), 110 110 document.styled([document.upside_down()], [ 111 - document.write("Hello"), 112 - document.write("Australia!"), 111 + document.text("Hello"), 112 + document.text("Australia!"), 113 113 ]), 114 114 document.line_feed(3), 115 115 document.cut(),