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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -2
- data/README.md +33 -891
- data/examples/rotary_encoder.rb +48 -0
- data/examples/ruby_stack.rb +169 -0
- data/examples/tft.rb +65 -0
- data/examples/touch_calibration.rb +61 -0
- data/examples/touch_paint.rb +67 -0
- data/lib/rgpio/devices/adt7410.rb +1 -1
- data/lib/rgpio/devices/font5x7.rb +209 -0
- data/lib/rgpio/devices/ili9341.rb +338 -0
- data/lib/rgpio/devices/rgb565.rb +61 -0
- data/lib/rgpio/devices/rotary_encoder.rb +202 -0
- data/lib/rgpio/devices/xpt2046.rb +294 -0
- data/lib/rgpio/i2c.rb +1 -1
- data/lib/rgpio/pwm.rb +7 -3
- data/lib/rgpio/spi.rb +39 -1
- data/lib/rgpio/version.rb +1 -1
- data/lib/rgpio.rb +5 -0
- metadata +17 -7
- data/PLAN.md +0 -347
|
@@ -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
|
|
@@ -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
|