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,294 @@
1
+ module Rgpio
2
+ # A resistive touch panel read through an XPT2046 (or ADS7843/TSC2046)
3
+ # controller over SPI — the touch half of the common 2.4"/2.8" ILI9341
4
+ # modules, on the same bus as the display with a chip select of its own.
5
+ #
6
+ # touch = Rgpio::XPT2046.new(irq: 17, calibration: [...])
7
+ # touch.when_touched { |x, y| puts "#{x}, #{y}" }
8
+ # touch.when_released { puts "released" }
9
+ # Rgpio.pause
10
+ #
11
+ # #raw returns the converter's 12-bit readings. Turning those into screen
12
+ # coordinates needs a calibration, since the film's resistance and how it is
13
+ # mounted vary from panel to panel: {.calibration_from} fits one from a few
14
+ # touches on known screen points. With no calibration, #position is the raw
15
+ # [x, y].
16
+ #
17
+ # T_IRQ (PENIRQ) goes low while the panel is pressed. It is optional: with
18
+ # it, the watcher thread sleeps on a kernel edge event until a touch; without
19
+ # it, the watcher samples the pressure every POLL_INTERVAL.
20
+ class XPT2046
21
+ DEFAULT_SPEED_HZ = 2_000_000
22
+
23
+ # Control byte: start bit, channel (A2..A0), 12-bit mode, differential
24
+ # reference, and power-down between conversions with PENIRQ enabled.
25
+ CMD_X = 0xd0
26
+ CMD_Y = 0x90
27
+ CMD_Z1 = 0xb0
28
+ CMD_Z2 = 0xc0
29
+
30
+ RESOLUTION = 4096
31
+
32
+ # Pressure (on the scale of #raw's z) below which the panel counts as not
33
+ # touched. Lower it for a light touch, raise it if the panel reports touches
34
+ # nobody made.
35
+ DEFAULT_THRESHOLD = 300
36
+
37
+ # Readings per coordinate; the median of them is used, which rejects the
38
+ # odd sample taken while a finger is landing or lifting.
39
+ SAMPLES = 5
40
+
41
+ # How often a held touch is re-read to notice its release, and how often
42
+ # the pressure is polled with no T_IRQ line.
43
+ POLL_INTERVAL = 0.02
44
+
45
+ # How long a single edge wait blocks before the watcher rechecks whether it
46
+ # has been asked to stop.
47
+ WATCH_TIMEOUT = 0.2
48
+
49
+ # Fit a calibration from touches on known screen points: a least-squares
50
+ # affine map, so a panel mounted rotated or mirrored needs nothing extra.
51
+ # Three points not on one line is the minimum; the four corners and the
52
+ # centre average out the error of each touch.
53
+ # @param screen [Array<Array(Numeric, Numeric)>] screen [x, y] of each target
54
+ # @param raw [Array<Array(Numeric, Numeric)>] raw [x, y] touched there
55
+ # @return [Array<Float>] [a, b, c, d, e, f] with x = a*rx + b*ry + c and
56
+ # y = d*rx + e*ry + f
57
+ def self.calibration_from(screen, raw)
58
+ raise ArgumentError, "calibration needs the same number of screen and raw points" unless screen.size == raw.size
59
+ raise ArgumentError, "calibration needs at least three points" if screen.size < 3
60
+
61
+ rows = raw.map { |rx, ry| [rx.to_f, ry.to_f, 1.0] }
62
+ solve_least_squares(rows, screen.map { |sx, _| sx.to_f }) +
63
+ solve_least_squares(rows, screen.map { |_, sy| sy.to_f })
64
+ end
65
+
66
+ # Solve rows * coef ~= targets for three coefficients through the normal
67
+ # equations.
68
+ def self.solve_least_squares(rows, targets)
69
+ matrix = normal_equations(rows, targets)
70
+ 3.times do |col|
71
+ pivot = (col...3).max_by { |row| matrix[row][col].abs }
72
+ raise ArgumentError, "calibration points must not all lie on one line" if matrix[pivot][col].abs < 1e-9
73
+
74
+ matrix[col], matrix[pivot] = matrix[pivot], matrix[col]
75
+ (0...3).each do |row|
76
+ next if row == col
77
+
78
+ factor = matrix[row][col] / matrix[col][col]
79
+ 4.times { |k| matrix[row][k] -= factor * matrix[col][k] }
80
+ end
81
+ end
82
+ Array.new(3) { |i| matrix[i][3] / matrix[i][i] }
83
+ end
84
+
85
+ # @return [Array<Array<Float>>] the augmented 3x4 matrix [AᵀA | Aᵀt]
86
+ def self.normal_equations(rows, targets)
87
+ Array.new(3) do |i|
88
+ Array.new(3) { |j| rows.sum { |r| r[i] * r[j] } } + [rows.each_with_index.sum { |r, k| r[i] * targets[k] }]
89
+ end
90
+ end
91
+ private_class_method :solve_least_squares, :normal_equations
92
+
93
+ # @param irq [Integer, nil] GPIO line wired to T_IRQ, or nil to poll
94
+ # @param calibration [Array<Numeric>, nil] six numbers from {.calibration_from}
95
+ # @param threshold [Integer] pressure that counts as a touch
96
+ # @param bus [Integer] spidev bus number
97
+ # @param device [Integer] chip-select index; CE1 when the display is on CE0
98
+ # @param speed_hz [Integer] clock rate
99
+ # @param spi [SPI, nil] an open bus device to share, or nil to open one
100
+ # @param chip [Chip, nil] chip for the T_IRQ line, or nil to open one
101
+ # @param consumer [String] name shown in the kernel's request list
102
+ def initialize(irq: nil, calibration: nil, threshold: DEFAULT_THRESHOLD, bus: 0, device: 1,
103
+ speed_hz: DEFAULT_SPEED_HZ, spi: nil, chip: nil, consumer: "rgpio")
104
+ self.calibration = calibration
105
+ @threshold = threshold
106
+ @callbacks = {}
107
+ @closed = false
108
+ @lock = Mutex.new
109
+ @owns_spi = spi.nil?
110
+ @spi = spi || SPI.new(bus: bus, device: device, speed_hz: speed_hz, mode: 0)
111
+ @irq = irq
112
+ return unless irq
113
+
114
+ @owns_chip = chip.nil?
115
+ @chip = chip || Chip.new
116
+ # PENIRQ is open drain; the boards have no pull-up of their own.
117
+ @irq_request = @chip.request_lines(offsets: [irq], direction: :input, edge: :falling, bias: :pull_up,
118
+ consumer: consumer)
119
+ end
120
+
121
+ # @return [SPI] the bus device readings go through
122
+ attr_reader :spi
123
+
124
+ # @return [Array<Float>, nil] the calibration in use
125
+ attr_reader :calibration
126
+
127
+ # @return [Integer] pressure that counts as a touch
128
+ attr_accessor :threshold
129
+
130
+ # @param coefficients [Array<Numeric>, nil] six numbers from {.calibration_from}
131
+ def calibration=(coefficients)
132
+ unless coefficients.nil? || (coefficients.is_a?(Array) && coefficients.size == 6 && coefficients.all?(Numeric))
133
+ raise ArgumentError, "calibration is six numbers from XPT2046.calibration_from, got #{coefficients.inspect}"
134
+ end
135
+
136
+ @calibration = coefficients&.map(&:to_f)&.freeze
137
+ end
138
+
139
+ # @return [Array(Integer, Integer, Integer)] raw x, y (0..4095) and
140
+ # pressure z (0 when untouched, larger the harder the press)
141
+ def raw
142
+ raise Error, "#{self.class} is closed" if @closed
143
+
144
+ sample
145
+ end
146
+
147
+ # @return [Boolean] whether the panel is pressed at least #threshold hard
148
+ def touched?
149
+ raw[2] >= @threshold
150
+ end
151
+
152
+ # @return [Array(Integer, Integer), nil] the touch in screen coordinates,
153
+ # or nil when untouched
154
+ def position
155
+ locate(raw)
156
+ end
157
+
158
+ # @yieldparam x [Integer] screen x of the touch
159
+ # @yieldparam y [Integer] screen y of the touch
160
+ def when_touched(&)
161
+ on(:touched, &)
162
+ end
163
+
164
+ def when_released(&)
165
+ on(:released, &)
166
+ end
167
+
168
+ # Stop the watcher, then release the T_IRQ line, the bus and the chip as
169
+ # far as this object opened them.
170
+ def close
171
+ return if @closed
172
+
173
+ @closed = true
174
+ @watching = false
175
+ @watcher&.join
176
+ @watcher = nil
177
+ @irq_request&.release
178
+ @spi.close if @owns_spi
179
+ @chip.close if @owns_chip
180
+ end
181
+
182
+ def closed?
183
+ @closed
184
+ end
185
+
186
+ private
187
+
188
+ # #raw without the closed check. The watcher reads through this: #close
189
+ # marks the device closed before it waits for the watcher, and the bus
190
+ # stays open until that wait is over, so the watcher's last poll is safe.
191
+ def sample
192
+ @lock.synchronize do
193
+ # Pressure is read on both sides of the position and the lower one
194
+ # kept: a pen landing or lifting part-way through leaves x/y unsettled
195
+ # (or at the untouched 0/4095), and one of the two pressures reads low.
196
+ z_before = read_pressure
197
+ # The first conversion after the drivers switch on is still settling.
198
+ read_channel(CMD_X)
199
+ x = median(SAMPLES) { read_channel(CMD_X) }
200
+ y = median(SAMPLES) { read_channel(CMD_Y) }
201
+ [x, y, [z_before, read_pressure].min]
202
+ end
203
+ end
204
+
205
+ # @return [Array(Integer, Integer), nil] screen position of a reading, or
206
+ # nil when it is not pressed hard enough
207
+ def locate(reading)
208
+ x, y, z = reading
209
+ z >= @threshold ? to_screen(x, y) : nil
210
+ end
211
+
212
+ # The 12-bit result arrives MSB first after the command byte, left-aligned
213
+ # with three trailing zero bits.
214
+ def read_channel(cmd)
215
+ received = @spi.transfer([cmd, 0x00, 0x00])
216
+ ((received[1] << 8) | received[2]) >> 3
217
+ end
218
+
219
+ def read_pressure
220
+ read_channel(CMD_Z1) + (RESOLUTION - 1) - read_channel(CMD_Z2)
221
+ end
222
+
223
+ def median(count, &)
224
+ Array.new(count, &).sort[count / 2]
225
+ end
226
+
227
+ def to_screen(x, y)
228
+ return [x, y] unless @calibration
229
+
230
+ a, b, c, d, e, f = @calibration
231
+ [((a * x) + (b * y) + c).round, ((d * x) + (e * y) + f).round]
232
+ end
233
+
234
+ def on(kind, &block)
235
+ raise ArgumentError, "a callback block is required" unless block
236
+
237
+ @callbacks[kind] = block
238
+ start_watching
239
+ self
240
+ end
241
+
242
+ def start_watching
243
+ return if @watcher
244
+
245
+ @watching = true
246
+ @watcher = Thread.new { watch_loop }
247
+ end
248
+
249
+ def watch_loop
250
+ while @watching
251
+ touch = wait_for_touch
252
+ follow_touch(*touch) if touch
253
+ end
254
+ rescue StandardError => e
255
+ warn "rgpio: touch watcher stopped: #{e.class}: #{e.message}"
256
+ end
257
+
258
+ # @return [Array(Integer, Integer), nil] where the panel is pressed, once it is
259
+ def wait_for_touch
260
+ if @irq_request
261
+ return nil if @irq_request.read_edge_events(timeout: WATCH_TIMEOUT).empty?
262
+ else
263
+ sleep(POLL_INTERVAL)
264
+ end
265
+ locate(sample)
266
+ end
267
+
268
+ # Report a touch, then poll until it lifts. Conversions make PENIRQ drop
269
+ # again while the pen is down, so the edges queued meanwhile are discarded
270
+ # rather than read as new touches.
271
+ def follow_touch(x, y)
272
+ dispatch(:touched, x, y)
273
+ sleep(POLL_INTERVAL) while @watching && sample[2] >= @threshold
274
+ dispatch(:released)
275
+ drain_edges
276
+ end
277
+
278
+ def drain_edges
279
+ return unless @irq_request
280
+
281
+ nil until @irq_request.read_edge_events(timeout: 0).empty?
282
+ end
283
+
284
+ # A raising callback must not take the watcher down with it.
285
+ def dispatch(kind, *)
286
+ callback = @callbacks[kind]
287
+ return unless callback
288
+
289
+ callback.call(*)
290
+ rescue StandardError => e
291
+ warn "rgpio: #{self.class} #{kind} callback raised #{e.class}: #{e.message}"
292
+ end
293
+ end
294
+ end
data/lib/rgpio/i2c.rb CHANGED
@@ -17,7 +17,7 @@ module Rgpio
17
17
  # i2c.close
18
18
  #
19
19
  # The I2C bus on the 40-pin header (GPIO2 = SDA, GPIO3 = SCL) is bus 1, and
20
- # only appears once it is enabled — see README for the dtparam line.
20
+ # only appears once it is enabled — see docs/guide.md for the dtparam line.
21
21
  class I2C
22
22
  # ioctl numbers from <linux/i2c-dev.h>.
23
23
  I2C_SLAVE = 0x0703
data/lib/rgpio/pwm.rb CHANGED
@@ -8,7 +8,7 @@ module Rgpio
8
8
  # -----------------------------
9
9
  # The RP1 PWM peripheral must be enabled via dtoverlay in
10
10
  # /boot/firmware/config.txt before the chip appears in sysfs.
11
- # See README.md for the required overlay configuration.
11
+ # See docs/guide.md for the required overlay configuration.
12
12
  #
13
13
  # GPIO-to-PWM mapping on Pi 5 (RP1):
14
14
  # GPIO12 → RP1 PWM chip, channel 0
@@ -34,6 +34,10 @@ module Rgpio
34
34
  class HardwarePWM
35
35
  PWM_SYSFS_ROOT = "/sys/class/pwm".freeze
36
36
 
37
+ # The overlay setup, linked from chip-detection errors. The gem ships no
38
+ # docs of its own, so the messages point at the guide on GitHub.
39
+ GUIDE_URL = "https://github.com/lumbermill/rgpio/blob/main/docs/guide.md#hardware-pwm-usage".freeze
40
+
37
41
  # Device-tree model string, used to pick the board's PWM mapping.
38
42
  BOARD_MODEL_PATH = "/proc/device-tree/model".freeze
39
43
 
@@ -251,7 +255,7 @@ module Rgpio
251
255
  chips = self.class.available_chips
252
256
  if chips.empty?
253
257
  raise PWMError, "No PWM chips found under #{PWM_SYSFS_ROOT}. " \
254
- "Is the dtoverlay configured? See README.md."
258
+ "Is the dtoverlay configured? See #{GUIDE_URL}"
255
259
  end
256
260
 
257
261
  profile = PWM_CHIP_PROFILE[@board]
@@ -273,7 +277,7 @@ module Rgpio
273
277
  chip_list = chips.map { |c| "pwmchip#{c[:chip]}(npwm=#{c[:npwm]})" }.join(", ")
274
278
  raise PWMError,
275
279
  "Cannot auto-detect the header PWM chip for board #{@board.inspect}. " \
276
- "Available: #{chip_list}. Pass chip: <number> explicitly (see README.md)."
280
+ "Available: #{chip_list}. Pass chip: <number> explicitly (see #{GUIDE_URL})."
277
281
  end
278
282
 
279
283
  def export_channel
data/lib/rgpio/spi.rb CHANGED
@@ -16,7 +16,7 @@ module Rgpio
16
16
  # {#write} and {#read} are that same transfer with one direction ignored.
17
17
  #
18
18
  # The header bus is SPI0 (GPIO10 = MOSI, GPIO9 = MISO, GPIO11 = SCLK, GPIO8 =
19
- # CE0, GPIO7 = CE1), and only appears once it is enabled — see README for the
19
+ # CE0, GPIO7 = CE1), and only appears once it is enabled — see docs/guide.md for the
20
20
  # dtparam line.
21
21
  class SPI
22
22
  # ioctl numbers from <linux/spi/spidev.h>. They are built here rather than
@@ -39,6 +39,22 @@ module Rgpio
39
39
  # Clock polarity and phase, as the kernel numbers them.
40
40
  MODE_RANGE = (0..3)
41
41
 
42
+ # Where spidev publishes the most it accepts in one message, and what that
43
+ # is when the parameter cannot be read.
44
+ BUFSIZ_PATH = "/sys/module/spidev/parameters/bufsiz".freeze
45
+ DEFAULT_BUFSIZ = 4096
46
+
47
+ # @return [Integer] the largest message spidev accepts, in bytes. A module
48
+ # parameter (spidev.bufsiz=), so it is the same for every bus.
49
+ def self.max_transfer_size
50
+ @max_transfer_size ||= begin
51
+ size = File.read(BUFSIZ_PATH).to_i
52
+ size.positive? ? size : DEFAULT_BUFSIZ
53
+ rescue SystemCallError
54
+ DEFAULT_BUFSIZ
55
+ end
56
+ end
57
+
42
58
  # @return [Integer] the ioctl request for a message of `count` transfers
43
59
  def self.message_ioctl(count)
44
60
  ioctl_number(IOC_WRITE, 0, TRANSFER_SIZE * count)
@@ -178,6 +194,28 @@ module Rgpio
178
194
  transfer(bytes).size
179
195
  end
180
196
 
197
+ # Send a block of any length, ignoring what comes back — for a display's
198
+ # pixel data, where a frame is many times what spidev takes in one message.
199
+ # The block goes out in messages of at most {.max_transfer_size} bytes, so
200
+ # chip select is released between them; a controller that keys on chip
201
+ # select rather than on a command (the ILI9341 does not) needs #transfer.
202
+ # Nothing is unpacked on the way back, which keeps a frame cheap.
203
+ # @param data [String] bytes to send
204
+ # @return [Integer] number of bytes sent
205
+ def send_bytes(data)
206
+ data = data.b
207
+ chunk = self.class.max_transfer_size
208
+ @tx_buffer ||= Fiddle::Pointer.malloc(chunk, Fiddle::RUBY_FREE)
209
+ (0...data.bytesize).step(chunk) do |offset|
210
+ part = data.byteslice(offset, chunk)
211
+ @tx_buffer[0, part.bytesize] = part
212
+ message = self.class.pack_transfer(@tx_buffer.to_i, 0, part.bytesize, @speed_hz,
213
+ bits_per_word: @bits_per_word)
214
+ @io.ioctl(self.class.message_ioctl(1), message)
215
+ end
216
+ data.bytesize
217
+ end
218
+
181
219
  # Clock in `count` bytes, sending zeros.
182
220
  # @return [Array<Integer>]
183
221
  def read(count)
data/lib/rgpio/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rgpio
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "0.1.1".freeze
3
3
  end
data/lib/rgpio.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "rgpio/spi"
10
10
  require_relative "rgpio/devices/device"
11
11
  require_relative "rgpio/devices/output_device"
12
12
  require_relative "rgpio/devices/input_device"
13
+ require_relative "rgpio/devices/rotary_encoder"
13
14
  require_relative "rgpio/devices/motor"
14
15
  require_relative "rgpio/devices/pwm_channel"
15
16
  require_relative "rgpio/devices/pwm_output_device"
@@ -18,6 +19,10 @@ require_relative "rgpio/devices/servo"
18
19
  require_relative "rgpio/devices/adt7410"
19
20
  require_relative "rgpio/devices/st7032"
20
21
  require_relative "rgpio/devices/mcp3208"
22
+ require_relative "rgpio/devices/rgb565"
23
+ require_relative "rgpio/devices/font5x7"
24
+ require_relative "rgpio/devices/ili9341"
25
+ require_relative "rgpio/devices/xpt2046"
21
26
 
22
27
  # Ruby bindings for libgpiod v2 (Linux GPIO character device), bound through
23
28
  # the stdlib `fiddle`. Targets Debian Trixie (libgpiod >= 2.1) on Raspberry Pi.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rgpio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ITO Yosei
@@ -70,10 +70,10 @@ description: 'Control a Raspberry Pi''s hardware from Ruby. GPIO goes through li
70
70
  interface. PWM is either the hardware peripheral through sysfs or timed in Ruby
71
71
  on any line; I2C and SPI are ioctl calls on /dev/i2c-N and /dev/spidev, needing
72
72
  no libgpiod at all. On top sits a gpiozero-style device layer: LED, Button, Motor,
73
- PWMLED, RGBLED, Servo, and drivers for the ADT7410 temperature sensor, ST7032 character
74
- LCD and MCP3208 ADC. Bound through the stdlib `fiddle`, which is built with the
75
- interpreter and so matches any Pi''s architecture, unlike the precompiled `ffi`
76
- gem.'
73
+ PWMLED, RGBLED, Servo, RotaryEncoder, and drivers for the ADT7410 temperature sensor,
74
+ ST7032 character LCD, MCP3208 ADC, ILI9341 colour TFT and XPT2046 touch panel. Bound
75
+ through the stdlib `fiddle`, which is built with the interpreter and so matches
76
+ any Pi''s architecture, unlike the precompiled `ffi` gem.'
77
77
  email:
78
78
  - y-itou@lumber-mill.co.jp
79
79
  executables: []
@@ -82,7 +82,6 @@ extra_rdoc_files: []
82
82
  files:
83
83
  - CHANGELOG.md
84
84
  - LICENSE
85
- - PLAN.md
86
85
  - README.md
87
86
  - examples/adc.rb
88
87
  - examples/adc_led.rb
@@ -100,22 +99,32 @@ files:
100
99
  - examples/pwm_led.rb
101
100
  - examples/rgb_balance.rb
102
101
  - examples/rgb_led.rb
102
+ - examples/rotary_encoder.rb
103
+ - examples/ruby_stack.rb
103
104
  - examples/servo.rb
104
105
  - examples/temperature.rb
106
+ - examples/tft.rb
107
+ - examples/touch_calibration.rb
108
+ - examples/touch_paint.rb
105
109
  - lib/rgpio.rb
106
110
  - lib/rgpio/bytes.rb
107
111
  - lib/rgpio/chip.rb
108
112
  - lib/rgpio/devices/adt7410.rb
109
113
  - lib/rgpio/devices/device.rb
114
+ - lib/rgpio/devices/font5x7.rb
115
+ - lib/rgpio/devices/ili9341.rb
110
116
  - lib/rgpio/devices/input_device.rb
111
117
  - lib/rgpio/devices/mcp3208.rb
112
118
  - lib/rgpio/devices/motor.rb
113
119
  - lib/rgpio/devices/output_device.rb
114
120
  - lib/rgpio/devices/pwm_channel.rb
115
121
  - lib/rgpio/devices/pwm_output_device.rb
122
+ - lib/rgpio/devices/rgb565.rb
116
123
  - lib/rgpio/devices/rgb_led.rb
124
+ - lib/rgpio/devices/rotary_encoder.rb
117
125
  - lib/rgpio/devices/servo.rb
118
126
  - lib/rgpio/devices/st7032.rb
127
+ - lib/rgpio/devices/xpt2046.rb
119
128
  - lib/rgpio/i2c.rb
120
129
  - lib/rgpio/line_request.rb
121
130
  - lib/rgpio/native.rb
@@ -128,6 +137,7 @@ licenses:
128
137
  - MIT
129
138
  metadata:
130
139
  source_code_uri: https://github.com/lumbermill/rgpio
140
+ documentation_uri: https://github.com/lumbermill/rgpio/blob/main/docs/guide.md
131
141
  changelog_uri: https://github.com/lumbermill/rgpio/blob/main/CHANGELOG.md
132
142
  bug_tracker_uri: https://github.com/lumbermill/rgpio/issues
133
143
  rubygems_mfa_required: 'true'
@@ -145,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
155
  - !ruby/object:Gem::Version
146
156
  version: '0'
147
157
  requirements: []
148
- rubygems_version: 4.0.10
158
+ rubygems_version: 4.0.23
149
159
  specification_version: 4
150
160
  summary: GPIO, PWM, I2C and SPI on Raspberry Pi, via libgpiod v2 and the Linux character
151
161
  devices