aboutsummaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/c_parse.py2
-rw-r--r--lib/python/qmk/cli/__init__.py1
-rw-r--r--lib/python/qmk/cli/console.py302
-rw-r--r--lib/python/qmk/commands.py1
-rw-r--r--lib/python/qmk/constants.py2
-rw-r--r--lib/python/qmk/tests/test_cli_commands.py4
6 files changed, 308 insertions, 4 deletions
diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py
index d4f39c883..991373d56 100644
--- a/lib/python/qmk/c_parse.py
+++ b/lib/python/qmk/c_parse.py
@@ -8,7 +8,7 @@ from milc import cli
8from qmk.comment_remover import comment_remover 8from qmk.comment_remover import comment_remover
9 9
10default_key_entry = {'x': -1, 'y': 0, 'w': 1} 10default_key_entry = {'x': -1, 'y': 0, 'w': 1}
11single_comment_regex = re.compile(r' */[/*].*$') 11single_comment_regex = re.compile(r'\s+/[/*].*$')
12multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE) 12multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE)
13 13
14 14
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 008e57f76..e7a5d5cd8 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -12,6 +12,7 @@ from . import chibios
12from . import clean 12from . import clean
13from . import compile 13from . import compile
14from . import config 14from . import config
15from . import console
15from . import docs 16from . import docs
16from . import doctor 17from . import doctor
17from . import fileformat 18from . import fileformat
diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py
new file mode 100644
index 000000000..45ff0c8be
--- /dev/null
+++ b/lib/python/qmk/cli/console.py
@@ -0,0 +1,302 @@
1"""Acquire debugging information from usb hid devices
2
3cli implementation of https://www.pjrc.com/teensy/hid_listen.html
4"""
5from pathlib import Path
6from threading import Thread
7from time import sleep, strftime
8
9import hid
10import usb.core
11
12from milc import cli
13
14LOG_COLOR = {
15 'next': 0,
16 'colors': [
17 '{fg_blue}',
18 '{fg_cyan}',
19 '{fg_green}',
20 '{fg_magenta}',
21 '{fg_red}',
22 '{fg_yellow}',
23 ],
24}
25
26KNOWN_BOOTLOADERS = {
27 # VID , PID
28 ('03EB', '2FEF'): 'atmel-dfu: ATmega16U2',
29 ('03EB', '2FF0'): 'atmel-dfu: ATmega32U2',
30 ('03EB', '2FF3'): 'atmel-dfu: ATmega16U4',
31 ('03EB', '2FF4'): 'atmel-dfu: ATmega32U4',
32 ('03EB', '2FF9'): 'atmel-dfu: AT90USB64',
33 ('03EB', '2FFA'): 'atmel-dfu: AT90USB162',
34 ('03EB', '2FFB'): 'atmel-dfu: AT90USB128',
35 ('03EB', '6124'): 'Microchip SAM-BA',
36 ('0483', 'DF11'): 'stm32-dfu: STM32 BOOTLOADER',
37 ('16C0', '05DC'): 'USBasp: USBaspLoader',
38 ('16C0', '05DF'): 'bootloadHID: HIDBoot',
39 ('16C0', '0478'): 'halfkay: Teensy Halfkay',
40 ('1B4F', '9203'): 'caterina: Pro Micro 3.3V',
41 ('1B4F', '9205'): 'caterina: Pro Micro 5V',
42 ('1B4F', '9207'): 'caterina: LilyPadUSB',
43 ('1C11', 'B007'): 'kiibohd: Kiibohd DFU Bootloader',
44 ('1EAF', '0003'): 'stm32duino: Maple 003',
45 ('1FFB', '0101'): 'caterina: Polou A-Star 32U4 Bootloader',
46 ('2341', '0036'): 'caterina: Arduino Leonardo',
47 ('2341', '0037'): 'caterina: Arduino Micro',
48 ('239A', '000C'): 'caterina: Adafruit Feather 32U4',
49 ('239A', '000D'): 'caterina: Adafruit ItsyBitsy 32U4 3v',
50 ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',
51 ('239A', '000E'): 'caterina: Adafruit ItsyBitsy 32U4 5v',
52 ('2A03', '0036'): 'caterina: Arduino Leonardo',
53 ('2A03', '0037'): 'caterina: Arduino Micro',
54 ('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode'
55}
56
57
58class MonitorDevice(object):
59 def __init__(self, hid_device, numeric):
60 self.hid_device = hid_device
61 self.numeric = numeric
62 self.device = hid.Device(path=hid_device['path'])
63 self.current_line = ''
64
65 cli.log.info('Console Connected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', hid_device)
66
67 def read(self, size, encoding='ascii', timeout=1):
68 """Read size bytes from the device.
69 """
70 return self.device.read(size, timeout).decode(encoding)
71
72 def read_line(self):
73 """Read from the device's console until we get a \n.
74 """
75 while '\n' not in self.current_line:
76 self.current_line += self.read(32).replace('\x00', '')
77
78 lines = self.current_line.split('\n', 1)
79 self.current_line = lines[1]
80
81 return lines[0]
82
83 def run_forever(self):
84 while True:
85 try:
86 message = {**self.hid_device, 'text': self.read_line()}
87 identifier = (int2hex(message['vendor_id']), int2hex(message['product_id'])) if self.numeric else (message['manufacturer_string'], message['product_string'])
88 message['identifier'] = ':'.join(identifier)
89 message['ts'] = '{style_dim}{fg_green}%s{style_reset_all} ' % (strftime(cli.config.general.datetime_fmt),) if cli.args.timestamp else ''
90
91 cli.echo('%(ts)s%(color)s%(identifier)s:%(index)d{style_reset_all}: %(text)s' % message)
92
93 except hid.HIDException:
94 break
95
96
97class FindDevices(object):
98 def __init__(self, vid, pid, index, numeric):
99 self.vid = vid
100 self.pid = pid
101 self.index = index
102 self.numeric = numeric
103
104 def run_forever(self):
105 """Process messages from our queue in a loop.
106 """
107 live_devices = {}
108 live_bootloaders = {}
109
110 while True:
111 try:
112 for device in list(live_devices):
113 if not live_devices[device]['thread'].is_alive():
114 cli.log.info('Console Disconnected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', live_devices[device])
115 del live_devices[device]
116
117 for device in self.find_devices():
118 if device['path'] not in live_devices:
119 device['color'] = LOG_COLOR['colors'][LOG_COLOR['next']]
120 LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors'])
121 live_devices[device['path']] = device
122
123 try:
124 monitor = MonitorDevice(device, self.numeric)
125 device['thread'] = Thread(target=monitor.run_forever, daemon=True)
126
127 device['thread'].start()
128 except Exception as e:
129 device['e'] = e
130 device['e_name'] = e.__class__.__name__
131 cli.log.error("Could not connect to %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s:%(vendor_id)04X:%(product_id)04X:%(index)d): %(e_name)s: %(e)s", device)
132 if cli.config.general.verbose:
133 cli.log.exception(e)
134 del live_devices[device['path']]
135
136 if cli.args.bootloaders:
137 for device in self.find_bootloaders():
138 if device.address in live_bootloaders:
139 live_bootloaders[device.address]._qmk_found = True
140 else:
141 name = KNOWN_BOOTLOADERS[(int2hex(device.idVendor), int2hex(device.idProduct))]
142 cli.log.info('Bootloader Connected: {style_bright}{fg_magenta}%s', name)
143 device._qmk_found = True
144 live_bootloaders[device.address] = device
145
146 for device in list(live_bootloaders):
147 if live_bootloaders[device]._qmk_found:
148 live_bootloaders[device]._qmk_found = False
149 else:
150 name = KNOWN_BOOTLOADERS[(int2hex(live_bootloaders[device].idVendor), int2hex(live_bootloaders[device].idProduct))]
151 cli.log.info('Bootloader Disconnected: {style_bright}{fg_magenta}%s', name)
152 del live_bootloaders[device]
153
154 sleep(.1)
155
156 except KeyboardInterrupt:
157 break
158
159 def is_bootloader(self, hid_device):
160 """Returns true if the device in question matches a known bootloader vid/pid.
161 """
162 return (int2hex(hid_device.idVendor), int2hex(hid_device.idProduct)) in KNOWN_BOOTLOADERS
163
164 def is_console_hid(self, hid_device):
165 """Returns true when the usage page indicates it's a teensy-style console.
166 """
167 return hid_device['usage_page'] == 0xFF31 and hid_device['usage'] == 0x0074
168
169 def is_filtered_device(self, hid_device):
170 """Returns True if the device should be included in the list of available consoles.
171 """
172 return int2hex(hid_device['vendor_id']) == self.vid and int2hex(hid_device['product_id']) == self.pid
173
174 def find_devices_by_report(self, hid_devices):
175 """Returns a list of available teensy-style consoles by doing a brute-force search.
176
177 Some versions of linux don't report usage and usage_page. In that case we fallback to reading the report (possibly inaccurately) ourselves.
178 """
179 devices = []
180
181 for device in hid_devices:
182 path = device['path'].decode('utf-8')
183
184 if path.startswith('/dev/hidraw'):
185 number = path[11:]
186 report = Path(f'/sys/class/hidraw/hidraw{number}/device/report_descriptor')
187
188 if report.exists():
189 rp = report.read_bytes()
190
191 if rp[1] == 0x31 and rp[3] == 0x09:
192 devices.append(device)
193
194 return devices
195
196 def find_bootloaders(self):
197 """Returns a list of available bootloader devices.
198 """
199 return list(filter(self.is_bootloader, usb.core.find(find_all=True)))
200
201 def find_devices(self):
202 """Returns a list of available teensy-style consoles.
203 """
204 hid_devices = hid.enumerate()
205 devices = list(filter(self.is_console_hid, hid_devices))
206
207 if not devices:
208 devices = self.find_devices_by_report(hid_devices)
209
210 if self.vid and self.pid:
211 devices = list(filter(self.is_filtered_device, devices))
212
213 # Add index numbers
214 device_index = {}
215 for device in devices:
216 id = ':'.join((int2hex(device['vendor_id']), int2hex(device['product_id'])))
217
218 if id not in device_index:
219 device_index[id] = 0
220
221 device_index[id] += 1
222 device['index'] = device_index[id]
223
224 return devices
225
226
227def int2hex(number):
228 """Returns a string representation of the number as hex.
229 """
230 return "%04X" % number
231
232
233def list_devices(device_finder):
234 """Show the user a nicely formatted list of devices.
235 """
236 devices = device_finder.find_devices()
237
238 if devices:
239 cli.log.info('Available devices:')
240 for dev in devices:
241 color = LOG_COLOR['colors'][LOG_COLOR['next']]
242 LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors'])
243 cli.log.info("\t%s%s:%s:%d{style_reset_all}\t%s %s", color, int2hex(dev['vendor_id']), int2hex(dev['product_id']), dev['index'], dev['manufacturer_string'], dev['product_string'])
244
245 if cli.args.bootloaders:
246 bootloaders = device_finder.find_bootloaders()
247
248 if bootloaders:
249 cli.log.info('Available Bootloaders:')
250
251 for dev in bootloaders:
252 cli.log.info("\t%s:%s\t%s", int2hex(dev.idVendor), int2hex(dev.idProduct), KNOWN_BOOTLOADERS[(int2hex(dev.idVendor), int2hex(dev.idProduct))])
253
254
255@cli.argument('--bootloaders', arg_only=True, default=True, action='store_boolean', help='displaying bootloaders.')
256@cli.argument('-d', '--device', help='Device to select - uses format <pid>:<vid>[:<index>].')
257@cli.argument('-l', '--list', arg_only=True, action='store_true', help='List available hid_listen devices.')
258@cli.argument('-n', '--numeric', arg_only=True, action='store_true', help='Show VID/PID instead of names.')
259@cli.argument('-t', '--timestamp', arg_only=True, action='store_true', help='Print the timestamp for received messages as well.')
260@cli.argument('-w', '--wait', type=int, default=1, help="How many seconds to wait between checks (Default: 1)")
261@cli.subcommand('Acquire debugging information from usb hid devices.', hidden=False if cli.config.user.developer else True)
262def console(cli):
263 """Acquire debugging information from usb hid devices
264 """
265 vid = None
266 pid = None
267 index = 1
268
269 if cli.config.console.device:
270 device = cli.config.console.device.split(':')
271
272 if len(device) == 2:
273 vid, pid = device
274
275 elif len(device) == 3:
276 vid, pid, index = device
277
278 if not index.isdigit():
279 cli.log.error('Device index must be a number! Got "%s" instead.', index)
280 exit(1)
281
282 index = int(index)
283
284 if index < 1:
285 cli.log.error('Device index must be greater than 0! Got %s', index)
286 exit(1)
287
288 else:
289 cli.log.error('Invalid format for device, expected "<pid>:<vid>[:<index>]" but got "%s".', cli.config.console.device)
290 cli.print_help()
291 exit(1)
292
293 vid = vid.upper()
294 pid = pid.upper()
295
296 device_finder = FindDevices(vid, pid, index, cli.args.numeric)
297
298 if cli.args.list:
299 return list_devices(device_finder)
300
301 print('Looking for devices...', flush=True)
302 device_finder.run_forever()
diff --git a/lib/python/qmk/commands.py b/lib/python/qmk/commands.py
index 8c3f95ea2..99461db32 100644
--- a/lib/python/qmk/commands.py
+++ b/lib/python/qmk/commands.py
@@ -203,6 +203,7 @@ def compile_configurator_json(user_keymap, bootloader=None, parallel=1, **env_va
203 f'VERBOSE={verbose}', 203 f'VERBOSE={verbose}',
204 f'COLOR={color}', 204 f'COLOR={color}',
205 'SILENT=false', 205 'SILENT=false',
206 f'QMK_BIN={"bin/qmk" if "DEPRECATED_BIN_QMK" in os.environ else "qmk"}',
206 ]) 207 ])
207 208
208 return make_command 209 return make_command
diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py
index 3ed69f3bf..49e5e0eb4 100644
--- a/lib/python/qmk/constants.py
+++ b/lib/python/qmk/constants.py
@@ -10,7 +10,7 @@ QMK_FIRMWARE = Path.cwd()
10MAX_KEYBOARD_SUBFOLDERS = 5 10MAX_KEYBOARD_SUBFOLDERS = 5
11 11
12# Supported processor types 12# Supported processor types
13CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32G431', 'STM32G474' 13CHIBIOS_PROCESSORS = 'cortex-m0', 'cortex-m0plus', 'cortex-m3', 'cortex-m4', 'MKL26Z64', 'MK20DX128', 'MK20DX256', 'MK66F18', 'STM32F042', 'STM32F072', 'STM32F103', 'STM32F303', 'STM32F401', 'STM32F411', 'STM32F446', 'STM32G431', 'STM32G474', 'STM32L433', 'STM32L443'
14LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None 14LUFA_PROCESSORS = 'at90usb162', 'atmega16u2', 'atmega32u2', 'atmega16u4', 'atmega32u4', 'at90usb646', 'at90usb647', 'at90usb1286', 'at90usb1287', None
15VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85' 15VUSB_PROCESSORS = 'atmega32a', 'atmega328p', 'atmega328', 'attiny85'
16 16
diff --git a/lib/python/qmk/tests/test_cli_commands.py b/lib/python/qmk/tests/test_cli_commands.py
index 741551e5e..d1f3a1811 100644
--- a/lib/python/qmk/tests/test_cli_commands.py
+++ b/lib/python/qmk/tests/test_cli_commands.py
@@ -8,7 +8,7 @@ is_windows = 'windows' in platform.platform().lower()
8 8
9 9
10def check_subcommand(command, *args): 10def check_subcommand(command, *args):
11 cmd = ['bin/qmk', command, *args] 11 cmd = ['qmk', command, *args]
12 result = run(cmd, stdout=PIPE, stderr=STDOUT, universal_newlines=True) 12 result = run(cmd, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
13 return result 13 return result
14 14
@@ -17,7 +17,7 @@ def check_subcommand_stdin(file_to_read, command, *args):
17 """Pipe content of a file to a command and return output. 17 """Pipe content of a file to a command and return output.
18 """ 18 """
19 with open(file_to_read, encoding='utf-8') as my_file: 19 with open(file_to_read, encoding='utf-8') as my_file:
20 cmd = ['bin/qmk', command, *args] 20 cmd = ['qmk', command, *args]
21 result = run(cmd, stdin=my_file, stdout=PIPE, stderr=STDOUT, universal_newlines=True) 21 result = run(cmd, stdin=my_file, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
22 return result 22 return result
23 23