🖨️ esc/pos implementation in gleam

text protocol basically done

okkdev cefa8edd b4981d17

Changed files
+370 -51
src
test
+129 -40
src/escpos.gleam
··· 1 1 import escpos/protocol 2 2 import gleam/bit_array 3 - import gleam/result 4 - import mug 3 + import gleam/bool 4 + 5 + pub opaque type CommandBuffer { 6 + CommandBuffer(data: BitArray) 7 + } 8 + 9 + pub fn new() -> CommandBuffer { 10 + CommandBuffer(<<>>) 11 + } 12 + 13 + pub fn write(cb: CommandBuffer, text: String) -> CommandBuffer { 14 + append(cb, bit_array.from_string(text)) 15 + } 16 + 17 + pub fn writeln(cb: CommandBuffer, text: String) -> CommandBuffer { 18 + append(cb, bit_array.from_string(text)) 19 + |> append(protocol.lf) 20 + } 21 + 22 + pub fn bold( 23 + cb: CommandBuffer, 24 + commands: fn(CommandBuffer) -> CommandBuffer, 25 + ) -> CommandBuffer { 26 + append(cb, protocol.bold(True)) 27 + |> commands 28 + |> append(protocol.bold(False)) 29 + } 30 + 31 + pub fn underline( 32 + cb: CommandBuffer, 33 + commands: fn(CommandBuffer) -> CommandBuffer, 34 + ) -> CommandBuffer { 35 + append(cb, protocol.underline(True)) 36 + |> commands 37 + |> append(protocol.underline(False)) 38 + } 39 + 40 + pub fn double_strike( 41 + cb: CommandBuffer, 42 + commands: fn(CommandBuffer) -> CommandBuffer, 43 + ) -> CommandBuffer { 44 + append(cb, protocol.double_strike(True)) 45 + |> commands 46 + |> append(protocol.double_strike(False)) 47 + } 5 48 6 - pub opaque type Printer { 7 - Printer(socket: mug.Socket) 49 + pub fn reverse( 50 + cb: CommandBuffer, 51 + commands: fn(CommandBuffer) -> CommandBuffer, 52 + ) -> CommandBuffer { 53 + append(cb, protocol.reverse(True)) 54 + |> commands 55 + |> append(protocol.reverse(False)) 8 56 } 9 57 10 - pub opaque type CommandBuffer { 11 - CommandBuffer(data: BitArray) 58 + pub fn upside_down( 59 + cb: CommandBuffer, 60 + commands: fn(CommandBuffer) -> CommandBuffer, 61 + ) -> CommandBuffer { 62 + append(cb, protocol.upside_down(True)) 63 + |> commands 64 + |> ensure_new_line 65 + |> append(protocol.upside_down(False)) 66 + } 67 + 68 + pub fn smooth( 69 + cb: CommandBuffer, 70 + commands: fn(CommandBuffer) -> CommandBuffer, 71 + ) -> CommandBuffer { 72 + append(cb, protocol.smooth(True)) 73 + |> commands 74 + |> append(protocol.smooth(False)) 12 75 } 13 76 14 - pub opaque type EscposError { 15 - ConnectionFailed(mug.ConnectError) 16 - DisconnectionFailed(mug.Error) 17 - TransmissionError(mug.Error) 18 - PrintError(mug.Error) 77 + pub fn flip( 78 + cb: CommandBuffer, 79 + commands: fn(CommandBuffer) -> CommandBuffer, 80 + ) -> CommandBuffer { 81 + append(cb, protocol.flip(True)) 82 + |> commands 83 + |> append(protocol.flip(False)) 19 84 } 20 85 21 - pub fn connect(ip: String, port: Int) -> Result(Printer, EscposError) { 22 - use socket <- result.try( 23 - mug.new(ip, port) 24 - |> mug.timeout(milliseconds: 500) 25 - |> mug.connect() 26 - |> result.map_error(ConnectionFailed), 27 - ) 86 + pub fn set_font(cb: CommandBuffer, font: protocol.Font) -> CommandBuffer { 87 + append(cb, protocol.font(font)) 88 + } 28 89 29 - use _ <- result.try( 30 - mug.send(socket, protocol.init) 31 - |> result.map_error(TransmissionError), 32 - ) 90 + pub fn reset_font(cb: CommandBuffer) -> CommandBuffer { 91 + append(cb, protocol.font(protocol.FontA)) 92 + } 33 93 34 - Ok(Printer(socket)) 94 + pub fn set_align(cb: CommandBuffer, justify: protocol.Justify) -> CommandBuffer { 95 + ensure_new_line(cb) 96 + |> append(protocol.justify(justify)) 35 97 } 36 98 37 - pub fn new() -> CommandBuffer { 38 - CommandBuffer(<<>>) 99 + pub fn set_text_size( 100 + cb: CommandBuffer, 101 + width: Int, 102 + height: Int, 103 + ) -> CommandBuffer { 104 + append(cb, protocol.character_size(width, height)) 39 105 } 40 106 41 - pub fn write(cb: CommandBuffer, text: String) -> CommandBuffer { 42 - bit_array.append(cb.data, bit_array.from_string(text)) 43 - |> CommandBuffer 107 + pub fn reset_text_size(cb: CommandBuffer) -> CommandBuffer { 108 + append(cb, protocol.character_size(1, 1)) 44 109 } 45 110 46 111 pub fn cut(cb: CommandBuffer) -> CommandBuffer { 47 - bit_array.append(cb.data, protocol.full_cut) 48 - |> CommandBuffer 112 + line_feed(cb, 3) 113 + |> append(protocol.full_cut) 49 114 } 50 115 51 116 pub fn partial_cut(cb: CommandBuffer) -> CommandBuffer { 52 - bit_array.append(cb.data, protocol.partial_cut) 53 - |> CommandBuffer 117 + line_feed(cb, 3) 118 + |> append(protocol.partial_cut) 119 + } 120 + 121 + pub fn new_line(cb: CommandBuffer) -> CommandBuffer { 122 + append(cb, protocol.lf) 54 123 } 55 124 56 125 pub fn line_feed(cb: CommandBuffer, lines: Int) -> CommandBuffer { 57 - bit_array.append(cb.data, protocol.line_feed(lines)) 126 + append(cb, protocol.line_feed(lines)) 127 + } 128 + 129 + pub fn reset(cb: CommandBuffer) -> CommandBuffer { 130 + append(cb, protocol.init) 131 + } 132 + 133 + pub fn custom(cb: CommandBuffer, data: BitArray) -> CommandBuffer { 134 + append(cb, data) 135 + } 136 + 137 + fn append(cb: CommandBuffer, data: BitArray) -> CommandBuffer { 138 + bit_array.append(cb.data, data) 58 139 |> CommandBuffer 59 140 } 60 141 61 - pub fn print(cb: CommandBuffer, printer: Printer) -> Result(Nil, EscposError) { 62 - let data = bit_array.append(cb.data, protocol.cr) 142 + pub fn get_payload(cb: CommandBuffer) -> BitArray { 143 + ensure_new_line(cb).data 144 + } 63 145 64 - mug.send(printer.socket, data) 65 - |> result.map_error(PrintError) 146 + fn ensure_new_line(cb: CommandBuffer) -> CommandBuffer { 147 + case is_new_line(cb) { 148 + True -> cb 149 + False -> new_line(cb) 150 + } 66 151 } 67 152 68 - pub fn disconnect(printer: Printer) -> Result(Nil, EscposError) { 69 - mug.shutdown(printer.socket) 70 - |> result.map_error(DisconnectionFailed) 153 + fn is_new_line(cb: CommandBuffer) -> Bool { 154 + use <- bool.guard(cb.data == <<>>, True) 155 + case bit_array.slice(cb.data, -1, 3) { 156 + Ok(<<27, "d", _>>) -> True 157 + Ok(<<_, _, 10>>) -> True 158 + _ -> False 159 + } 71 160 }
+44
src/escpos/printer.gleam
··· 1 + import escpos.{type CommandBuffer} 2 + import escpos/protocol 3 + import gleam/result 4 + import mug 5 + 6 + pub opaque type Printer { 7 + Printer(socket: mug.Socket) 8 + } 9 + 10 + pub opaque type PrinterError { 11 + ConnectionFailed(mug.ConnectError) 12 + DisconnectionFailed(mug.Error) 13 + TransmissionError(mug.Error) 14 + PrintError(mug.Error) 15 + } 16 + 17 + pub fn connect(ip: String, port: Int) -> Result(Printer, PrinterError) { 18 + use socket <- result.try( 19 + mug.new(ip, port) 20 + |> mug.timeout(milliseconds: 500) 21 + |> mug.connect() 22 + |> result.map_error(ConnectionFailed), 23 + ) 24 + 25 + use _ <- result.try( 26 + mug.send(socket, protocol.init) 27 + |> result.map_error(TransmissionError), 28 + ) 29 + 30 + Ok(Printer(socket)) 31 + } 32 + 33 + /// Sends the CommandBuffer to the printer 34 + pub fn print(cb: CommandBuffer, printer: Printer) -> Result(Nil, PrinterError) { 35 + let data = escpos.get_payload(cb) 36 + 37 + mug.send(printer.socket, data) 38 + |> result.map_error(PrintError) 39 + } 40 + 41 + pub fn disconnect(printer: Printer) -> Result(Nil, PrinterError) { 42 + mug.shutdown(printer.socket) 43 + |> result.map_error(DisconnectionFailed) 44 + }
+96 -4
src/escpos/protocol.gleam
··· 1 + import gleam/int 2 + 3 + pub type Justify { 4 + Left 5 + Center 6 + Right 7 + } 8 + 9 + pub type Font { 10 + FontA 11 + FontB 12 + FontC 13 + FontD 14 + FontE 15 + SpecialFontA 16 + SpecialFontB 17 + } 18 + 1 19 const esc = 27 2 20 3 21 const gs = 29 ··· 6 24 7 25 pub const lf = <<10>> 8 26 9 - pub const cr = <<13>> 10 - 11 27 pub const full_cut = <<gs, "V", 0>> 12 28 13 29 pub const partial_cut = <<gs, "V", 1>> 14 30 15 31 pub fn line_feed(lines: Int) -> BitArray { 16 32 case lines { 17 - l if l < 2 -> lf 18 - l if l > 255 -> <<esc, "d", 255>> 33 + l if l < 2 -> <<esc, "d", 1>> 34 + l if l > 254 -> <<esc, "d", 255>> 19 35 _ -> <<esc, "d", lines>> 20 36 } 21 37 } 38 + 39 + /// requires to be on a new line to take effect 40 + pub fn justify(justify: Justify) -> BitArray { 41 + case justify { 42 + Left -> <<esc, "a", 0>> 43 + Center -> <<esc, "a", 1>> 44 + Right -> <<esc, "a", 2>> 45 + } 46 + } 47 + 48 + pub fn bold(on: Bool) -> BitArray { 49 + case on { 50 + True -> <<esc, "E", 1>> 51 + False -> <<esc, "E", 0>> 52 + } 53 + } 54 + 55 + pub fn underline(on: Bool) -> BitArray { 56 + case on { 57 + True -> <<esc, "-", 1>> 58 + False -> <<esc, "-", 0>> 59 + } 60 + } 61 + 62 + pub fn double_strike(on: Bool) -> BitArray { 63 + case on { 64 + True -> <<esc, "G", 1>> 65 + False -> <<esc, "G", 0>> 66 + } 67 + } 68 + 69 + pub fn reverse(on: Bool) -> BitArray { 70 + case on { 71 + True -> <<gs, "B", 1>> 72 + False -> <<gs, "B", 0>> 73 + } 74 + } 75 + 76 + pub fn upside_down(on: Bool) -> BitArray { 77 + case on { 78 + True -> <<esc, "{", 1>> 79 + False -> <<esc, "{", 0>> 80 + } 81 + } 82 + 83 + pub fn smooth(on: Bool) -> BitArray { 84 + case on { 85 + True -> <<gs, "b", 1>> 86 + False -> <<gs, "b", 0>> 87 + } 88 + } 89 + 90 + pub fn flip(on: Bool) -> BitArray { 91 + case on { 92 + True -> <<esc, "V", 1>> 93 + False -> <<esc, "V", 0>> 94 + } 95 + } 96 + 97 + pub fn font(font: Font) -> BitArray { 98 + case font { 99 + FontA -> <<esc, "M", 0>> 100 + FontB -> <<esc, "M", 1>> 101 + FontC -> <<esc, "M", 2>> 102 + FontD -> <<esc, "M", 3>> 103 + FontE -> <<esc, "M", 4>> 104 + SpecialFontA -> <<esc, "M", 97>> 105 + SpecialFontB -> <<esc, "M", 98>> 106 + } 107 + } 108 + 109 + pub fn character_size(width: Int, height: Int) -> BitArray { 110 + let w = int.clamp(width, min: 1, max: 8) |> int.subtract(1) 111 + let h = int.clamp(height, min: 1, max: 8) |> int.subtract(1) 112 + <<gs, "!", 0:1, w:3, 0:1, h:3>> 113 + }
+101 -7
test/escpos_test.gleam
··· 1 1 import escpos 2 + import escpos/printer 3 + import escpos/protocol 2 4 import gleeunit 3 5 4 6 pub fn main() -> Nil { 5 7 gleeunit.main() 6 8 } 7 9 10 + fn setup_printer() -> Result(printer.Printer, printer.PrinterError) { 11 + printer.connect("10.255.8.62", 9100) 12 + } 13 + 14 + fn test_print( 15 + printer: printer.Printer, 16 + name: String, 17 + commands: fn(escpos.CommandBuffer) -> escpos.CommandBuffer, 18 + ) { 19 + escpos.new() 20 + |> escpos.writeln("----- " <> name) 21 + |> commands 22 + |> escpos.new_line 23 + |> printer.print(printer) 24 + } 25 + 8 26 pub fn print_test() { 9 - let assert Ok(printer) = escpos.connect("localhost", 9100) 27 + let assert Ok(printer) = setup_printer() 10 28 11 29 let assert Ok(Nil) = 12 - escpos.new() 13 - |> escpos.write("Hello, World!") 14 - |> escpos.line_feed(2) 15 - |> escpos.partial_cut() 16 - |> escpos.print(printer) 30 + test_print(printer, "writeln", escpos.write(_, "Hello, World!")) 17 31 18 - escpos.disconnect(printer) 32 + let assert Ok(Nil) = 33 + test_print( 34 + printer, 35 + "write bold", 36 + escpos.bold(_, escpos.write(_, "Hello, World!")), 37 + ) 38 + 39 + let assert Ok(Nil) = 40 + test_print( 41 + printer, 42 + "write underline", 43 + escpos.underline(_, escpos.write(_, "Hello, World!")), 44 + ) 45 + 46 + let assert Ok(Nil) = 47 + test_print( 48 + printer, 49 + "write double strike", 50 + escpos.double_strike(_, escpos.write(_, "Hello, World!")), 51 + ) 52 + 53 + let assert Ok(Nil) = 54 + test_print( 55 + printer, 56 + "write reverse", 57 + escpos.reverse(_, escpos.write(_, "Hello, World!")), 58 + ) 59 + 60 + let assert Ok(Nil) = 61 + test_print( 62 + printer, 63 + "write upside down", 64 + escpos.upside_down(_, escpos.write(_, "Hello, World!")), 65 + ) 66 + 67 + let assert Ok(Nil) = 68 + test_print(printer, "font B", fn(b) { 69 + escpos.set_font(b, protocol.FontB) 70 + |> escpos.write("Hello, World!") 71 + |> escpos.reset_font 72 + }) 73 + 74 + let assert Ok(Nil) = 75 + test_print(printer, "font C", fn(b) { 76 + escpos.set_font(b, protocol.FontC) 77 + |> escpos.write("Hello, World!") 78 + |> escpos.reset_font 79 + }) 80 + 81 + let assert Ok(Nil) = 82 + test_print(printer, "high text size", fn(b) { 83 + escpos.set_text_size(b, 1, 8) 84 + |> escpos.write("Hello, World!") 85 + |> escpos.reset_text_size 86 + }) 87 + 88 + let assert Ok(Nil) = 89 + test_print(printer, "wide text size", fn(b) { 90 + escpos.set_text_size(b, 8, 1) 91 + |> escpos.write("Hello, World!") 92 + |> escpos.reset_text_size 93 + }) 94 + 95 + let assert Ok(Nil) = 96 + test_print(printer, "large text size", fn(b) { 97 + escpos.set_text_size(b, 3, 3) 98 + |> escpos.write("Hello, World!") 99 + |> escpos.reset_text_size 100 + }) 101 + 102 + let assert Ok(Nil) = 103 + test_print(printer, "line feed 5", escpos.line_feed(_, 1)) 104 + 105 + let assert Ok(Nil) = test_print(printer, "partial cut", escpos.partial_cut) 106 + 107 + printer.disconnect(printer) 108 + } 109 + 110 + pub fn protocol_character_size_test() { 111 + assert protocol.character_size(2, 2) == <<29, "!", 0:1, 1:3, 0:1, 1:3>> 112 + assert protocol.character_size(20, 20) == <<29, "!", 0:1, 7:3, 0:1, 7:3>> 19 113 }