a2_printer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
@@ -0,0 +1,32 @@
1
+ A2 Printer
2
+ ==========
3
+
4
+ A small library for working with cheap thermal "A2" printers, such as those available from Adafruit and Sparkfun.
5
+
6
+ Simple example
7
+ ------
8
+
9
+ serial_connection = SerialConnection.new("/dev/serial")
10
+ printer = A2Printer.new(serial_connection)
11
+
12
+ printer.begin
13
+ printer.println("Hello world!")
14
+ printer.feed
15
+ printer.bold_on
16
+ printer.println("BOOM!")
17
+ printer.bold_off
18
+
19
+
20
+ Writing to a file
21
+ --------
22
+
23
+ This can be useful if you're going to use some other mechanism to send the bytes to the printer
24
+
25
+ commands_file = File.open("serial_commands.data", "w")
26
+ printer = A2Printer.new(commands_file)
27
+ printer.begin
28
+ printer.println("Hello world!")
29
+
30
+ # etc ...
31
+
32
+ commands_file.close
@@ -0,0 +1,332 @@
1
+ require "serial_connection"
2
+
3
+ class A2Printer
4
+ def initialize(connection)
5
+ @connection = connection
6
+ @print_mode = 0
7
+ end
8
+
9
+ def test_page
10
+ write_bytes(18, 84)
11
+ end
12
+
13
+ def begin(heat_time=150)
14
+ reset()
15
+
16
+ heat_interval = 50 # 2 is default from page 23 of datasheet. Controls speed of printing and darkness
17
+ print_density = 15 # Not sure what the default is. Testing shows the max helps darken text. From page 23.
18
+ print_break_time = 15 # Not sure what the default is. Testing shows the max helps darken text. From page 23.
19
+
20
+ write_bytes(27, 55)
21
+ write_bytes(7) # Default 64 dots = 8*('7'+1)
22
+ write_bytes(heat_time) # Default 80 or 800us
23
+ write_bytes(heat_interval) # Default 2 or 20us
24
+
25
+ # Modify the print density and timeout
26
+ write_bytes(18, 35)
27
+ print_setting = (print_density<<4) | print_break_time
28
+ write_bytes(print_setting) # Combination of print_density and print_break_time
29
+ end
30
+
31
+ # reset printer
32
+ def reset
33
+ write_bytes(27, 64)
34
+ end
35
+
36
+ # reset formatting
37
+ def set_default
38
+ online
39
+ normal
40
+ underline_off
41
+ justify(:left)
42
+ set_line_height(32)
43
+ set_barcode_height(50)
44
+ end
45
+
46
+ # Feeds by the specified number of lines
47
+ def feed(lines=1)
48
+ # The datasheet claims sending bytes 27, 100, <x> will work
49
+ # but it feeds much much more.
50
+ lines.times { write(10) }
51
+ end
52
+
53
+ # Feeds by the specified number of rows of pixels
54
+ def feed_rows(rows)
55
+ write_bytes(27, 74, rows)
56
+ end
57
+
58
+ def flush
59
+ write_bytes(12)
60
+ end
61
+
62
+ def test_page
63
+ write_bytes(18, 84)
64
+ end
65
+
66
+ def print(string)
67
+ string.bytes { |b| write(b) }
68
+ end
69
+
70
+ def println(string)
71
+ print(string + "\n")
72
+ end
73
+
74
+ def write(c)
75
+ return if (c == 0x13)
76
+ write_bytes(c)
77
+ end
78
+
79
+ def write_bytes(*bytes)
80
+ bytes.each { |b| @connection.putc(b) }
81
+ end
82
+
83
+ # Character commands
84
+
85
+ INVERSE_MASK = (1 << 1)
86
+ UPDOWN_MASK = (1 << 2)
87
+ BOLD_MASK = (1 << 3)
88
+ DOUBLE_HEIGHT_MASK = (1 << 4)
89
+ DOUBLE_WIDTH_MASK = (1 << 5)
90
+ STRIKE_MASK = (1 << 6)
91
+
92
+ def set_print_mode(mask)
93
+ @print_mode |= mask;
94
+ write_print_mode
95
+ end
96
+
97
+ def unset_print_mode(mask)
98
+ @print_mode &= ~mask;
99
+ write_print_mode
100
+ end
101
+
102
+ def write_print_mode
103
+ write_bytes(27, 33, @print_mode)
104
+ end
105
+
106
+ # This will reset bold, inverse, strikeout, upside down and font size
107
+ # It does not reset underline, justification or line height
108
+ def normal
109
+ @print_mode = 0
110
+ write_print_mode
111
+ end
112
+
113
+ def inverse_on
114
+ set_print_mode(INVERSE_MASK)
115
+ end
116
+
117
+ def inverse_off
118
+ unset_print_mode(INVERSE_MASK)
119
+ end
120
+
121
+ def upside_down_on
122
+ set_print_mode(UPDOWN_MASK);
123
+ end
124
+
125
+ def upside_down_off
126
+ unset_print_mode(UPDOWN_MASK);
127
+ end
128
+
129
+ def double_height_on
130
+ set_print_mode(DOUBLE_HEIGHT_MASK)
131
+ end
132
+
133
+ def double_height_off
134
+ unset_print_mode(DOUBLE_HEIGHT_MASK)
135
+ end
136
+
137
+ def double_width_on
138
+ set_print_mode(DOUBLE_WIDTH_MASK)
139
+ end
140
+
141
+ def double_width_off
142
+ unset_print_mode(DOUBLE_WIDTH_MASK)
143
+ end
144
+
145
+ def strike_on
146
+ set_print_mode(STRIKE_MASK)
147
+ end
148
+
149
+ def strike_off
150
+ unset_print_mode(STRIKE_MASK)
151
+ end
152
+
153
+ def bold_on
154
+ set_print_mode(BOLD_MASK)
155
+ end
156
+
157
+ def bold_off
158
+ unset_print_mode(BOLD_MASK)
159
+ end
160
+
161
+ def set_size(size)
162
+ byte = case size
163
+ when :small
164
+ 0
165
+ when :medium
166
+ 10
167
+ when :large
168
+ 25
169
+ end
170
+
171
+ write_bytes(29, 33, byte, 10)
172
+ end
173
+
174
+ # Underlines of different weights can be produced:
175
+ # 0 - no underline
176
+ # 1 - normal underline
177
+ # 2 - thick underline
178
+ def underline_on(weight=1)
179
+ write_bytes(27, 45, weight)
180
+ end
181
+
182
+ def underline_off
183
+ underline_on(0)
184
+ end
185
+
186
+ def justify(position)
187
+ byte = case position
188
+ when :left
189
+ 0
190
+ when :center
191
+ 1
192
+ when :right
193
+ 2
194
+ end
195
+
196
+ write_bytes(0x1B, 0x61, byte)
197
+ end
198
+
199
+ # Bitmaps
200
+
201
+ class Bitmap
202
+ attr_reader :width, :height
203
+
204
+ def initialize(width_or_source, height=nil, source=nil)
205
+ if height.nil? && source.nil?
206
+ set_source(width_or_source)
207
+ extract_width_and_height_from_data
208
+ else
209
+ set_source(source)
210
+ @width = width_or_source
211
+ @height = height
212
+ end
213
+ end
214
+
215
+ def each_block
216
+ row_start = 0
217
+ width_in_bytes = width / 8
218
+ while row_start < height do
219
+ chunk_height = ((height - row_start) > 255) ? 255 : (height - row_start)
220
+ bytes = (0...(width_in_bytes * chunk_height)).map { @data.getbyte }
221
+ yield width_in_bytes, chunk_height, bytes
222
+ row_start += 255
223
+ end
224
+ end
225
+
226
+ private
227
+
228
+ def set_source(source)
229
+ if source.respond_to?(:getbyte)
230
+ @data = source
231
+ else
232
+ @data = StringIO.new(source.map(&:chr).join)
233
+ end
234
+ end
235
+
236
+ def extract_width_and_height_from_data
237
+ tmp = @data.getbyte
238
+ @width = (@data.getbyte << 8) + tmp
239
+ tmp = @data.getbyte
240
+ @height = (@data.getbyte << 8) + tmp
241
+ end
242
+ end
243
+
244
+ def print_bitmap(*args)
245
+ bitmap = Bitmap.new(*args)
246
+ return if (bitmap.width > 384) # maximum width of the printer
247
+ bitmap.each_block do |w, h, bytes|
248
+ write_bytes(18, 42)
249
+ write_bytes(h, w)
250
+ write_bytes(*bytes)
251
+ end
252
+ end
253
+
254
+ # def print_bitmap(stream)
255
+ # tmp = stream.getbyte
256
+ # width = (stream.getbyte << 8) + tmp
257
+ #
258
+ # tmp = stream.getbyte
259
+ # height = (stream.getbyte << 8) + tmp
260
+ #
261
+ # print_bitmap(width, height, stream)
262
+ # end
263
+
264
+ # Barcodes
265
+
266
+ def set_barcode_height(val)
267
+ # default is 50
268
+ write_bytes(29, 104, val)
269
+ end
270
+
271
+ UPC_A = 0
272
+ UPC_E = 1
273
+ EAN13 = 2
274
+ EAN8 = 3
275
+ CODE39 = 4
276
+ I25 = 5
277
+ CODEBAR = 6
278
+ CODE93 = 7
279
+ CODE128 = 8
280
+ CODE11 = 9
281
+ MSI = 10
282
+
283
+ def print_barcode(text, type)
284
+ write_bytes(29, 107, type) # set the type first
285
+ text.bytes { |b| write(b) }
286
+ write(0) # Terminator
287
+ end
288
+
289
+ # Take the printer offline. Print commands sent after this will be
290
+ # ignored until `online` is called
291
+ def offline
292
+ write_bytes(27, 61, 0)
293
+ end
294
+
295
+ # Take the printer back online. Subsequent print commands will be
296
+ # obeyed.
297
+ def online
298
+ write_bytes(27, 61, 1)
299
+ end
300
+
301
+ # Put the printer into a low-energy state immediately
302
+ def sleep
303
+ sleep_after(0)
304
+ end
305
+
306
+ # Put the printer into a low-energy state after the given number
307
+ # of seconds
308
+ def sleep_after(seconds)
309
+ write_bytes(27, 56, seconds)
310
+ end
311
+
312
+ # Wake the printer from a low-energy state. This command will wait
313
+ # for 50ms (as directed by the datasheet) before allowing further
314
+ # commands to be send.
315
+ def wake
316
+ write_bytes(255)
317
+ # delay(50) # ?
318
+ end
319
+
320
+ # ==== not working? ====
321
+ def tab
322
+ write(9)
323
+ end
324
+
325
+ def set_char_spacing(spacing)
326
+ write_bytes(27, 32, 0, 10)
327
+ end
328
+
329
+ def set_line_height(val=32)
330
+ write_bytes(27, 51, val) # default is 32
331
+ end
332
+ end
@@ -0,0 +1,18 @@
1
+ require "serialport"
2
+
3
+ class SerialConnection
4
+ def initialize(port=nil)
5
+ port_str = port || "/dev/tty.usbserial-FTF8DKJT"
6
+ baud_rate = 19200
7
+ data_bits = 8
8
+ stop_bits = 1
9
+ parity = SerialPort::NONE
10
+ @serial = SerialPort.new(port_str, baud_rate, data_bits, stop_bits, parity)
11
+ @serial.sync = true
12
+ end
13
+
14
+ def putc(byte)
15
+ @serial.putc(byte)
16
+ end
17
+ end
18
+
@@ -0,0 +1,194 @@
1
+ require "test_helper"
2
+ require "a2_printer"
3
+
4
+ describe A2Printer do
5
+ def sent_bytes
6
+ @test_connection.bytes
7
+ end
8
+
9
+ before do
10
+ @test_connection = TestConnection.new
11
+ @printer = A2Printer.new(@test_connection)
12
+ end
13
+
14
+ it "can be reset" do
15
+ @printer.reset
16
+ sent_bytes.must_equal [27, 64]
17
+ end
18
+
19
+ it "prints a test page" do
20
+ @printer.test_page
21
+ sent_bytes.must_equal [18, 84]
22
+ end
23
+
24
+ it "can be flushed" do
25
+ @printer.flush
26
+ sent_bytes.must_equal [12]
27
+ end
28
+
29
+ it "can be set offline" do
30
+ @printer.offline
31
+ sent_bytes.must_equal [27, 61, 0]
32
+ end
33
+
34
+ it "can be set offline" do
35
+ @printer.online
36
+ sent_bytes.must_equal [27, 61, 1]
37
+ end
38
+
39
+ describe "feeding" do
40
+ it "feeds a single line" do
41
+ @printer.feed
42
+ sent_bytes.must_equal [10]
43
+ end
44
+
45
+ it "feeds multiple lines" do
46
+ @printer.feed(3)
47
+ sent_bytes.must_equal [10, 10, 10]
48
+ end
49
+ end
50
+
51
+ describe "initializing" do
52
+ it "resets the print settings" do
53
+ @printer.begin(100)
54
+ sent_bytes.must_equal [
55
+ 27, 64, # reset
56
+ 27, 55, # start control parameter command
57
+ 7, # dots
58
+ 100, # heat time
59
+ 50, # heat interval
60
+ 18, 35, # start print density command
61
+ (15 << 4) | 15 # print density
62
+ ]
63
+ end
64
+
65
+ it "sets the defaults" do
66
+ @printer.expects(:online)
67
+ @printer.expects(:justify).with(:left)
68
+ @printer.expects(:normal)
69
+ @printer.expects(:underline_off)
70
+ @printer.expects(:set_line_height).with(32)
71
+ @printer.set_default
72
+ end
73
+ end
74
+
75
+ describe "printing characters" do
76
+ it "sends the characters as bytes" do
77
+ @printer.print("hello")
78
+ sent_bytes.must_equal %w(h e l l o).map(&:ord)
79
+ end
80
+
81
+ it "sends a newline character when printing with println" do
82
+ @printer.println("hello")
83
+ sent_bytes.must_equal %w(h e l l o).map(&:ord) + [10]
84
+ end
85
+ end
86
+
87
+ describe "printing bitmaps" do
88
+ describe "shorter than 255 rows" do
89
+ before do
90
+ @printer.print_bitmap(width=8, height=8, [1,2,3,4,5,6,7,8])
91
+ end
92
+
93
+ it "writes the prefix" do
94
+ sent_bytes[0,2].must_equal [18, 42]
95
+ end
96
+
97
+ it "writes the number of rows of the bitmap" do
98
+ sent_bytes[2,1].must_equal [8]
99
+ end
100
+
101
+ it "writes the width of the bitmap in bytes" do
102
+ sent_bytes[3,1].must_equal [1]
103
+ end
104
+
105
+ it "writes the bitmap data" do
106
+ sent_bytes[4..-1].must_equal [1,2,3,4,5,6,7,8]
107
+ end
108
+ end
109
+
110
+ describe "longer than 255 rows" do
111
+ before do
112
+ @printer.print_bitmap(width=8, height=256, [3]*256)
113
+ end
114
+
115
+ it "writes the prefix" do
116
+ sent_bytes[0,2].must_equal [18, 42]
117
+ end
118
+
119
+ it "writes the number of rows of the first bitmap chunk" do
120
+ sent_bytes[2,1].must_equal [255]
121
+ end
122
+
123
+ it "writes the width of the bitmap in bytes" do
124
+ sent_bytes[3,1].must_equal [1]
125
+ end
126
+
127
+ it "writes the first chunk of bitmap data" do
128
+ sent_bytes[4,255].must_equal [3] * 255
129
+ end
130
+
131
+ it "writes the prefix a second time" do
132
+ sent_bytes[259,2].must_equal [18, 42]
133
+ end
134
+
135
+ it "writes the number of rows of the second bitmap chunk" do
136
+ sent_bytes[261,1].must_equal [1]
137
+ end
138
+
139
+ it "writes the width of the bitmap in bytes" do
140
+ sent_bytes[262,1].must_equal [1]
141
+ end
142
+
143
+ it "writes the second chunk of bitmap data" do
144
+ sent_bytes[263..-1].must_equal [3]
145
+ end
146
+ end
147
+
148
+ describe "from an IO object" do
149
+ before do
150
+ data = StringIO.new(([3]*8).map(&:chr).join)
151
+ @printer.print_bitmap(width=8, height=8, data)
152
+ end
153
+
154
+ it "writes the prefix" do
155
+ sent_bytes[0,2].must_equal [18, 42]
156
+ end
157
+
158
+ it "writes the number of rows of the bitmap" do
159
+ sent_bytes[2,1].must_equal [8]
160
+ end
161
+
162
+ it "writes the width of the bitmap in bytes" do
163
+ sent_bytes[3,1].must_equal [1]
164
+ end
165
+
166
+ it "writes the bitmap data" do
167
+ sent_bytes[4..-1].must_equal [3,3,3,3,3,3,3,3]
168
+ end
169
+ end
170
+
171
+ describe "where width and height aren't given" do
172
+ before do
173
+ data = StringIO.new(([8,0,8,0] + [3]*8).map(&:chr).join)
174
+ @printer.print_bitmap(data)
175
+ end
176
+
177
+ it "writes the prefix" do
178
+ sent_bytes[0,2].must_equal [18, 42]
179
+ end
180
+
181
+ it "writes the number of rows of the bitmap" do
182
+ sent_bytes[2,1].must_equal [8]
183
+ end
184
+
185
+ it "writes the width of the bitmap in bytes" do
186
+ sent_bytes[3,1].must_equal [1]
187
+ end
188
+
189
+ it "writes the bitmap data" do
190
+ sent_bytes[4..-1].must_equal [3,3,3,3,3,3,3,3]
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,18 @@
1
+ require "minitest/autorun"
2
+ require "rubygems"
3
+ require "bundler"
4
+ Bundler.require(:default, :test)
5
+ require "mocha"
6
+
7
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
8
+
9
+ class TestConnection
10
+ attr_reader :bytes
11
+ def initialize
12
+ @bytes = []
13
+ end
14
+ def putc(byte)
15
+ @bytes << byte
16
+ end
17
+ end
18
+
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: a2_printer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - James Adam
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: serialport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.4
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.4
30
+ - !ruby/object:Gem::Dependency
31
+ name: mocha
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description:
47
+ email: james@lazyatom.com
48
+ executables: []
49
+ extensions: []
50
+ extra_rdoc_files:
51
+ - README.md
52
+ files:
53
+ - README.md
54
+ - Gemfile
55
+ - test/a2_printer_test.rb
56
+ - test/test_helper.rb
57
+ - lib/a2_printer.rb
58
+ - lib/serial_connection.rb
59
+ homepage: http://lazyatom.com
60
+ licenses: []
61
+ post_install_message:
62
+ rdoc_options:
63
+ - --main
64
+ - README.md
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ segments:
74
+ - 0
75
+ hash: 2772800540462270427
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ segments:
83
+ - 0
84
+ hash: 2772800540462270427
85
+ requirements: []
86
+ rubyforge_project:
87
+ rubygems_version: 1.8.23
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Sending commands to a small thermal printer
91
+ test_files: []