rgpio 0.1.0 → 0.1.1

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.
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Count the turns of a rotary encoder with Rgpio::RotaryEncoder, and reset the
4
+ # count with the push switch on its shaft through Rgpio::Button.
5
+ #
6
+ # Module: a KY-040 — CLK / DT / SW / + / GND, with 10 kΩ pull-ups on CLK and DT
7
+ # on the board. Power it from 3.3 V, never 5 V: the pull-ups go to "+", so 5 V
8
+ # there puts 5 V on the GPIO lines. SW has no pull-up on most boards, hence the
9
+ # pull_up: true on the Button below.
10
+ #
11
+ # Wiring:
12
+ # CLK -- GPIO17 (pin 11) phase A
13
+ # DT -- GPIO18 (pin 12) phase B
14
+ # SW -- GPIO27 (pin 13)
15
+ # + -- 3.3 V (pin 1)
16
+ # GND -- GND (pin 6)
17
+ #
18
+ # Run:
19
+ # ruby examples/rotary_encoder.rb
20
+ #
21
+ # A bare encoder (three pins A / C / B, as verified on a Pi 5) needs no board:
22
+ # A to GPIO17, C (the middle pin) to GND, B to GPIO18 — the internal pull-ups
23
+ # do the rest. Without a shaft switch, leave GPIO27 unconnected.
24
+ #
25
+ # Turn the knob; the count stops at ±16. Press the knob to reset it to 0. Stop
26
+ # with Ctrl-C. If clockwise counts down, swap a: and b: (or CLK and DT).
27
+
28
+ require_relative "../lib/rgpio"
29
+
30
+ chip = Rgpio::Chip.new
31
+ encoder = Rgpio::RotaryEncoder.new(a: 17, b: 18, max_steps: 16, chip: chip)
32
+ button = Rgpio::Button.new(27, pull_up: true, chip: chip)
33
+
34
+ begin
35
+ encoder.when_rotated_clockwise { puts format("CW %3d value %+.2f", encoder.steps, encoder.value) }
36
+ encoder.when_rotated_counter_clockwise { puts format("CCW %3d value %+.2f", encoder.steps, encoder.value) }
37
+ button.when_pressed do
38
+ encoder.steps = 0
39
+ puts "Reset"
40
+ end
41
+
42
+ puts "Turn the encoder on GPIO17/18. Press Ctrl-C to stop."
43
+ Rgpio.pause
44
+ ensure
45
+ button.close
46
+ encoder.close
47
+ chip.close
48
+ end
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Tap the screen to drop a ruby: it falls from where you touched and piles up
4
+ # on the ones already there. A game of nothing, on a 2.8" ILI9341 + XPT2046.
5
+ #
6
+ # Wiring: as in examples/touch_paint.rb.
7
+ #
8
+ # Run:
9
+ # ruby examples/ruby_stack.rb
10
+ # ruby examples/ruby_stack.rb <six calibration numbers> # skip calibrating
11
+ #
12
+ # The screen is used in landscape, turned 90 degrees to the left (ROTATION; 90
13
+ # turns it the other way). The calibration follows the rotation, so numbers
14
+ # printed by examples/touch_paint.rb, which runs upright, do not carry over.
15
+ #
16
+ # Touching the grey "Hello, rgpio!" strip at the top clears the pile, and so
17
+ # does a pile that reaches the strip. Ctrl-C to stop.
18
+ #
19
+ # The gem is drawn here from flat facets — it is not the Ruby logo artwork.
20
+
21
+ require_relative "../lib/rgpio"
22
+ require_relative "touch_calibration"
23
+
24
+ $stdout.sync = true
25
+
26
+ ROTATION = 270
27
+ BAR = 24
28
+ TITLE = "Hello, rgpio!".freeze
29
+ GRAVITY = 900.0 # px/s²
30
+ FRAME = 1.0 / 60
31
+
32
+ # One cut gem, 32x28 at scale 1: a crown of five facets over a pavilion of
33
+ # three, each a triangle in its own shade of red.
34
+ module Jewel
35
+ TOP = [[8, 0], [16, 0], [24, 0]].freeze
36
+ GIRDLE = [[0, 8], [11, 8], [21, 8], [32, 8]].freeze
37
+ TIP = [16, 28].freeze
38
+ FACETS = [
39
+ [[TOP[0], GIRDLE[0], GIRDLE[1]], [220, 50, 60]],
40
+ [[TOP[0], TOP[1], GIRDLE[1]], [245, 90, 95]],
41
+ [[TOP[1], GIRDLE[1], GIRDLE[2]], [255, 150, 150]], # the table catches the light
42
+ [[TOP[1], TOP[2], GIRDLE[2]], [235, 70, 80]],
43
+ [[TOP[2], GIRDLE[2], GIRDLE[3]], [190, 20, 35]],
44
+ [[GIRDLE[0], GIRDLE[1], TIP], [170, 10, 30]],
45
+ [[GIRDLE[1], GIRDLE[2], TIP], [215, 30, 45]],
46
+ [[GIRDLE[2], GIRDLE[3], TIP], [130, 0, 20]],
47
+ ].freeze
48
+ WIDTH = 32
49
+ HEIGHT = 28
50
+
51
+ # @return [Array(Integer, Integer, String)] width, height and RGB565 pixels
52
+ # on black, ready for ILI9341#blit
53
+ def self.sprite(scale)
54
+ w = (WIDTH * scale).round
55
+ h = (HEIGHT * scale).round
56
+ facets = FACETS.map { |points, rgb| [points.map { |x, y| [x * scale, y * scale] }, Rgpio::RGB565.from(rgb)] }
57
+ pixels = Array.new(w * h, 0)
58
+ h.times do |y|
59
+ w.times do |x|
60
+ px = x + 0.5
61
+ py = y + 0.5
62
+ hit = facets.find { |points, _| inside?(points, px, py) }
63
+ pixels[(y * w) + x] = hit[1] if hit
64
+ end
65
+ end
66
+ [w, h, pixels.pack("n*")]
67
+ end
68
+
69
+ def self.inside?((a, b, c), x, y)
70
+ d1 = cross(a, b, x, y)
71
+ d2 = cross(b, c, x, y)
72
+ d3 = cross(c, a, x, y)
73
+ !((d1.negative? || d2.negative? || d3.negative?) && (d1.positive? || d2.positive? || d3.positive?))
74
+ end
75
+
76
+ def self.cross((ax, ay), (bx, by), x, y)
77
+ ((bx - ax) * (y - ay)) - ((by - ay) * (x - ax))
78
+ end
79
+ end
80
+
81
+ # A gem on its way down. It lands on the highest thing under any of its columns.
82
+ Falling = Struct.new(:x, :y, :speed, :sprite) do
83
+ def width = sprite[0]
84
+ def height = sprite[1]
85
+ end
86
+
87
+ def clear(lcd, floor)
88
+ floor.fill(lcd.height)
89
+ lcd.fill(:black)
90
+ lcd.fill_rect(0, 0, lcd.width, BAR, :gray)
91
+ title_width, = lcd.text_size(TITLE, scale: 2)
92
+ lcd.text((lcd.width - title_width) / 2, 4, TITLE, color: :red, bg: :gray, scale: 2)
93
+ end
94
+
95
+ chip = Rgpio::Chip.new
96
+ lcd = Rgpio::ILI9341.new(dc: 24, reset: 25, backlight: 18, chip: chip)
97
+ touch = Rgpio::XPT2046.new(irq: 17, chip: chip)
98
+
99
+ begin
100
+ lcd.rotation = ROTATION
101
+ touch.calibration = TouchCalibration.load_or_run(lcd, touch)
102
+
103
+ sprites = [0.75, 1.0, 1.25, 1.5, 2.0].map { |scale| Jewel.sprite(scale) }
104
+ floor = Array.new(lcd.width) # the top of the pile, per column
105
+ clear(lcd, floor)
106
+ puts "2) tap to drop rubies (the strip at the top clears; Ctrl-C to stop)"
107
+
108
+ falling = []
109
+ was_down = false
110
+ dropped = 0
111
+ last = Process.clock_gettime(Process::CLOCK_MONOTONIC)
112
+ loop do
113
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
114
+ dt = now - last
115
+ last = now
116
+
117
+ # A new press, not a held finger, drops one gem.
118
+ x, y = touch.position
119
+ down = !x.nil?
120
+ if down && !was_down
121
+ if y < BAR
122
+ falling.clear
123
+ clear(lcd, floor)
124
+ else
125
+ sprite = sprites.sample
126
+ left = (x - (sprite[0] / 2)).clamp(0, lcd.width - sprite[0])
127
+ top = [y - (sprite[1] / 2), BAR].max
128
+ falling << Falling.new(left, top.to_f, 0.0, sprite)
129
+ dropped += 1
130
+ puts " ruby #{dropped} at #{x}, #{y}"
131
+ end
132
+ end
133
+ was_down = down
134
+
135
+ falling.each do |jewel|
136
+ old_y = jewel.y.round
137
+ jewel.speed += GRAVITY * dt
138
+ jewel.y += jewel.speed * dt
139
+ landing = floor[jewel.x, jewel.width].min - jewel.height
140
+ landed = jewel.y >= landing
141
+ jewel.y = landing.to_f if landed
142
+ new_y = jewel.y.round
143
+
144
+ # Black out only the rows the jewel has left, then draw it lower down.
145
+ lcd.fill_rect(jewel.x, old_y, jewel.width, new_y - old_y, :black) if new_y > old_y
146
+ lcd.blit(jewel.x, new_y, jewel.width, jewel.height, jewel.sprite[2])
147
+ next unless landed
148
+
149
+ floor.fill(new_y, jewel.x, jewel.width)
150
+ jewel.speed = nil
151
+ end
152
+ falling.reject! { |jewel| jewel.speed.nil? }
153
+
154
+ if floor.min < BAR + 40
155
+ puts " the pile reached the top — clearing"
156
+ sleep 1
157
+ falling.clear
158
+ clear(lcd, floor)
159
+ end
160
+
161
+ sleep FRAME
162
+ end
163
+ rescue Interrupt
164
+ nil
165
+ ensure
166
+ touch.close
167
+ lcd.close
168
+ chip.close
169
+ end
data/examples/tft.rb ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Draw on a 2.8" ILI9341 TFT with Rgpio::ILI9341: full-screen fills (timed),
4
+ # rectangles, text at several sizes, and the four rotations.
5
+ #
6
+ # Module: the common red-board 240x320 SPI TFT with an XPT2046 touch
7
+ # controller (pins VCC GND CS RESET DC SDI SCK LED SDO T_CLK T_CS T_DIN T_DO
8
+ # T_IRQ). This example uses the display half only; examples/touch_paint.rb
9
+ # adds the touch panel on the same bus.
10
+ #
11
+ # Wiring (SPI0 must be enabled: sudo raspi-config nonint do_spi 0):
12
+ # VCC -- 3.3 V (pin 1)
13
+ # GND -- GND (pin 6)
14
+ # CS -- GPIO8 (pin 24, CE0)
15
+ # RESET -- GPIO25 (pin 22)
16
+ # DC -- GPIO24 (pin 18)
17
+ # SDI -- GPIO10 (pin 19, MOSI)
18
+ # SCK -- GPIO11 (pin 23, SCLK)
19
+ # LED -- GPIO18 (pin 12)
20
+ # SDO -- leave unconnected: nothing is read from the display, and on some
21
+ # boards it does not let go of MISO, which corrupts touch readings
22
+ #
23
+ # Run:
24
+ # ruby examples/tft.rb
25
+
26
+ require_relative "../lib/rgpio"
27
+
28
+ $stdout.sync = true
29
+
30
+ puts "0) init (reset + init sequence)"
31
+ lcd = Rgpio::ILI9341.new(dc: 24, reset: 25, backlight: 18)
32
+
33
+ begin
34
+ puts "1) full-screen fills, each held for 2 s"
35
+ %i[red green blue white black].each do |color|
36
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
37
+ lcd.fill(color)
38
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
39
+ puts format(" fill %-5s %6.1f ms", color, elapsed * 1000)
40
+ sleep 2
41
+ end
42
+
43
+ puts "2) rectangles and text, held for 3 s"
44
+
45
+ lcd.fill_rect(10, 10, 100, 50, :red)
46
+ lcd.fill_rect(130, 10, 100, 50, [0, 128, 255])
47
+ lcd.text(10, 80, "Hello", color: :white, scale: 2)
48
+ lcd.text(10, 110, "rgpio", color: :yellow, bg: :navy, scale: 3)
49
+ lcd.text(0, 150, (32..126).map(&:chr).join.scan(/.{1,40}/).join("\n"), color: :cyan)
50
+ sleep 3
51
+
52
+ puts "3) rotations, each held for 2 s (red square = top-left)"
53
+ [90, 180, 270, 0].each do |rotation|
54
+ puts " rotation #{rotation}"
55
+ lcd.rotation = rotation
56
+ lcd.fill(:black)
57
+ lcd.fill_rect(0, 0, 40, 40, :red) # always the top-left corner
58
+ lcd.text(50, 10, "rotation #{rotation}", scale: 2)
59
+ lcd.text(50, 40, "#{lcd.width} x #{lcd.height}", color: :green, scale: 2)
60
+ sleep 2
61
+ end
62
+ puts "4) done; closing"
63
+ ensure
64
+ lcd.close
65
+ end
@@ -0,0 +1,61 @@
1
+ # Touch calibration shared by examples/touch_paint.rb and examples/ruby_stack.rb.
2
+ #
3
+ # Six numbers on the command line are used as the calibration as they are;
4
+ # otherwise a cross is shown at each corner and the centre in turn, and the
5
+ # fitted calibration is printed together with the command line that reuses it.
6
+ module TouchCalibration
7
+ MARGIN = 20
8
+
9
+ module_function
10
+
11
+ # @return [Array<Float>] the calibration to give XPT2046#calibration=
12
+ def load_or_run(lcd, touch, args = ARGV)
13
+ unless args.empty?
14
+ raise ArgumentError, "a calibration is six numbers, got #{args.size}" unless args.size == 6
15
+
16
+ return args.map { Float(_1) }
17
+ end
18
+
19
+ puts "1) calibration: touch the centre of each cross"
20
+ calibration = run(lcd, touch)
21
+ numbers = calibration.map { |c| c.round(5) }
22
+ puts "calibration: #{numbers.inspect}"
23
+ puts " skip this next time: ruby #{$PROGRAM_NAME} #{numbers.join(" ")}"
24
+ calibration
25
+ end
26
+
27
+ def run(lcd, touch)
28
+ w = lcd.width
29
+ h = lcd.height
30
+ targets = [[MARGIN, MARGIN], [w - MARGIN, MARGIN], [w - MARGIN, h - MARGIN], [MARGIN, h - MARGIN], [w / 2, h / 2]]
31
+ raw = targets.each_with_index.map do |(x, y), i|
32
+ puts " cross #{i + 1}/#{targets.size} at #{x}, #{y}"
33
+ lcd.fill(:black)
34
+ lcd.text(30, (h / 2) + 30, "Touch the cross", scale: 2)
35
+ cross(lcd, x, y, :white)
36
+ raw_touch(touch)
37
+ end
38
+ Rgpio::XPT2046.calibration_from(targets, raw)
39
+ end
40
+
41
+ def cross(lcd, x, y, color)
42
+ lcd.fill_rect(x - 10, y, 21, 1, color)
43
+ lcd.fill_rect(x, y - 10, 1, 21, color)
44
+ end
45
+
46
+ # Wait for a press, average the raw readings while it is held, then wait for
47
+ # the release.
48
+ def raw_touch(touch)
49
+ sleep 0.01 until touch.touched?
50
+ samples = []
51
+ while (sample = touch.raw)[2] >= touch.threshold
52
+ samples << sample
53
+ sleep 0.01
54
+ end
55
+ sleep 0.2
56
+ return raw_touch(touch) if samples.empty? # a tap too brief to read
57
+
58
+ samples = samples.drop(2) if samples.size > 4 # the first readings are still landing
59
+ [samples.sum { |s| s[0] } / samples.size, samples.sum { |s| s[1] } / samples.size]
60
+ end
61
+ end
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Calibrate the XPT2046 touch panel of a 2.8" ILI9341 module, then draw with a
4
+ # finger or stylus.
5
+ #
6
+ # Wiring: the display as in examples/tft.rb, plus the touch controller on the
7
+ # same SPI bus with chip select CE1:
8
+ # T_CLK -- GPIO11 (pin 23, SCLK) shared with SCK
9
+ # T_DIN -- GPIO10 (pin 19, MOSI) shared with SDI
10
+ # T_DO -- GPIO9 (pin 21, MISO)
11
+ # T_CS -- GPIO7 (pin 26, CE1)
12
+ # T_IRQ -- GPIO17 (pin 11)
13
+ #
14
+ # Run:
15
+ # ruby examples/touch_paint.rb
16
+ # ruby examples/touch_paint.rb <six calibration numbers> # skip calibrating
17
+ #
18
+ # Touch the centre of each cross as it appears. The calibration is printed so
19
+ # it can be passed on the command line next time, or as `calibration: [...]`
20
+ # in a script of your own. Then draw; touching the grey strip at the top clears
21
+ # the screen. Ctrl-C to stop.
22
+
23
+ require_relative "../lib/rgpio"
24
+ require_relative "touch_calibration"
25
+
26
+ $stdout.sync = true
27
+
28
+ BAR = 24
29
+
30
+ def clear(lcd)
31
+ lcd.fill(:black)
32
+ lcd.fill_rect(0, 0, lcd.width, BAR, :gray)
33
+ lcd.text(4, 4, "clear", color: :black, bg: :gray, scale: 2)
34
+ end
35
+
36
+ chip = Rgpio::Chip.new
37
+ lcd = Rgpio::ILI9341.new(dc: 24, reset: 25, backlight: 18, chip: chip)
38
+ touch = Rgpio::XPT2046.new(irq: 17, chip: chip)
39
+
40
+ begin
41
+ touch.calibration = TouchCalibration.load_or_run(lcd, touch)
42
+ clear(lcd)
43
+ puts "2) draw (grey strip at the top clears; Ctrl-C to stop)"
44
+
45
+ # The callbacks run on the touch watcher thread; the loop below polls
46
+ # #position on the main thread. Both can draw: the display serialises them.
47
+ touch.when_touched { |x, y| puts "touched at #{x}, #{y}" }
48
+ touch.when_released { puts "released" }
49
+
50
+ loop do
51
+ x, y = touch.position
52
+ if x.nil?
53
+ sleep 0.01
54
+ elsif y < BAR
55
+ clear(lcd)
56
+ sleep 0.3
57
+ else
58
+ lcd.fill_rect(x - 2, y - 2, 5, 5, :yellow)
59
+ end
60
+ end
61
+ rescue Interrupt
62
+ nil
63
+ ensure
64
+ touch.close
65
+ lcd.close
66
+ chip.close
67
+ end
@@ -97,7 +97,7 @@ module Rgpio
97
97
  raw = ((msb << 8) | lsb)
98
98
  raw >>= 3 if resolution == 13
99
99
  sign_bit = resolution == 13 ? 0x1000 : 0x8000
100
- raw -= sign_bit * 2 if raw & sign_bit != 0
100
+ raw -= sign_bit * 2 if raw.anybits?(sign_bit)
101
101
  raw / counts
102
102
  end
103
103
 
@@ -0,0 +1,209 @@
1
+ module Rgpio
2
+ # The classic 5x7 column font, printable ASCII only (0x20..0x7e).
3
+ #
4
+ # Each glyph is five column bytes, left to right; bit 0 is the top row and
5
+ # bit 6 the bottom, bit 7 unused. A character cell is one column and one row
6
+ # wider than the glyph, so text set at the cell pitch has its own spacing.
7
+ module Font5x7
8
+ WIDTH = 5
9
+ HEIGHT = 7
10
+ CELL_WIDTH = WIDTH + 1
11
+ CELL_HEIGHT = HEIGHT + 1
12
+
13
+ FIRST = 0x20
14
+
15
+ GLYPHS = [
16
+ [0x00, 0x00, 0x00, 0x00, 0x00], # ' '
17
+ [0x00, 0x00, 0x5f, 0x00, 0x00], # !
18
+ [0x00, 0x07, 0x00, 0x07, 0x00], # "
19
+ [0x14, 0x7f, 0x14, 0x7f, 0x14], # #
20
+ [0x24, 0x2a, 0x7f, 0x2a, 0x12], # $
21
+ [0x23, 0x13, 0x08, 0x64, 0x62], # %
22
+ [0x36, 0x49, 0x55, 0x22, 0x50], # &
23
+ [0x00, 0x05, 0x03, 0x00, 0x00], # '
24
+ [0x00, 0x1c, 0x22, 0x41, 0x00], # (
25
+ [0x00, 0x41, 0x22, 0x1c, 0x00], # )
26
+ [0x08, 0x2a, 0x1c, 0x2a, 0x08], # *
27
+ [0x08, 0x08, 0x3e, 0x08, 0x08], # +
28
+ [0x00, 0x50, 0x30, 0x00, 0x00], # ,
29
+ [0x08, 0x08, 0x08, 0x08, 0x08], # -
30
+ [0x00, 0x60, 0x60, 0x00, 0x00], # .
31
+ [0x20, 0x10, 0x08, 0x04, 0x02], # /
32
+ [0x3e, 0x51, 0x49, 0x45, 0x3e], # 0
33
+ [0x00, 0x42, 0x7f, 0x40, 0x00], # 1
34
+ [0x42, 0x61, 0x51, 0x49, 0x46], # 2
35
+ [0x21, 0x41, 0x45, 0x4b, 0x31], # 3
36
+ [0x18, 0x14, 0x12, 0x7f, 0x10], # 4
37
+ [0x27, 0x45, 0x45, 0x45, 0x39], # 5
38
+ [0x3c, 0x4a, 0x49, 0x49, 0x30], # 6
39
+ [0x01, 0x71, 0x09, 0x05, 0x03], # 7
40
+ [0x36, 0x49, 0x49, 0x49, 0x36], # 8
41
+ [0x06, 0x49, 0x49, 0x29, 0x1e], # 9
42
+ [0x00, 0x36, 0x36, 0x00, 0x00], # :
43
+ [0x00, 0x56, 0x36, 0x00, 0x00], # ;
44
+ [0x08, 0x14, 0x22, 0x41, 0x00], # <
45
+ [0x14, 0x14, 0x14, 0x14, 0x14], # =
46
+ [0x00, 0x41, 0x22, 0x14, 0x08], # >
47
+ [0x02, 0x01, 0x51, 0x09, 0x06], # ?
48
+ [0x32, 0x49, 0x79, 0x41, 0x3e], # @
49
+ [0x7e, 0x11, 0x11, 0x11, 0x7e], # A
50
+ [0x7f, 0x49, 0x49, 0x49, 0x36], # B
51
+ [0x3e, 0x41, 0x41, 0x41, 0x22], # C
52
+ [0x7f, 0x41, 0x41, 0x22, 0x1c], # D
53
+ [0x7f, 0x49, 0x49, 0x49, 0x41], # E
54
+ [0x7f, 0x09, 0x09, 0x09, 0x01], # F
55
+ [0x3e, 0x41, 0x49, 0x49, 0x7a], # G
56
+ [0x7f, 0x08, 0x08, 0x08, 0x7f], # H
57
+ [0x00, 0x41, 0x7f, 0x41, 0x00], # I
58
+ [0x20, 0x40, 0x41, 0x3f, 0x01], # J
59
+ [0x7f, 0x08, 0x14, 0x22, 0x41], # K
60
+ [0x7f, 0x40, 0x40, 0x40, 0x40], # L
61
+ [0x7f, 0x02, 0x0c, 0x02, 0x7f], # M
62
+ [0x7f, 0x04, 0x08, 0x10, 0x7f], # N
63
+ [0x3e, 0x41, 0x41, 0x41, 0x3e], # O
64
+ [0x7f, 0x09, 0x09, 0x09, 0x06], # P
65
+ [0x3e, 0x41, 0x51, 0x21, 0x5e], # Q
66
+ [0x7f, 0x09, 0x19, 0x29, 0x46], # R
67
+ [0x46, 0x49, 0x49, 0x49, 0x31], # S
68
+ [0x01, 0x01, 0x7f, 0x01, 0x01], # T
69
+ [0x3f, 0x40, 0x40, 0x40, 0x3f], # U
70
+ [0x1f, 0x20, 0x40, 0x20, 0x1f], # V
71
+ [0x3f, 0x40, 0x38, 0x40, 0x3f], # W
72
+ [0x63, 0x14, 0x08, 0x14, 0x63], # X
73
+ [0x07, 0x08, 0x70, 0x08, 0x07], # Y
74
+ [0x61, 0x51, 0x49, 0x45, 0x43], # Z
75
+ [0x00, 0x7f, 0x41, 0x41, 0x00], # [
76
+ [0x02, 0x04, 0x08, 0x10, 0x20], # \
77
+ [0x00, 0x41, 0x41, 0x7f, 0x00], # ]
78
+ [0x04, 0x02, 0x01, 0x02, 0x04], # ^
79
+ [0x40, 0x40, 0x40, 0x40, 0x40], # _
80
+ [0x00, 0x01, 0x02, 0x04, 0x00], # `
81
+ [0x20, 0x54, 0x54, 0x54, 0x78], # a
82
+ [0x7f, 0x48, 0x44, 0x44, 0x38], # b
83
+ [0x38, 0x44, 0x44, 0x44, 0x20], # c
84
+ [0x38, 0x44, 0x44, 0x48, 0x7f], # d
85
+ [0x38, 0x54, 0x54, 0x54, 0x18], # e
86
+ [0x08, 0x7e, 0x09, 0x01, 0x02], # f
87
+ [0x0c, 0x52, 0x52, 0x52, 0x3e], # g
88
+ [0x7f, 0x08, 0x04, 0x04, 0x78], # h
89
+ [0x00, 0x44, 0x7d, 0x40, 0x00], # i
90
+ [0x20, 0x40, 0x44, 0x3d, 0x00], # j
91
+ [0x7f, 0x10, 0x28, 0x44, 0x00], # k
92
+ [0x00, 0x41, 0x7f, 0x40, 0x00], # l
93
+ [0x7c, 0x04, 0x18, 0x04, 0x78], # m
94
+ [0x7c, 0x08, 0x04, 0x04, 0x78], # n
95
+ [0x38, 0x44, 0x44, 0x44, 0x38], # o
96
+ [0x7c, 0x14, 0x14, 0x14, 0x08], # p
97
+ [0x08, 0x14, 0x14, 0x18, 0x7c], # q
98
+ [0x7c, 0x08, 0x04, 0x04, 0x08], # r
99
+ [0x48, 0x54, 0x54, 0x54, 0x20], # s
100
+ [0x04, 0x3f, 0x44, 0x40, 0x20], # t
101
+ [0x3c, 0x40, 0x40, 0x20, 0x7c], # u
102
+ [0x1c, 0x20, 0x40, 0x20, 0x1c], # v
103
+ [0x3c, 0x40, 0x30, 0x40, 0x3c], # w
104
+ [0x44, 0x28, 0x10, 0x28, 0x44], # x
105
+ [0x0c, 0x50, 0x50, 0x50, 0x3c], # y
106
+ [0x44, 0x64, 0x54, 0x4c, 0x44], # z
107
+ [0x00, 0x08, 0x36, 0x41, 0x00], # {
108
+ [0x00, 0x00, 0x7f, 0x00, 0x00], # |
109
+ [0x00, 0x41, 0x36, 0x08, 0x00], # }
110
+ [0x02, 0x01, 0x02, 0x04, 0x02], # ~
111
+ ].map(&:freeze).freeze
112
+
113
+ # Shown for anything outside printable ASCII: a hollow box.
114
+ MISSING = [0x7f, 0x41, 0x41, 0x41, 0x7f].freeze
115
+
116
+ # @return [Array<Integer>] the five column bytes for +char+
117
+ def self.glyph(char)
118
+ code = char.ord
119
+ return MISSING unless code.between?(FIRST, FIRST + GLYPHS.size - 1)
120
+
121
+ GLYPHS[code - FIRST]
122
+ end
123
+
124
+ # @return [Boolean] whether the pixel at column +x+, row +y+ of +char+ is set
125
+ def self.set?(char, x, y)
126
+ return false if x >= WIDTH || y >= HEIGHT
127
+
128
+ glyph(char)[x][y] == 1
129
+ end
130
+
131
+ # Text in this font for any display with #fill_rect and #blit (and a
132
+ # private #synchronize that holds its drawing lock).
133
+ module Drawing
134
+ # Draw text in the built-in 5x7 font (printable ASCII; anything else shows
135
+ # as a box). "\n" starts a new line. With bg: nil only the strokes are drawn,
136
+ # leaving what was underneath; with a background colour each character
137
+ # cell is painted in full, which overwrites old text cleanly and is faster.
138
+ # @return [Array(Integer, Integer)] width and height of what was drawn
139
+ def text(x, y, str, color: :white, bg: nil, scale: 1)
140
+ unless scale.is_a?(Integer) && scale.positive?
141
+ raise ArgumentError,
142
+ "scale must be a positive Integer, got #{scale.inspect}"
143
+ end
144
+
145
+ fg = RGB565.from(color)
146
+ bg &&= RGB565.from(bg)
147
+ lines = str.to_s.split("\n", -1)
148
+ synchronize do
149
+ lines.each_with_index do |line, i|
150
+ line_y = y + (i * Font5x7::CELL_HEIGHT * scale)
151
+ bg ? text_line_opaque(x, line_y, line, [fg, bg], scale) : text_line_transparent(x, line_y, line, fg, scale)
152
+ end
153
+ end
154
+ text_size(str, scale: scale)
155
+ end
156
+
157
+ # @return [Array(Integer, Integer)] width and height #text would draw
158
+ def text_size(str, scale: 1)
159
+ lines = str.to_s.split("\n", -1)
160
+ [lines.map(&:length).max.to_i * Font5x7::CELL_WIDTH * scale, lines.size * Font5x7::CELL_HEIGHT * scale]
161
+ end
162
+
163
+ private
164
+
165
+ # One line in full: render every cell to a buffer, then send it at once.
166
+ def text_line_opaque(x, y, line, colors, scale)
167
+ return if line.empty?
168
+
169
+ fg, bg = colors
170
+ on = [fg].pack("n") * scale
171
+ off = [bg].pack("n") * scale
172
+ rows = Array.new(Font5x7::CELL_HEIGHT) do |row|
173
+ line.each_char.map do |char|
174
+ Array.new(Font5x7::CELL_WIDTH) { |col| Font5x7.set?(char, col, row) ? on : off }.join
175
+ end.join * scale
176
+ end
177
+ w = line.length * Font5x7::CELL_WIDTH * scale
178
+ blit(x, y, w, Font5x7::CELL_HEIGHT * scale, rows.join)
179
+ end
180
+
181
+ # Strokes only: one rectangle per horizontal run of set pixels.
182
+ def text_line_transparent(x, y, line, fg, scale)
183
+ line.each_char.with_index do |char, i|
184
+ cell_x = x + (i * Font5x7::CELL_WIDTH * scale)
185
+ Font5x7::HEIGHT.times do |row|
186
+ runs(char, row).each do |start, length|
187
+ fill_rect(cell_x + (start * scale), y + (row * scale), length * scale, scale, fg)
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ # @return [Array<Array(Integer, Integer)>] [start column, length] of each run
194
+ def runs(char, row)
195
+ result = []
196
+ Font5x7::WIDTH.times do |col|
197
+ next unless Font5x7.set?(char, col, row)
198
+
199
+ if result.last && result.last.sum == col
200
+ result.last[1] += 1
201
+ else
202
+ result << [col, 1]
203
+ end
204
+ end
205
+ result
206
+ end
207
+ end
208
+ end
209
+ end