gogo_keyboard 0.0.0__py3-none-any.whl

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 @@
1
+
@@ -0,0 +1,50 @@
1
+ from contextlib import suppress
2
+ import os
3
+ import time
4
+ from typing import Optional
5
+
6
+ import zenoh
7
+
8
+
9
+ def main(topic: str = "key_press", config_path: Optional[str] = None):
10
+ if config_path is None:
11
+ conf = zenoh.Config()
12
+ else:
13
+ filepath = os.path.expanduser(config_path)
14
+ conf = zenoh.Config.from_file(filepath)
15
+
16
+ with zenoh.open(conf) as session:
17
+ print(f"Subscribing to '{topic}'")
18
+ def listener(sample: zenoh.Sample):
19
+ print(
20
+ f"Received {sample.kind} ('{sample.key_expr}': '{sample.payload.to_string()}')"
21
+ )
22
+ session.declare_subscriber(topic, listener)
23
+ while True:
24
+ time.sleep(1)
25
+
26
+
27
+ # --- Command line argument parsing --- --- --- --- --- ---
28
+ if __name__ == "__main__":
29
+ import argparse
30
+
31
+ parser = argparse.ArgumentParser(prog="z_sub", description="zenoh sub example")
32
+ parser.add_argument(
33
+ "-c",
34
+ "--config",
35
+ dest="config",
36
+ default=None,
37
+ help="Zenoh config path",
38
+ )
39
+ parser.add_argument(
40
+ "-t",
41
+ "--topic",
42
+ dest="topic",
43
+ default="key_press",
44
+ help="ROS2 topic to publish key events to",
45
+ )
46
+
47
+ args = parser.parse_args()
48
+
49
+ with suppress(KeyboardInterrupt):
50
+ main(args.topic, args.config)
gogo_keyboard/codes.py ADDED
@@ -0,0 +1,263 @@
1
+ KEY_UNKNOWN = 0
2
+ KEY_A = 4
3
+ KEY_B = 5
4
+ KEY_C = 6
5
+ KEY_D = 7
6
+ KEY_E = 8
7
+ KEY_F = 9
8
+ KEY_G = 10
9
+ KEY_H = 11
10
+ KEY_I = 12
11
+ KEY_J = 13
12
+ KEY_K = 14
13
+ KEY_L = 15
14
+ KEY_M = 16
15
+ KEY_N = 17
16
+ KEY_O = 18
17
+ KEY_P = 19
18
+ KEY_Q = 20
19
+ KEY_R = 21
20
+ KEY_S = 22
21
+ KEY_T = 23
22
+ KEY_U = 24
23
+ KEY_V = 25
24
+ KEY_W = 26
25
+ KEY_X = 27
26
+ KEY_Y = 28
27
+ KEY_Z = 29
28
+ KEY_1 = 30
29
+ KEY_2 = 31
30
+ KEY_3 = 32
31
+ KEY_4 = 33
32
+ KEY_5 = 34
33
+ KEY_6 = 35
34
+ KEY_7 = 36
35
+ KEY_8 = 37
36
+ KEY_9 = 38
37
+ KEY_0 = 39
38
+ KEY_RETURN = 40
39
+ KEY_ESCAPE = 41
40
+ KEY_BACKSPACE = 42
41
+ KEY_TAB = 43
42
+ KEY_SPACE = 44
43
+ KEY_MINUS = 45
44
+ KEY_EQUALS = 46
45
+ KEY_LEFTBRACKET = 47
46
+ KEY_RIGHTBRACKET = 48
47
+ KEY_BACKSLASH = 49
48
+ KEY_NONUSHASH = 50
49
+ KEY_SEMICOLON = 51
50
+ KEY_APOSTROPHE = 52
51
+ KEY_GRAVE = 53
52
+ KEY_COMMA = 54
53
+ KEY_PERIOD = 55
54
+ KEY_SLASH = 56
55
+ KEY_CAPSLOCK = 57
56
+ KEY_F1 = 58
57
+ KEY_F2 = 59
58
+ KEY_F3 = 60
59
+ KEY_F4 = 61
60
+ KEY_F5 = 62
61
+ KEY_F6 = 63
62
+ KEY_F7 = 64
63
+ KEY_F8 = 65
64
+ KEY_F9 = 66
65
+ KEY_F10 = 67
66
+ KEY_F11 = 68
67
+ KEY_F12 = 69
68
+ KEY_PRINTSCREEN = 70
69
+ KEY_SCROLLLOCK = 71
70
+ KEY_PAUSE = 72
71
+ KEY_INSERT = 73
72
+ KEY_HOME = 74
73
+ KEY_PAGEUP = 75
74
+ KEY_DELETE = 76
75
+ KEY_END = 77
76
+ KEY_PAGEDOWN = 78
77
+ KEY_RIGHT = 79
78
+ KEY_LEFT = 80
79
+ KEY_DOWN = 81
80
+ KEY_UP = 82
81
+ KEY_NUMLOCKCLEAR = 83
82
+ KEY_KP_DIVIDE = 84
83
+ KEY_KP_MULTIPLY = 85
84
+ KEY_KP_MINUS = 86
85
+ KEY_KP_PLUS = 87
86
+ KEY_KP_ENTER = 88
87
+ KEY_KP_1 = 89
88
+ KEY_KP_2 = 90
89
+ KEY_KP_3 = 91
90
+ KEY_KP_4 = 92
91
+ KEY_KP_5 = 93
92
+ KEY_KP_6 = 94
93
+ KEY_KP_7 = 95
94
+ KEY_KP_8 = 96
95
+ KEY_KP_9 = 97
96
+ KEY_KP_0 = 98
97
+ KEY_KP_PERIOD = 99
98
+ KEY_NONUSBACKSLASH = 100
99
+ KEY_APPLICATION = 101
100
+ KEY_POWER = 102
101
+ KEY_KP_EQUALS = 103
102
+ KEY_F13 = 104
103
+ KEY_F14 = 105
104
+ KEY_F15 = 106
105
+ KEY_F16 = 107
106
+ KEY_F17 = 108
107
+ KEY_F18 = 109
108
+ KEY_F19 = 110
109
+ KEY_F20 = 111
110
+ KEY_F21 = 112
111
+ KEY_F22 = 113
112
+ KEY_F23 = 114
113
+ KEY_F24 = 115
114
+ KEY_EXECUTE = 116
115
+ KEY_HELP = 117
116
+ KEY_MENU = 118
117
+ KEY_SELECT = 119
118
+ KEY_STOP = 120
119
+ KEY_AGAIN = 121
120
+ KEY_UNDO = 122
121
+ KEY_CUT = 123
122
+ KEY_COPY = 124
123
+ KEY_PASTE = 125
124
+ KEY_FIND = 126
125
+ KEY_MUTE = 127
126
+ KEY_VOLUMEUP = 128
127
+ KEY_VOLUMEDOWN = 129
128
+ KEY_KP_COMMA = 133
129
+ KEY_KP_EQUALSAS400 = 134
130
+ KEY_INTERNATIONAL1 = 135
131
+ KEY_INTERNATIONAL2 = 136
132
+ KEY_INTERNATIONAL3 = 137
133
+ KEY_INTERNATIONAL4 = 138
134
+ KEY_INTERNATIONAL5 = 139
135
+ KEY_INTERNATIONAL6 = 140
136
+ KEY_INTERNATIONAL7 = 141
137
+ KEY_INTERNATIONAL8 = 142
138
+ KEY_INTERNATIONAL9 = 143
139
+ KEY_LANG1 = 144
140
+ KEY_LANG2 = 145
141
+ KEY_LANG3 = 146
142
+ KEY_LANG4 = 147
143
+ KEY_LANG5 = 148
144
+ KEY_LANG6 = 149
145
+ KEY_LANG7 = 150
146
+ KEY_LANG8 = 151
147
+ KEY_LANG9 = 152
148
+ KEY_ALTERASE = 153
149
+ KEY_SYSREQ = 154
150
+ KEY_CANCEL = 155
151
+ KEY_CLEAR = 156
152
+ KEY_PRIOR = 157
153
+ KEY_RETURN2 = 158
154
+ KEY_SEPARATOR = 159
155
+ KEY_OUT = 160
156
+ KEY_OPER = 161
157
+ KEY_CLEARAGAIN = 162
158
+ KEY_CRSEL = 163
159
+ KEY_EXSEL = 164
160
+ KEY_KP_00 = 176
161
+ KEY_KP_000 = 177
162
+ KEY_THOUSANDSSEPARATOR = 178
163
+ KEY_DECIMALSEPARATOR = 179
164
+ KEY_CURRENCYUNIT = 180
165
+ KEY_CURRENCYSUBUNIT = 181
166
+ KEY_KP_LEFTPAREN = 182
167
+ KEY_KP_RIGHTPAREN = 183
168
+ KEY_KP_LEFTBRACE = 184
169
+ KEY_KP_RIGHTBRACE = 185
170
+ KEY_KP_TAB = 186
171
+ KEY_KP_BACKSPACE = 187
172
+ KEY_KP_A = 188
173
+ KEY_KP_B = 189
174
+ KEY_KP_C = 190
175
+ KEY_KP_D = 191
176
+ KEY_KP_E = 192
177
+ KEY_KP_F = 193
178
+ KEY_KP_XOR = 194
179
+ KEY_KP_POWER = 195
180
+ KEY_KP_PERCENT = 196
181
+ KEY_KP_LESS = 197
182
+ KEY_KP_GREATER = 198
183
+ KEY_KP_AMPERSAND = 199
184
+ KEY_KP_DBLAMPERSAND = 200
185
+ KEY_KP_VERTICALBAR = 201
186
+ KEY_KP_DBLVERTICALBAR = 202
187
+ KEY_KP_COLON = 203
188
+ KEY_KP_HASH = 204
189
+ KEY_KP_SPACE = 205
190
+ KEY_KP_AT = 206
191
+ KEY_KP_EXCLAM = 207
192
+ KEY_KP_MEMSTORE = 208
193
+ KEY_KP_MEMRECALL = 209
194
+ KEY_KP_MEMCLEAR = 210
195
+ KEY_KP_MEMADD = 211
196
+ KEY_KP_MEMSUBTRACT = 212
197
+ KEY_KP_MEMMULTIPLY = 213
198
+ KEY_KP_MEMDIVIDE = 214
199
+ KEY_KP_PLUSMINUS = 215
200
+ KEY_KP_CLEAR = 216
201
+ KEY_KP_CLEARENTRY = 217
202
+ KEY_KP_BINARY = 218
203
+ KEY_KP_OCTAL = 219
204
+ KEY_KP_DECIMAL = 220
205
+ KEY_KP_HEXADECIMAL = 221
206
+ KEY_LCTRL = 224
207
+ KEY_LSHIFT = 225
208
+ KEY_LALT = 226
209
+ KEY_LGUI = 227
210
+ KEY_RCTRL = 228
211
+ KEY_RSHIFT = 229
212
+ KEY_RALT = 230
213
+ KEY_RGUI = 231
214
+ KEY_MODE = 257
215
+ KEY_AUDIONEXT = 258
216
+ KEY_AUDIOPREV = 259
217
+ KEY_AUDIOSTOP = 260
218
+ KEY_AUDIOPLAY = 261
219
+ KEY_AUDIOMUTE = 262
220
+ KEY_MEDIASELECT = 263
221
+ KEY_WWW = 264
222
+ KEY_MAIL = 265
223
+ KEY_CALCULATOR = 266
224
+ KEY_COMPUTER = 267
225
+ KEY_AC_SEARCH = 268
226
+ KEY_AC_HOME = 269
227
+ KEY_AC_BACK = 270
228
+ KEY_AC_FORWARD = 271
229
+ KEY_AC_STOP = 272
230
+ KEY_AC_REFRESH = 273
231
+ KEY_AC_BOOKMARKS = 274
232
+ KEY_BRIGHTNESSDOWN = 275
233
+ KEY_BRIGHTNESSUP = 276
234
+ KEY_DISPLAYSWITCH = 277
235
+ KEY_KBDILLUMTOGGLE = 278
236
+ KEY_KBDILLUMDOWN = 279
237
+ KEY_KBDILLUMUP = 280
238
+ KEY_EJECT = 281
239
+ KEY_SLEEP = 282
240
+ KEY_APP1 = 283
241
+ KEY_APP2 = 284
242
+ KEY_AUDIOREWIND = 285
243
+ KEY_AUDIOFASTFORWARD = 286
244
+ KEY_SOFTLEFT = 287
245
+ KEY_SOFTRIGHT = 288
246
+ KEY_CALL = 289
247
+ KEY_ENDCALL = 290
248
+
249
+ MODIFIER_NONE=0
250
+ MODIFIER_LSHIFT=1
251
+ MODIFIER_RSHIFT=2
252
+ MODIFIER_LCTRL=64
253
+ MODIFIER_RCTRL=128
254
+ MODIFIER_LALT=256
255
+ MODIFIER_RALT=512
256
+ MODIFIER_LMETA=1024
257
+ MODIFIER_RMETA=2048
258
+ MODIFIER_NUM=4096
259
+ MODIFIER_CAPS=8192
260
+ MODIFIER_MODE=16384
261
+ MODIFIER_RESERVED=32768
262
+
263
+ SDL_NUM_SCANCODES = 512
@@ -0,0 +1,60 @@
1
+ import asyncio
2
+ import json
3
+ from dataclasses import asdict
4
+
5
+ import sdl2
6
+ import sdl2.ext
7
+
8
+ from . import codes
9
+ from .keyboard import Key, KeySub
10
+
11
+
12
+ async def detect_window_closed(event: asyncio.Event):
13
+ await event.wait()
14
+ print("Gorilla window closed by user.")
15
+
16
+
17
+ async def detect_CtrlC(key_sub: KeySub):
18
+ async for k in key_sub.listen_reliable():
19
+ if k.symbol == "C" and (k.modifiers & codes.MODIFIER_LCTRL) and k.is_pressed:
20
+ print("Ctrl+C in Gorilla window by user.")
21
+ return
22
+
23
+
24
+ async def print_keys(key_sub: KeySub):
25
+ async for k in key_sub.listen_reliable():
26
+ print(json.dumps({k: str(v) for k,v in asdict(k).items()}, indent=4))
27
+
28
+
29
+ async def async_main():
30
+ window_closed_event = asyncio.Event()
31
+ key_sub = KeySub(
32
+ termination_callback=window_closed_event.set,
33
+ )
34
+
35
+ try:
36
+ print_task = asyncio.create_task(print_keys(key_sub))
37
+ close_task = asyncio.create_task(detect_window_closed(window_closed_event))
38
+ ctrl_task = asyncio.create_task(detect_CtrlC(key_sub))
39
+ await asyncio.wait(
40
+ [print_task, close_task, ctrl_task], return_when=asyncio.FIRST_COMPLETED
41
+ )
42
+ print_task.cancel()
43
+ close_task.cancel()
44
+ ctrl_task.cancel()
45
+ finally:
46
+ key_sub.close()
47
+
48
+
49
+ def main():
50
+ try:
51
+ asyncio.run(async_main())
52
+ except KeyboardInterrupt:
53
+ print("KeyboardInterrupt in python process")
54
+ finally:
55
+ sdl2.ext.quit()
56
+ print("Exited cleanly :)")
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
Binary file
Binary file
Binary file
@@ -0,0 +1,184 @@
1
+ import asyncio
2
+ import colorsys
3
+ import copy
4
+ import dataclasses
5
+ import random
6
+ from importlib.resources import files
7
+ from typing import Any, Callable, Dict, Tuple
8
+
9
+ import asyncio_for_robotics
10
+ import sdl2
11
+ import sdl2.ext
12
+ from asyncio_for_robotics.core.sub import BaseSub
13
+
14
+
15
+ def scancode_to_color(scancode: int) -> Tuple[int, int, int]:
16
+ if scancode == 0:
17
+ return 255, 0, 0
18
+ brightness = 255
19
+ r, g, b = colorsys.hsv_to_rgb((scancode % 30) / 30, 0.5, 1)
20
+ return int(r * brightness), int(g * brightness), int(b * brightness)
21
+
22
+
23
+ @dataclasses.dataclass(frozen=True)
24
+ class Key:
25
+ symbol: str
26
+ code: int
27
+ modifiers: int
28
+ is_pressed: bool
29
+ sdl_event: sdl2.SDL_KeyboardEvent
30
+
31
+ @classmethod
32
+ def from_sdl(cls, sdl_event: sdl2.SDL_KeyboardEvent) -> "Key":
33
+ return cls(
34
+ symbol=sdl2.SDL_GetKeyName(sdl_event.keysym.sym).decode(),
35
+ code=sdl_event.keysym.scancode,
36
+ modifiers=sdl_event.keysym.mod,
37
+ is_pressed=bool(sdl_event.state),
38
+ sdl_event=sdl_event,
39
+ )
40
+
41
+
42
+ def raise_keyboard_interupt():
43
+ raise KeyboardInterrupt
44
+
45
+
46
+ class KeySub(BaseSub[Key]):
47
+ def __init__(
48
+ self,
49
+ termination_callback: Callable[[], Any] = raise_keyboard_interupt,
50
+ ) -> None:
51
+ """Creates a sdl2 window with an asyncio_for_robotics subcriber getting
52
+ the key presses inputed in the window.
53
+
54
+ Args:
55
+ termination_callback: Will be called when the Gorilla window is
56
+ closed by the user.
57
+ """
58
+ self.termination_callback: Callable[[], Any] = termination_callback
59
+ r, g, b = colorsys.hsv_to_rgb(random.random(), (random.random() + 1) / 2, 1)
60
+ self.idle_color: Tuple[int, int, int] = int(r * 255), int(g * 255), int(b * 255)
61
+
62
+ self._pressed_keys: Dict[int, Key] = dict()
63
+ self._surface_icon = sdl2.ext.load_img(
64
+ str(files("gogo_keyboard").joinpath("icons/gogo.png"))
65
+ )
66
+ super().__init__()
67
+ self.window: sdl2.ext.Window
68
+ self.renderer: sdl2.ext.Renderer
69
+ self._init_sdl()
70
+ self._sdl_thread: asyncio.Task = asyncio.create_task(self._sdl_loop())
71
+
72
+ self.texture_idle = sdl2.SDL_CreateTextureFromSurface(
73
+ self.renderer.sdlrenderer,
74
+ sdl2.ext.load_img(
75
+ str(files("gogo_keyboard").joinpath("icons/gogo.png")),
76
+ ),
77
+ )
78
+ self.texture_loop = [
79
+ sdl2.SDL_CreateTextureFromSurface(
80
+ self.renderer.sdlrenderer,
81
+ sdl2.ext.load_img(
82
+ str(files("gogo_keyboard").joinpath("icons/gogo_happy.png"))
83
+ ),
84
+ ),
85
+ sdl2.SDL_CreateTextureFromSurface(
86
+ self.renderer.sdlrenderer,
87
+ sdl2.ext.load_img(
88
+ str(files("gogo_keyboard").joinpath("icons/gogo_happy2.png"))
89
+ ),
90
+ ),
91
+ ]
92
+ self.tex_ind: int = 0
93
+ self._draw()
94
+ self.renderer.present()
95
+
96
+ @property
97
+ def pressed_keys(self) -> Dict[int, Key]:
98
+ return copy.deepcopy(self._pressed_keys)
99
+
100
+ @property
101
+ def name(self) -> str:
102
+ return "Gorillinput"
103
+
104
+ def close(self):
105
+ self.window.close()
106
+ sdl2.ext.quit()
107
+ self._sdl_thread.cancel()
108
+
109
+ def _scancode_to_color(self, scancode):
110
+ return scancode_to_color(scancode)
111
+
112
+ def _init_sdl(self):
113
+ sdl2.ext.init()
114
+ self.window = sdl2.ext.Window(
115
+ "Input",
116
+ size=(150, 150),
117
+ # flags=sdl2.SDL_WINDOW_RESIZABLE, # icon disapears if used
118
+ )
119
+ sdl2.SDL_SetWindowIcon(self.window.window, self._surface_icon)
120
+ self.renderer = sdl2.ext.Renderer(self.window)
121
+ self.window.show()
122
+ self.texture_frame = sdl2.SDL_Rect(0, 0, 150, 150) # x, y, width, height
123
+
124
+ def _on_window_close(self):
125
+ self.close()
126
+ self.termination_callback()
127
+
128
+ def _draw(self):
129
+ self.renderer.color = (
130
+ self._scancode_to_color(list(self._pressed_keys.keys())[-1])
131
+ if len(self._pressed_keys) > 0
132
+ else self.idle_color
133
+ )
134
+ self.renderer.clear()
135
+ if len(self._pressed_keys) > 0 and self.texture_loop != []:
136
+ self.tex_ind = (self.tex_ind + 1) % len(self.texture_loop)
137
+ sdl2.SDL_RenderCopy(
138
+ self.renderer.sdlrenderer,
139
+ self.texture_loop[self.tex_ind],
140
+ None,
141
+ self.texture_frame,
142
+ )
143
+ else:
144
+ sdl2.SDL_RenderCopy(
145
+ self.renderer.sdlrenderer, self.texture_idle, None, self.texture_frame
146
+ )
147
+
148
+ async def _sdl_loop(self):
149
+ async for t in asyncio_for_robotics.Rate(30).listen():
150
+ events = sdl2.ext.get_events()
151
+ for e in events:
152
+ if e.type == sdl2.SDL_QUIT:
153
+
154
+ self._on_window_close()
155
+ return
156
+
157
+ elif e.type == sdl2.SDL_KEYDOWN:
158
+ if e.key.repeat:
159
+ continue
160
+ k = Key.from_sdl(e.key)
161
+ self.input_data(k)
162
+ self._pressed_keys[k.code] = k
163
+
164
+ elif e.type == sdl2.SDL_KEYUP:
165
+ k = Key.from_sdl(e.key)
166
+ self.input_data(k)
167
+ del self._pressed_keys[k.code]
168
+
169
+ elif e.type in [
170
+ sdl2.SDL_WINDOWEVENT,
171
+ ]:
172
+ sdl2.SDL_SetWindowIcon(self.window.window, self._surface_icon)
173
+ self.window.show()
174
+ if e.window.event in [
175
+ sdl2.SDL_WINDOWEVENT_SIZE_CHANGED,
176
+ sdl2.SDL_WINDOWEVENT_RESIZED,
177
+ ]:
178
+ pass # continues to update the window to new size
179
+ else:
180
+ continue # does nothing
181
+ else:
182
+ continue # does nothing
183
+ self._draw()
184
+ self.renderer.present()
@@ -0,0 +1,9 @@
1
+ import asyncio
2
+ from gogo_keyboard.keyboard import KeySub
3
+
4
+ async def async_main():
5
+ key_sub = KeySub()
6
+ async for key in key_sub.listen_reliable():
7
+ print(key)
8
+
9
+ asyncio.run(async_main())
gogo_keyboard/py.typed ADDED
File without changes
@@ -0,0 +1,75 @@
1
+ """ROS2 node that publishes keyboard events as JSON-encoded std_msgs/String messages.
2
+
3
+ Run using:
4
+ - python3 -m gogo_keyboard.ros_node
5
+ - python3 -m gogo_keyboard.ros_node -t my_topic
6
+ - python3 -m gogo_keyboard.ros_node --topic my_topic
7
+ """
8
+ import argparse
9
+ import asyncio
10
+ import json
11
+ from contextlib import suppress
12
+ from dataclasses import asdict
13
+
14
+ import rclpy
15
+ from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile, ReliabilityPolicy
16
+ from std_msgs.msg import String
17
+
18
+ from gogo_keyboard.keyboard import Key, KeySub
19
+
20
+
21
+ def make_ros_msg(key: Key) -> String:
22
+ """Convert a Key object into a JSON-encoded ROS String message."""
23
+ dict_key = asdict(key)
24
+ del dict_key["sdl_event"]
25
+ return String(data=json.dumps(dict_key))
26
+
27
+
28
+ async def async_main(topic: str = "key_press"):
29
+ """Run the async keyboard listener and publish key events to a ROS2 topic.
30
+
31
+ Args:
32
+ topic: ROS2 topic to publish key events to
33
+ """
34
+ rclpy.init()
35
+ node = rclpy.create_node("gogo_keyboard")
36
+ pub = node.create_publisher(
37
+ String,
38
+ topic,
39
+ QoSProfile( # no message lost (hopefuly)
40
+ reliability=ReliabilityPolicy.RELIABLE,
41
+ history=HistoryPolicy.KEEP_ALL,
42
+ durability=DurabilityPolicy.VOLATILE,
43
+ ),
44
+ )
45
+ key_sub = KeySub()
46
+ print(
47
+ f"🦍🦍_keyboard publishing onto `{node.resolve_topic_name(pub.topic)}`. \nListen using `ros2 topic echo {node.resolve_topic_name(pub.topic)}`"
48
+ )
49
+ try:
50
+ async for key in key_sub.listen_reliable():
51
+ msg = make_ros_msg(key)
52
+ pub.publish(msg)
53
+ finally:
54
+ print(f"gogo_keyboard exiting.")
55
+ key_sub.close()
56
+ rclpy.shutdown()
57
+
58
+
59
+ def main():
60
+ parser = argparse.ArgumentParser()
61
+ parser.add_argument(
62
+ "-t",
63
+ "--topic",
64
+ default="key_press",
65
+ help="ROS2 topic to publish key events to",
66
+ )
67
+ args = parser.parse_args()
68
+ with suppress(
69
+ asyncio.CancelledError, KeyboardInterrupt, rclpy._rclpy_pybind11.RCLError
70
+ ):
71
+ asyncio.run(async_main(args.topic))
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
@@ -0,0 +1,75 @@
1
+ """Zenoh publisher that publishes keyboard events as JSON-encoded messages.
2
+
3
+ Run using:
4
+ - python3 -m gogo_keyboard.zenoh_node
5
+ """
6
+
7
+ import argparse
8
+ import asyncio
9
+ import json
10
+ import os
11
+ from contextlib import suppress
12
+ from dataclasses import asdict
13
+ from typing import Optional
14
+
15
+ import zenoh
16
+
17
+ from gogo_keyboard.keyboard import Key, KeySub
18
+
19
+
20
+ def make_msg(key: Key) -> str:
21
+ """Convert a Key object into a JSON-encoded message."""
22
+ dict_key = asdict(key)
23
+ del dict_key["sdl_event"]
24
+ return json.dumps(dict_key)
25
+
26
+
27
+ async def async_main(topic: str = "key_press", config_path: Optional[str] = None):
28
+ """Run the async keyboard listener and publish key events to a zenoh key_expr.
29
+
30
+ Args:
31
+ topic: key_expr to publish key events to
32
+ """
33
+ if config_path is None:
34
+ config = zenoh.Config()
35
+ else:
36
+ filepath = os.path.expanduser(config_path)
37
+ config = zenoh.Config.from_file(filepath)
38
+ with zenoh.open(config) as ses:
39
+ pub = ses.declare_publisher(topic, reliability=zenoh.Reliability.RELIABLE)
40
+ key_sub = KeySub()
41
+ print(
42
+ f"🦍🦍_keyboard publishing onto `{pub.key_expr}`. \nListen using `python3 -m gogo_keyboard._zenoh_simple_listener`"
43
+ )
44
+ try:
45
+ async for key in key_sub.listen_reliable():
46
+ msg = make_msg(key)
47
+ pub.put(msg)
48
+ finally:
49
+ print(f"gogo_keyboard exiting.")
50
+ key_sub.close()
51
+
52
+
53
+ def main():
54
+ parser = argparse.ArgumentParser()
55
+ parser.add_argument(
56
+ "-c",
57
+ "--config",
58
+ dest="config",
59
+ default=None,
60
+ help="Zenoh config path",
61
+ )
62
+ parser.add_argument(
63
+ "-t",
64
+ "--topic",
65
+ dest="topic",
66
+ default="key_press",
67
+ help="ROS2 topic to publish key events to",
68
+ )
69
+ args = parser.parse_args()
70
+ with suppress(asyncio.CancelledError, KeyboardInterrupt):
71
+ asyncio.run(async_main(args.topic, args.config))
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: gogo_keyboard
3
+ Version: 0.0.0
4
+ Summary: Press keyboard 🦍 Get key 🦍 Python Asyncio for Robotics
5
+ Author-email: Elian NEPPEL <elian.dev@posteo.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/2lian/gogo_keyboard
8
+ Project-URL: Repository, https://github.com/2lian/gogo_keyboard
9
+ Project-URL: Issues, https://github.com/2lian/gogo_keyboard/issues
10
+ Keywords: asyncio,robotics,ros2,zenoh,publisher,subscriber
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Natural Language :: English
15
+ Classifier: Environment :: Console
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Operating System :: POSIX :: Linux
23
+ Classifier: Operating System :: Microsoft :: Windows
24
+ Classifier: Operating System :: MacOS
25
+ Classifier: Topic :: Scientific/Engineering
26
+ Classifier: Framework :: AsyncIO
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: asyncio_for_robotics>=1.0.0
31
+ Requires-Dist: pysdl2>=0.9.0
32
+ Provides-Extra: dll
33
+ Requires-Dist: pysdl2-dll; extra == "dll"
34
+ Provides-Extra: dev
35
+ Requires-Dist: colorama; extra == "dev"
36
+ Requires-Dist: pytest; extra == "dev"
37
+ Requires-Dist: pytest-asyncio; extra == "dev"
38
+ Provides-Extra: build
39
+ Requires-Dist: build; extra == "build"
40
+ Requires-Dist: twine; extra == "build"
41
+ Dynamic: license-file
42
+
43
+ # Gogo Keyboard
44
+ ## Press keyboard 🦍 Get key 🦍 Unga Bunga
45
+
46
+ | Requirements | Compatibility |
47
+ |---|---|
48
+ | [![python](https://img.shields.io/pypi/pyversions/asyncio_for_robotics?logo=python&logoColor=white&label=Python&color=%20blue)](https://pypi.org/project/asyncio_for_robotics/)<br>![sdl2](https://img.shields.io/badge/sdl-sdl2__images-%20blue?link=https%3A%2F%2Fwww.libsdl.org%2F)<br>[![mit](https://img.shields.io/badge/License-MIT-gold)](https://opensource.org/license/mit)| ![asyncio](https://img.shields.io/badge/Framework-Asyncio-blue?logo=python&logoColor=white) <br>[![ROS 2](https://img.shields.io/badge/ROS_2-Humble%20%7C%20Jazzy%20%7C%20Kilted-blue?logo=ros)](https://github.com/ros2) <br>[![zenoh](https://img.shields.io/badge/Zenoh-%3E%3D1.0-blue)](https://zenoh.io/) |
49
+
50
+ Python Asyncio library to simply get keyboard presses and releases. Gogo Keyboard creates a new independent SDL2 window that captures the key events.
51
+
52
+ ```python3
53
+ pip install https://github.com/2lian/gogo_keyboard.git[dll]
54
+ python3 -m gogo_keyboard.example
55
+ ```
56
+
57
+ Motivation:
58
+ - Keyboard presses and releases in Asyncio.
59
+ - Only when clicking on the Gorilla window.
60
+ - Python terminal is free for other tasks.
61
+ - Based on [`asyncio_for_robotics`](https://github.com/2lian/asyncio-for-robotics) for seamless compatibility with:
62
+ - ROS 2
63
+ - Zenoh
64
+ - More
65
+
66
+ | ![python](./media/Screenshot1.png) | ![python](./media/Screenshot2.png) | ![python](./media/Screenshot3.png) |
67
+ |---|---|---|
68
+
69
+ ## Installation
70
+
71
+ This library requires `sdl2` and `sdl2_image`. By specifying the `[dll]` optional dependency, those will be installed by pip.
72
+
73
+ ```python3
74
+ pip install gogo_keyboard[dll]
75
+ ```
76
+
77
+ Conda pacakge: soon!
78
+
79
+ ## Python Example
80
+
81
+ Example is [provided here](./src/gogo_keyboard/example.py) and can be run with `python3 -m gogo_keyboard.example`.
82
+
83
+ Here is a minimal piece of working code:
84
+
85
+ ```python
86
+ import asyncio
87
+ from gogo_keyboard.keyboard import KeySub
88
+
89
+ async def async_main():
90
+ key_sub = KeySub()
91
+ async for key in key_sub.listen_reliable():
92
+ print(key)
93
+
94
+ asyncio.run(async_main())
95
+ ```
96
+
97
+ ## ROS 2 Example (Humble, Jazzy, Kilted)
98
+
99
+ A very simple ROS 2 node is [provided here](./src/gogo_keyboard/ros_node.py), run it with `python3 -m gogo_keyboard.ros_node`. The messages format is a `json` formatted `String`, 🦍 simple 🦍 Unga Bunga.
100
+
101
+ ## Zenoh Example
102
+
103
+ A very simple Zenoh publisher is [provided here](./src/gogo_keyboard/zenoh_node.py), run it with `python3 -m gogo_keyboard.zenoh_node`. The messages format is a `json` formatted `String`, 🦍 simple 🦍 Unga Bunga.
@@ -0,0 +1,17 @@
1
+ gogo_keyboard/__init__.py,sha256=4W8VliAYUP1KY2gLJ_YDy2TmcXYVm-PY7XikQD_bFwA,2
2
+ gogo_keyboard/_zenoh_simple_listener.py,sha256=ALLACoGxLugo7ICrEkZwaH_xsynmZ_XIyQyxoS1TcHY,1303
3
+ gogo_keyboard/codes.py,sha256=GDGXKh6D0wPVsrFM8UglDkCrjH7ayBki484X_MhDQJs,4465
4
+ gogo_keyboard/example.py,sha256=m8KyUvp1Bz6avF3nQwI23bwvyfvSWZPNzH19V9UUwqk,1539
5
+ gogo_keyboard/keyboard.py,sha256=Fj0FjPNW2rD3-hde5-q-6srCvO3suGd_PP9ml0Ezm4A,6048
6
+ gogo_keyboard/mini_example.py,sha256=1zWGsAVTrHgPwUQYGV4anN9O7x4nriOvUbagO2SC7LM,199
7
+ gogo_keyboard/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ gogo_keyboard/ros_node.py,sha256=6_x3QAmEvsxYwBTxgXZzc6etdRjl2jxkpCpQXiXR_5U,2133
9
+ gogo_keyboard/zenoh_node.py,sha256=DTk014KQGkwFlNcnzRPYdESjqBEMybPHvUVYPs4-pCo,2011
10
+ gogo_keyboard/icons/gogo.png,sha256=Csa0zE_0SNgSGvBqBKAyzJ8LT-abBTCldKFrUgL7Z08,918
11
+ gogo_keyboard/icons/gogo_happy.png,sha256=LywojJg3pYiTtdLJhFPHehhqgQl3V5GBX64Mvnkgkxo,940
12
+ gogo_keyboard/icons/gogo_happy2.png,sha256=uNG6elJSZAkSDSW4RRTjgCZWajJnZZ3IpI53jmmFkLQ,1113
13
+ gogo_keyboard-0.0.0.dist-info/licenses/LICENSE,sha256=mLZSGzYp72Ys0WWGIGWxPamHm1loDM6dDtG-4nQOI-0,1069
14
+ gogo_keyboard-0.0.0.dist-info/METADATA,sha256=Jb3awk5-ZvKXcXKJjV8y3TgG3fseD75C_FH_5exByZ4,4238
15
+ gogo_keyboard-0.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
16
+ gogo_keyboard-0.0.0.dist-info/top_level.txt,sha256=RC5VsKo6kb70BmF7AIkFPB7mguExOHb5QAMMZNlxEk0,14
17
+ gogo_keyboard-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Elian NEPPEL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ gogo_keyboard