fusuma-plugin-remap 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +10 -3
- data/exe/fusuma-remap +74 -25
- data/exe/fusuma-touchpad-remap +42 -0
- data/fusuma-plugin-remap.gemspec +2 -2
- data/lib/fusuma/plugin/inputs/remap_keyboard_input.rb +21 -63
- data/lib/fusuma/plugin/inputs/remap_touchpad_input.rb +112 -0
- data/lib/fusuma/plugin/inputs/remap_touchpad_input.yml +4 -0
- data/lib/fusuma/plugin/remap/keyboard_remapper.rb +343 -0
- data/lib/fusuma/plugin/remap/layer_manager.rb +18 -13
- data/lib/fusuma/plugin/remap/touchpad_remapper.rb +168 -0
- data/lib/fusuma/plugin/remap/uinput_keyboard.rb +95 -0
- data/lib/fusuma/plugin/remap/uinput_touchpad.rb +172 -0
- data/lib/fusuma/plugin/remap/version.rb +1 -1
- metadata +17 -11
- data/lib/fusuma/plugin/remap/remapper.rb +0 -233
- data/lib/fusuma/plugin/remap/ruinput_device_patched.rb +0 -49
@@ -0,0 +1,343 @@
|
|
1
|
+
require "revdev"
|
2
|
+
require "msgpack"
|
3
|
+
require "set"
|
4
|
+
require_relative "layer_manager"
|
5
|
+
require_relative "uinput_keyboard"
|
6
|
+
|
7
|
+
module Fusuma
|
8
|
+
module Plugin
|
9
|
+
module Remap
|
10
|
+
class KeyboardRemapper
|
11
|
+
include Revdev
|
12
|
+
|
13
|
+
VIRTUAL_KEYBOARD_NAME = "fusuma_virtual_keyboard"
|
14
|
+
|
15
|
+
# @param layer_manager [Fusuma::Plugin::Remap::LayerManager]
|
16
|
+
# @param fusuma_writer [IO]
|
17
|
+
# @param config [Hash]
|
18
|
+
def initialize(layer_manager:, fusuma_writer:, config: {})
|
19
|
+
@layer_manager = layer_manager # request to change layer
|
20
|
+
@fusuma_writer = fusuma_writer # write event to original keyboard
|
21
|
+
@config = config
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
create_virtual_keyboard
|
26
|
+
@source_keyboards = reload_keyboards
|
27
|
+
|
28
|
+
old_ie = nil
|
29
|
+
layer = nil
|
30
|
+
next_mapping = nil
|
31
|
+
current_mapping = {}
|
32
|
+
|
33
|
+
loop do
|
34
|
+
ios = IO.select([*@source_keyboards.map(&:file), @layer_manager.reader])
|
35
|
+
io = ios.first.first
|
36
|
+
|
37
|
+
if io == @layer_manager.reader
|
38
|
+
layer = @layer_manager.receive_layer # update @current_layer
|
39
|
+
if layer.nil?
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
next_mapping = @layer_manager.find_mapping(layer)
|
44
|
+
next
|
45
|
+
end
|
46
|
+
|
47
|
+
if next_mapping && virtual_keyboard_all_key_released?
|
48
|
+
if current_mapping != next_mapping
|
49
|
+
current_mapping = next_mapping
|
50
|
+
end
|
51
|
+
next_mapping = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
input_event = @source_keyboards.find { |kbd| kbd.file == io }.read_input_event
|
55
|
+
input_key = find_key_from_code(input_event.code)
|
56
|
+
|
57
|
+
if input_event.type == EV_KEY
|
58
|
+
@emergency_stop.call(old_ie, input_event)
|
59
|
+
|
60
|
+
old_ie = input_event
|
61
|
+
if input_event.value != 2 # repeat
|
62
|
+
data = {key: input_key, status: input_event.value, layer: layer}
|
63
|
+
@fusuma_writer.write(data.to_msgpack)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
remapped = current_mapping.fetch(input_key.to_sym, nil)
|
68
|
+
if remapped.nil?
|
69
|
+
uinput_keyboard.write_input_event(input_event)
|
70
|
+
next
|
71
|
+
end
|
72
|
+
|
73
|
+
remapped_event = InputEvent.new(nil, input_event.type, find_code_from_key(remapped), input_event.value)
|
74
|
+
|
75
|
+
# Workaround to solve the problem that the remapped key remains pressed
|
76
|
+
# when the key pressed before remapping is released after remapping
|
77
|
+
unless record_virtual_keyboard_event?(remapped, remapped_event.value)
|
78
|
+
# set original key before remapping
|
79
|
+
remapped_event.code = input_event.code
|
80
|
+
end
|
81
|
+
|
82
|
+
# remap to command will be nil
|
83
|
+
# e.g) remap: { X: { command: echo 'foo' } }
|
84
|
+
# this is because the command will be executed by fusuma process
|
85
|
+
next if remapped_event.code.nil?
|
86
|
+
|
87
|
+
uinput_keyboard.write_input_event(remapped_event)
|
88
|
+
rescue Errno::ENODEV => e # device is removed
|
89
|
+
MultiLogger.error "Device is removed: #{e.message}"
|
90
|
+
@source_keyboards = reload_keyboards
|
91
|
+
end
|
92
|
+
rescue EOFError => e # device is closed
|
93
|
+
MultiLogger.error "Device is closed: #{e.message}"
|
94
|
+
ensure
|
95
|
+
@destroy.call
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def reload_keyboards
|
101
|
+
source_keyboards = KeyboardSelector.new(@config[:keyboard_name_patterns]).select
|
102
|
+
|
103
|
+
MultiLogger.info("Reload keyboards: #{source_keyboards.map(&:device_name)}")
|
104
|
+
|
105
|
+
set_trap(source_keyboards)
|
106
|
+
# TODO: Extract to a configuration file or make it optional
|
107
|
+
# it should stop other remappers
|
108
|
+
set_emergency_ungrab_keybinds("RIGHTCTRL", "LEFTCTRL")
|
109
|
+
grab_keyboards(source_keyboards)
|
110
|
+
rescue => e
|
111
|
+
MultiLogger.error "Failed to reload keyboards: #{e.message}"
|
112
|
+
MultiLogger.error e.backtrace.join("\n")
|
113
|
+
end
|
114
|
+
|
115
|
+
def uinput_keyboard
|
116
|
+
@uinput_keyboard ||= UinputKeyboard.new("/dev/uinput")
|
117
|
+
end
|
118
|
+
|
119
|
+
def pressed_virtual_keys
|
120
|
+
@pressed_virtual_keys ||= Set.new
|
121
|
+
end
|
122
|
+
|
123
|
+
# record virtual keyboard event
|
124
|
+
# @param [String] remapped_value remapped key name
|
125
|
+
# @param [Integer] event_value event value
|
126
|
+
# @return [Boolean] false if the key was pressed before remapping started and was released
|
127
|
+
# @return [Boolean] true if the key was not pressed before remapping started
|
128
|
+
def record_virtual_keyboard_event?(remapped_value, event_value)
|
129
|
+
case event_value
|
130
|
+
when 0
|
131
|
+
pressed_virtual_keys.delete?(remapped_value)
|
132
|
+
when 1
|
133
|
+
pressed_virtual_keys.add?(remapped_value)
|
134
|
+
true # Always return true because the remapped key may be the same
|
135
|
+
else
|
136
|
+
# 2 is repeat
|
137
|
+
true
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def virtual_keyboard_all_key_released?
|
142
|
+
pressed_virtual_keys.empty?
|
143
|
+
end
|
144
|
+
|
145
|
+
def create_virtual_keyboard
|
146
|
+
touchpad_name_patterns = @config[:touchpad_name_patterns]
|
147
|
+
internal_touchpad = TouchpadSelector.new(touchpad_name_patterns).select.first
|
148
|
+
|
149
|
+
if internal_touchpad.nil?
|
150
|
+
MultiLogger.error("No touchpad found: #{touchpad_name_patterns}")
|
151
|
+
exit
|
152
|
+
end
|
153
|
+
|
154
|
+
MultiLogger.info "Create virtual keyboard: #{VIRTUAL_KEYBOARD_NAME}"
|
155
|
+
|
156
|
+
uinput_keyboard.create VIRTUAL_KEYBOARD_NAME,
|
157
|
+
Revdev::InputId.new(
|
158
|
+
# disable while typing is enabled when
|
159
|
+
# - Both the keyboard and touchpad are BUS_I8042
|
160
|
+
# - The touchpad and keyboard have the same vendor/product
|
161
|
+
# ref: (https://wayland.freedesktop.org/libinput/doc/latest/palm-detection.html#disable-while-typing)
|
162
|
+
#
|
163
|
+
{
|
164
|
+
bustype: Revdev::BUS_I8042,
|
165
|
+
vendor: internal_touchpad.device_id.vendor,
|
166
|
+
product: internal_touchpad.device_id.product,
|
167
|
+
version: internal_touchpad.device_id.version
|
168
|
+
}
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
172
|
+
def grab_keyboards(keyboards)
|
173
|
+
keyboards.each do |keyboard|
|
174
|
+
wait_release_all_keys(keyboard)
|
175
|
+
begin
|
176
|
+
keyboard.grab
|
177
|
+
MultiLogger.info "Grabbed #{keyboard.device_name}"
|
178
|
+
rescue Errno::EBUSY
|
179
|
+
MultiLogger.error "Failed to grab #{keyboard.device_name}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# @param [Array<Revdev::EventDevice>] keyboards
|
185
|
+
def set_trap(keyboards)
|
186
|
+
@destroy = lambda do
|
187
|
+
keyboards.each do |kbd|
|
188
|
+
kbd.ungrab
|
189
|
+
rescue Errno::EINVAL
|
190
|
+
rescue Errno::ENODEV
|
191
|
+
# already ungrabbed
|
192
|
+
end
|
193
|
+
|
194
|
+
begin
|
195
|
+
uinput_keyboard.destroy
|
196
|
+
rescue IOError
|
197
|
+
# already destroyed
|
198
|
+
end
|
199
|
+
|
200
|
+
exit 0
|
201
|
+
end
|
202
|
+
|
203
|
+
Signal.trap(:INT) { @destroy.call }
|
204
|
+
Signal.trap(:TERM) { @destroy.call }
|
205
|
+
end
|
206
|
+
|
207
|
+
# Emergency stop keybind for virtual keyboard
|
208
|
+
def set_emergency_ungrab_keybinds(first_key, second_key)
|
209
|
+
first_keycode = find_code_from_key(first_key)
|
210
|
+
second_keycode = find_code_from_key(second_key)
|
211
|
+
MultiLogger.info "Emergency ungrab keybind: #{first_key} + #{second_key}"
|
212
|
+
|
213
|
+
@emergency_stop = lambda do |prev, current|
|
214
|
+
if (prev&.code == first_keycode && prev.value != 0) && (current.code == second_keycode && current.value != 0)
|
215
|
+
MultiLogger.info "Emergency ungrab keybind: #{first_key} + #{second_key}"
|
216
|
+
@destroy.call
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Find remappable key from mapping and return remapped key code
|
222
|
+
# If not found, return original key code
|
223
|
+
# If the key is found but its value is not valid, return nil
|
224
|
+
# @example
|
225
|
+
# find_remapped_code({ "A" => "b" }, 30) # => 48
|
226
|
+
# find_remapped_code({ "A" => "b" }, 100) # => 100
|
227
|
+
# find_remapped_code({ "A" => {command: 'echo foobar'} }, 30) # => nil
|
228
|
+
#
|
229
|
+
# @param [Hash] mapping
|
230
|
+
# @param [Integer] code
|
231
|
+
# @return [Integer, nil]
|
232
|
+
def find_remapped_code(mapping, code)
|
233
|
+
key = find_key_from_code(code) # key = "A"
|
234
|
+
remapped_key = mapping.fetch(key.to_sym, nil) # remapped_key = "b"
|
235
|
+
return code unless remapped_key # return original code if key is not found
|
236
|
+
|
237
|
+
find_code_from_key(remapped_key) # remapped_code = 48
|
238
|
+
end
|
239
|
+
|
240
|
+
# Find key name from key code
|
241
|
+
# @example
|
242
|
+
# find_key_from_code(30) # => "A"
|
243
|
+
# find_key_from_code(48) # => "B"
|
244
|
+
# @param [Integer] code
|
245
|
+
# @return [String]
|
246
|
+
def find_key_from_code(code)
|
247
|
+
# { 30 => :A, 48 => :B, ... }
|
248
|
+
@keys_per_code ||= Revdev.constants.select { |c| c.start_with? "KEY_" }.map { |c| [Revdev.const_get(c), c.to_s.delete_prefix("KEY_")] }.to_h
|
249
|
+
@keys_per_code[code]
|
250
|
+
end
|
251
|
+
|
252
|
+
# Find key code from key name (e.g. "A", "B", "BTN_LEFT")
|
253
|
+
# If key name is not found, return nil
|
254
|
+
# @example
|
255
|
+
# find_code_from_key("A") # => 30
|
256
|
+
# find_code_from_key("B") # => 48
|
257
|
+
# find_code_from_key("BTN_LEFT") # => 272
|
258
|
+
# find_code_from_key("NOT_FOUND") # => nil
|
259
|
+
# @param [String] key
|
260
|
+
# @return [Integer] when key is available
|
261
|
+
# @return [nil] when key is not available
|
262
|
+
def find_code_from_key(key)
|
263
|
+
# { KEY_A => 30, KEY_B => 48, ... }
|
264
|
+
@codes_per_key ||= Revdev.constants.select { |c| c.start_with?("KEY_", "BTN_") }.map { |c| [c, Revdev.const_get(c)] }.to_h
|
265
|
+
|
266
|
+
case key
|
267
|
+
when String
|
268
|
+
if key.start_with?("BTN_")
|
269
|
+
@codes_per_key[key.upcase.to_sym]
|
270
|
+
else
|
271
|
+
@codes_per_key["KEY_#{key}".upcase.to_sym]
|
272
|
+
end
|
273
|
+
when Integer
|
274
|
+
@codes_per_key["KEY_#{key}".upcase.to_sym]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def released_all_keys?(device)
|
279
|
+
# key status if all bytes are 0, the key is not pressed
|
280
|
+
bytes = device.read_ioctl_with(Revdev::EVIOCGKEY)
|
281
|
+
bytes.unpack("C*").all? { |byte| byte == 0 }
|
282
|
+
end
|
283
|
+
|
284
|
+
def wait_release_all_keys(device, &block)
|
285
|
+
loop do
|
286
|
+
if released_all_keys?(device)
|
287
|
+
break true
|
288
|
+
else
|
289
|
+
# wait until all keys are released
|
290
|
+
device.read_input_event
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Devices to detect key presses and releases
|
296
|
+
class KeyboardSelector
|
297
|
+
def initialize(names = ["keyboard", "Keyboard", "KEYBOARD"])
|
298
|
+
@names = names
|
299
|
+
end
|
300
|
+
|
301
|
+
# Select devices that match the name
|
302
|
+
# If no device is found, it will wait for 3 seconds and try again
|
303
|
+
# @return [Array<Revdev::EventDevice>]
|
304
|
+
def select
|
305
|
+
loop do
|
306
|
+
Fusuma::Device.reset # reset cache to get the latest device information
|
307
|
+
devices = Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } }
|
308
|
+
if devices.empty?
|
309
|
+
wait_for_device
|
310
|
+
|
311
|
+
next
|
312
|
+
end
|
313
|
+
|
314
|
+
return devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") }
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def wait_for_device
|
319
|
+
sleep 3
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
class TouchpadSelector
|
324
|
+
def initialize(names = nil)
|
325
|
+
@names = names
|
326
|
+
end
|
327
|
+
|
328
|
+
# @return [Array<Revdev::EventDevice>]
|
329
|
+
def select
|
330
|
+
devices = if @names
|
331
|
+
Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } }
|
332
|
+
else
|
333
|
+
# available returns only touchpad devices
|
334
|
+
Fusuma::Device.available
|
335
|
+
end
|
336
|
+
|
337
|
+
devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") }
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
@@ -8,28 +8,33 @@ module Fusuma
|
|
8
8
|
class LayerManager
|
9
9
|
require "singleton"
|
10
10
|
include Singleton
|
11
|
-
attr_reader :reader, :writer, :current_layer
|
11
|
+
attr_reader :reader, :writer, :current_layer, :layers
|
12
12
|
|
13
13
|
def initialize
|
14
14
|
@layers = {}
|
15
15
|
@reader, @writer = IO.pipe
|
16
16
|
@current_layer = {} # preserve order
|
17
|
+
@last_layer = nil
|
18
|
+
@last_remove = nil
|
17
19
|
end
|
18
20
|
|
19
21
|
# @param [Hash] layer
|
20
22
|
# @param [Boolean] remove
|
21
23
|
def send_layer(layer:, remove: false)
|
22
|
-
|
23
|
-
return if @last_layer == layer && @last_remove == remove
|
24
|
+
return if (@last_layer == layer) && (@last_remove == remove)
|
24
25
|
|
25
26
|
@last_layer = layer
|
26
27
|
@last_remove = remove
|
27
|
-
@writer.
|
28
|
+
@writer.write({layer: layer, remove: remove}.to_msgpack)
|
28
29
|
end
|
29
30
|
|
30
|
-
# Read layer from pipe and
|
31
|
+
# Read layer from pipe and update @current_layer
|
32
|
+
# @return [Hash] current layer
|
31
33
|
# @example
|
32
|
-
#
|
34
|
+
# receive_layer
|
35
|
+
# # => { thumbsense: true }
|
36
|
+
# receive_layer
|
37
|
+
# # => { thumbsense: true, application: "Google-chrome" }
|
33
38
|
def receive_layer
|
34
39
|
@layer_unpacker ||= MessagePack::Unpacker.new(@reader)
|
35
40
|
|
@@ -41,28 +46,28 @@ module Fusuma
|
|
41
46
|
layer = data[:layer] # e.g { thumbsense: true }
|
42
47
|
remove = data[:remove] # e.g true
|
43
48
|
|
49
|
+
# update @current_layer
|
44
50
|
if remove
|
45
51
|
@current_layer.delete_if { |k, _v| layer.key?(k) }
|
46
52
|
else
|
47
53
|
# If duplicate keys exist, order of keys is preserved
|
48
54
|
@current_layer.merge!(layer)
|
49
55
|
end
|
56
|
+
@current_layer
|
50
57
|
end
|
51
58
|
|
52
59
|
# Find remap layer from config
|
53
60
|
# @param [Hash] layer
|
54
|
-
# @return [Hash]
|
55
|
-
def find_mapping(layer
|
61
|
+
# @return [Hash] remap layer
|
62
|
+
def find_mapping(layer)
|
56
63
|
@layers[layer] ||= begin
|
57
64
|
result = nil
|
58
|
-
_ = Fusuma::Config::Searcher.find_context(layer)
|
65
|
+
_ = Fusuma::Config::Searcher.find_context(layer) do
|
59
66
|
result = Fusuma::Config.search(Fusuma::Config::Index.new(:remap))
|
60
67
|
next unless result
|
61
68
|
|
62
|
-
result = result.deep_transform_keys
|
63
|
-
|
64
|
-
end
|
65
|
-
}
|
69
|
+
result = result.deep_transform_keys { |key| key.upcase.to_sym }
|
70
|
+
end
|
66
71
|
|
67
72
|
result || {}
|
68
73
|
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require "revdev"
|
2
|
+
require "msgpack"
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
require_relative "uinput_touchpad"
|
6
|
+
|
7
|
+
module Fusuma
|
8
|
+
module Plugin
|
9
|
+
module Remap
|
10
|
+
class TouchpadRemapper
|
11
|
+
include Revdev
|
12
|
+
|
13
|
+
VIRTUAL_TOUCHPAD_NAME = "fusuma_virtual_touchpad"
|
14
|
+
|
15
|
+
# @param fusuma_writer [IO]
|
16
|
+
# @param source_touchpad [Revdev::Device]
|
17
|
+
def initialize(fusuma_writer:, source_touchpad:)
|
18
|
+
@source_touchpad = source_touchpad # original touchpad
|
19
|
+
@fusuma_writer = fusuma_writer # write event to fusuma_input
|
20
|
+
@palm_detector ||= PalmDetection.new(source_touchpad)
|
21
|
+
end
|
22
|
+
|
23
|
+
# TODO: grab touchpad events and remap them
|
24
|
+
# send remapped events to virtual touchpad or virtual mouse
|
25
|
+
def run
|
26
|
+
create_virtual_touchpad
|
27
|
+
|
28
|
+
touch_state = {}
|
29
|
+
mt_slot = 0
|
30
|
+
finger_state = nil
|
31
|
+
loop do
|
32
|
+
IO.select([@source_touchpad.file]) # , @layer_manager.reader])
|
33
|
+
|
34
|
+
## example of input_event
|
35
|
+
# Event: time 1698456258.380027, type 3 (EV_ABS), code 57 (ABS_MT_TRACKING_ID), value 43679
|
36
|
+
# Event: time 1698456258.380027, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 648
|
37
|
+
# Event: time 1698456258.380027, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 209
|
38
|
+
# Event: time 1698456258.380027, type 1 (EV_KEY), code 330 (BTN_TOUCH), value 1
|
39
|
+
# Event: time 1698456258.380027, type 1 (EV_KEY), code 325 (BTN_TOOL_FINGER), value 1
|
40
|
+
# Event: time 1698456258.380027, type 3 (EV_ABS), code 0 (ABS_X), value 648
|
41
|
+
# Event: time 1698456258.380027, type 3 (EV_ABS), code 1 (ABS_Y), value 209
|
42
|
+
# Event: time 1698456258.380027, type 4 (EV_MSC), code 5 (MSC_TIMESTAMP), value 0
|
43
|
+
# Event: time 1698456258.380027, -------------- SYN_REPORT ------------
|
44
|
+
# Event: time 1698456258.382693, type 3 (EV_ABS), code 47 (ABS_MT_SLOT), value 1
|
45
|
+
# Event: time 1698456258.382693, type 3 (EV_ABS), code 57 (ABS_MT_TRACKING_ID), value 43680
|
46
|
+
# Event: time 1698456258.382693, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 400
|
47
|
+
# Event: time 1698456258.382693, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 252
|
48
|
+
# Event: time 1698456258.382693, type 1 (EV_KEY), code 325 (BTN_TOOL_FINGER), value 0
|
49
|
+
# Event: time 1698456258.382693, type 1 (EV_KEY), code 333 (BTN_TOOL_DOUBLETAP), value 1
|
50
|
+
# Event: time 1698456258.382693, type 4 (EV_MSC), code 5 (MSC_TIMESTAMP), value 7100
|
51
|
+
# Event: time 1698456258.382693, -------------- SYN_REPORT ------------
|
52
|
+
input_event = @source_touchpad.read_input_event
|
53
|
+
|
54
|
+
touch_state[mt_slot] ||= {MT_TRACKING_ID: nil, X: nil, Y: nil, valid_touch_point: false}
|
55
|
+
syn_report = nil
|
56
|
+
|
57
|
+
case input_event.type
|
58
|
+
when Revdev::EV_ABS
|
59
|
+
case input_event.code
|
60
|
+
when Revdev::ABS_MT_SLOT
|
61
|
+
mt_slot = input_event.value
|
62
|
+
touch_state[mt_slot] ||= {}
|
63
|
+
when Revdev::ABS_MT_TRACKING_ID
|
64
|
+
touch_state[mt_slot][:MT_TRACKING_ID] = input_event.value
|
65
|
+
if input_event.value == -1
|
66
|
+
touch_state[mt_slot] = {}
|
67
|
+
end
|
68
|
+
when Revdev::ABS_MT_POSITION_X
|
69
|
+
touch_state[mt_slot][:X] = input_event.value
|
70
|
+
when Revdev::ABS_MT_POSITION_Y
|
71
|
+
touch_state[mt_slot][:Y] = input_event.value
|
72
|
+
when Revdev::ABS_X, Revdev::ABS_Y
|
73
|
+
# ignore
|
74
|
+
when Revdev::ABS_MT_PRESSURE
|
75
|
+
# ignore
|
76
|
+
when Revdev::ABS_MT_TOOL_TYPE
|
77
|
+
# ignore
|
78
|
+
else
|
79
|
+
raise "unhandled event"
|
80
|
+
end
|
81
|
+
when Revdev::EV_KEY
|
82
|
+
case input_event.code
|
83
|
+
when Revdev::BTN_TOUCH
|
84
|
+
# ignore
|
85
|
+
when Revdev::BTN_TOOL_FINGER
|
86
|
+
finger_state = (input_event.value == 1) ? 1 : 0
|
87
|
+
when Revdev::BTN_TOOL_DOUBLETAP
|
88
|
+
finger_state = (input_event.value == 1) ? 2 : 1
|
89
|
+
when Revdev::BTN_TOOL_TRIPLETAP
|
90
|
+
finger_state = (input_event.value == 1) ? 3 : 2
|
91
|
+
when Revdev::BTN_TOOL_QUADTAP
|
92
|
+
finger_state = (input_event.value == 1) ? 4 : 3
|
93
|
+
when 0x148 # define BTN_TOOL_QUINTTAP 0x148 /* Five fingers on trackpad */
|
94
|
+
finger_state = (input_event.value == 1) ? 5 : 4
|
95
|
+
end
|
96
|
+
when Revdev::EV_MSC
|
97
|
+
case input_event.code
|
98
|
+
when 0x05 # define MSC_TIMESTAMP 0x05
|
99
|
+
# ignore
|
100
|
+
# current_timestamp = input_event.value
|
101
|
+
end
|
102
|
+
when Revdev::EV_SYN
|
103
|
+
case input_event.code
|
104
|
+
when Revdev::SYN_REPORT
|
105
|
+
syn_report = input_event.value
|
106
|
+
when Revdev::SYN_DROPPED
|
107
|
+
MultiLogger.error "Dropped: #{input_event.value}"
|
108
|
+
else
|
109
|
+
raise "unhandled event", "#{input_event.hr_type}, #{input_event.hr_code}, #{input_event.value}"
|
110
|
+
end
|
111
|
+
else
|
112
|
+
raise "unhandled event", "#{input_event.hr_type}, #{input_event.hr_code}, #{input_event.value}"
|
113
|
+
end
|
114
|
+
|
115
|
+
# TODO:
|
116
|
+
# Remember the most recent valid touch position and exclude it if it is close to that position
|
117
|
+
# For example, when dragging, it is possible to touch around the edge of the touchpad again after reaching the edge of the touchpad, so in that case, you do not want to execute palm detection
|
118
|
+
if touch_state[mt_slot][:valid_touch_point] != true
|
119
|
+
touch_state[mt_slot][:valid_touch_point] = @palm_detector.palm?(touch_state[mt_slot])
|
120
|
+
end
|
121
|
+
|
122
|
+
if syn_report
|
123
|
+
# TODO: define format as fusuma_input
|
124
|
+
data = {finger: finger_state, touch_state: touch_state}
|
125
|
+
@fusuma_writer.write(data.to_msgpack)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def uinput
|
133
|
+
@uinput ||= UinputTouchpad.new "/dev/uinput"
|
134
|
+
end
|
135
|
+
|
136
|
+
def create_virtual_touchpad
|
137
|
+
MultiLogger.info "Create virtual keyboard: #{VIRTUAL_TOUCHPAD_NAME}"
|
138
|
+
|
139
|
+
uinput.create_from_device(name: VIRTUAL_TOUCHPAD_NAME, device: @source_touchpad)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Detect palm touch
|
143
|
+
class PalmDetection
|
144
|
+
def initialize(touchpad)
|
145
|
+
@max_x = touchpad.absinfo_for_axis(Revdev::ABS_MT_POSITION_X)[:absmax]
|
146
|
+
@max_y = touchpad.absinfo_for_axis(Revdev::ABS_MT_POSITION_Y)[:absmax]
|
147
|
+
end
|
148
|
+
|
149
|
+
def palm?(touch_state)
|
150
|
+
return false unless touch_state[:X] && touch_state[:Y]
|
151
|
+
|
152
|
+
if 0.8 * @max_y < touch_state[:Y]
|
153
|
+
true
|
154
|
+
else
|
155
|
+
!(
|
156
|
+
# Disable 20% of the touch area on the left, right
|
157
|
+
(touch_state[:X] < 0.2 * @max_x || touch_state[:X] > 0.8 * @max_x) ||
|
158
|
+
# Disable 10% of the touch area on the top edge
|
159
|
+
(touch_state[:Y] < 0.1 * @max_y && (touch_state[:X] < 0.2 * @max_x || touch_state[:X] > 0.8 * @max_x)
|
160
|
+
)
|
161
|
+
)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "ruinput"
|
2
|
+
|
3
|
+
class UinputKeyboard < Ruinput::UinputDevice
|
4
|
+
include Ruinput
|
5
|
+
|
6
|
+
# create virtual event divece
|
7
|
+
# _name_ :: device name
|
8
|
+
# _id_ :: InputId ("struct input_id" on input.h)
|
9
|
+
def create name = DEFAULT_DEVICE_NAME, id = DEFAULT_INPUT_ID
|
10
|
+
if !name.is_a? String
|
11
|
+
raise ArgumentError, "1st arg expect String"
|
12
|
+
elsif !id.is_a? Revdev::InputId
|
13
|
+
raise ArgumentError, "2nd arg expect Revdev::InputId"
|
14
|
+
end
|
15
|
+
|
16
|
+
uud = Ruinput::UinputUserDev.new({
|
17
|
+
name: name,
|
18
|
+
id: id,
|
19
|
+
ff_effects_max: 0,
|
20
|
+
absmax: [],
|
21
|
+
absmin: [],
|
22
|
+
absfuzz: [],
|
23
|
+
absflat: []
|
24
|
+
})
|
25
|
+
|
26
|
+
@file.syswrite uud.to_byte_string
|
27
|
+
|
28
|
+
set_all_events
|
29
|
+
|
30
|
+
@file.ioctl UI_DEV_CREATE, nil
|
31
|
+
@is_created = true
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_all_events
|
35
|
+
raise "invalid method call: this uinput is already created" if @is_created
|
36
|
+
|
37
|
+
mouse_btns = [
|
38
|
+
Revdev::BTN_0,
|
39
|
+
Revdev::BTN_MISC,
|
40
|
+
Revdev::BTN_1,
|
41
|
+
Revdev::BTN_2,
|
42
|
+
Revdev::BTN_3,
|
43
|
+
Revdev::BTN_4,
|
44
|
+
Revdev::BTN_5,
|
45
|
+
Revdev::BTN_6,
|
46
|
+
Revdev::BTN_7,
|
47
|
+
Revdev::BTN_8,
|
48
|
+
Revdev::BTN_9,
|
49
|
+
Revdev::BTN_LEFT,
|
50
|
+
Revdev::BTN_MOUSE,
|
51
|
+
Revdev::BTN_MIDDLE,
|
52
|
+
Revdev::BTN_RIGHT,
|
53
|
+
Revdev::BTN_SIDE,
|
54
|
+
Revdev::BTN_EXTRA,
|
55
|
+
Revdev::BTN_FORWARD,
|
56
|
+
Revdev::BTN_BACK,
|
57
|
+
Revdev::BTN_TASK
|
58
|
+
# Revdev::BTN_TRIGGER, # libinput recognized as joystick if set
|
59
|
+
].freeze
|
60
|
+
|
61
|
+
keyboard_keys = Revdev.constants.select { |c| c.start_with? "KEY_" }.map { |c| Revdev.const_get(c) }.freeze
|
62
|
+
|
63
|
+
@file.ioctl UI_SET_EVBIT, Revdev::EV_KEY
|
64
|
+
@counter = 0
|
65
|
+
Revdev::KEY_CNT.times do |i|
|
66
|
+
# https://github.com/mooz/xkeysnail/pull/101/files
|
67
|
+
if keyboard_keys.include?(i) || mouse_btns.include?(i)
|
68
|
+
# puts "setting #{i} (#{Revdev::REVERSE_MAPS[:KEY][i]})"
|
69
|
+
@file.ioctl UI_SET_KEYBIT, i
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# @file.ioctl UI_SET_EVBIT, Revdev::EV_MSC
|
74
|
+
# Revdev::MSC_CNT.times do |i|
|
75
|
+
# @file.ioctl UI_SET_MSCBIT, i
|
76
|
+
# end
|
77
|
+
|
78
|
+
mouse_rel = [
|
79
|
+
Revdev::REL_X,
|
80
|
+
Revdev::REL_Y,
|
81
|
+
Revdev::REL_WHEEL,
|
82
|
+
Revdev::REL_HWHEEL
|
83
|
+
].freeze
|
84
|
+
|
85
|
+
@file.ioctl UI_SET_EVBIT, Revdev::EV_REL
|
86
|
+
Revdev::REL_CNT.times do |i|
|
87
|
+
if mouse_rel.include?(i)
|
88
|
+
# puts "setting #{i} (#{Revdev::REVERSE_MAPS[:REL][i]})"
|
89
|
+
@file.ioctl UI_SET_RELBIT, i
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
@file.ioctl UI_SET_EVBIT, Revdev::EV_REP
|
94
|
+
end
|
95
|
+
end
|