From 7190d7b071ec1c73f4cd2608289740a3be4a0cb0 Mon Sep 17 00:00:00 2001 From: zvecr Date: Thu, 14 May 2020 21:43:48 +0100 Subject: [PATCH 1/9] stash poc --- lib/python/qmk/cli/__init__.py | 1 + lib/python/qmk/cli/console.py | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 lib/python/qmk/cli/console.py diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index 372c40921a86..32ed15baac25 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -12,6 +12,7 @@ from . import clean from . import compile from . import config +from . import console from . import docs from . import doctor from . import fileformat diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py new file mode 100644 index 000000000000..26c7a6fca891 --- /dev/null +++ b/lib/python/qmk/cli/console.py @@ -0,0 +1,56 @@ +"""hid_listen +""" +import hid +import platform +from pathlib import Path +from time import sleep + +from milc import cli + +def patch_linux(dev): + platform_id = platform.platform().lower() + if 'linux' in platform_id: + hidraw = Path(dev['path'].decode('UTF-8')).name + descriptor_path = Path('/sys/class/hidraw/') / hidraw / 'device/report_descriptor' + + report = descriptor_path.read_bytes() + + dev['usage_page'] = (report[2] << 8) + report[1]; + dev['usage'] = report[4]; + return dev + +def is_console_hid(x): + return x['usage_page'] == 0xFF31 and x['usage'] == 0x0074 + +def search(): + return list(filter(is_console_hid, map(patch_linux, hid.enumerate()))) + +@cli.argument('-l', '--list', arg_only=True, action='store_true', help='List available hid_listen devices.') +@cli.argument('-i', '--index', default=0, type=int, help='Device index.') +@cli.subcommand('kinda hid_listen ish.') +def console(cli): + """TODO. + """ + + if cli.args.list: + cli.log.info('Available devices:') + devices = search() + for dev in devices: + cli.log.info("%02x:%02x %s %s", dev['vendor_id'], dev['product_id'], dev['manufacturer_string'], dev['product_string']) + return + + print('Waiting for device:') + + selected = None + while selected is None: + found = search() + selected = found[cli.args.index] if found[cli.args.index:] else None + + print('.', end = '', flush=True) + sleep(1) + + print() + print('Listening to %s:' % selected['path'].decode()) + device = hid.Device(path=selected['path']) + while True: + print(device.read(32).decode('ascii'), end = '') From 21b5cdfef0bdd9d71a8b02f2a6937c0c70aaf3c0 Mon Sep 17 00:00:00 2001 From: zvecr Date: Mon, 15 Feb 2021 20:21:24 +0000 Subject: [PATCH 2/9] stash --- lib/python/qmk/cli/console.py | 48 ++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py index 26c7a6fca891..cfd322e871f8 100644 --- a/lib/python/qmk/cli/console.py +++ b/lib/python/qmk/cli/console.py @@ -8,15 +8,15 @@ from milc import cli def patch_linux(dev): - platform_id = platform.platform().lower() - if 'linux' in platform_id: - hidraw = Path(dev['path'].decode('UTF-8')).name - descriptor_path = Path('/sys/class/hidraw/') / hidraw / 'device/report_descriptor' + # platform_id = platform.platform().lower() + # if 'linux' in platform_id: + # hidraw = Path(dev['path'].decode('UTF-8')).name + # descriptor_path = Path('/sys/class/hidraw/') / hidraw / 'device/report_descriptor' - report = descriptor_path.read_bytes() + # report = descriptor_path.read_bytes() - dev['usage_page'] = (report[2] << 8) + report[1]; - dev['usage'] = report[4]; + # dev['usage_page'] = (report[2] << 8) + report[1]; + # dev['usage'] = report[4]; return dev def is_console_hid(x): @@ -29,7 +29,7 @@ def search(): @cli.argument('-i', '--index', default=0, type=int, help='Device index.') @cli.subcommand('kinda hid_listen ish.') def console(cli): - """TODO. + """TODO: """ if cli.args.list: @@ -39,18 +39,26 @@ def console(cli): cli.log.info("%02x:%02x %s %s", dev['vendor_id'], dev['product_id'], dev['manufacturer_string'], dev['product_string']) return - print('Waiting for device:') + try: + print('Waiting for device:') - selected = None - while selected is None: - found = search() - selected = found[cli.args.index] if found[cli.args.index:] else None + while True: + selected = None + while selected is None: + found = search() + selected = found[cli.args.index] if found[cli.args.index:] else None + print('.', end = '', flush=True) + sleep(1) - print('.', end = '', flush=True) - sleep(1) + print() + print('Listening to %s:' % selected['path'].decode()) + device = hid.Device(path=selected['path']) + try: + while True: + print(device.read(32).decode('ascii'), end = '') - print() - print('Listening to %s:' % selected['path'].decode()) - device = hid.Device(path=selected['path']) - while True: - print(device.read(32).decode('ascii'), end = '') + except hid.HIDException: + print('Device disconnected.') + print('Waiting for new device:') + except KeyboardInterrupt: + pass From f529fc032a7df12e383727b5833116c976cf3ad2 Mon Sep 17 00:00:00 2001 From: zvecr Date: Mon, 15 Feb 2021 21:32:12 +0000 Subject: [PATCH 3/9] tidy up implementation --- lib/python/qmk/cli/console.py | 99 ++++++++++++++++++++--------------- 1 file changed, 56 insertions(+), 43 deletions(-) diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py index cfd322e871f8..856de7d44368 100644 --- a/lib/python/qmk/cli/console.py +++ b/lib/python/qmk/cli/console.py @@ -1,64 +1,77 @@ """hid_listen """ import hid -import platform +import asyncio +import signal from pathlib import Path -from time import sleep from milc import cli -def patch_linux(dev): - # platform_id = platform.platform().lower() - # if 'linux' in platform_id: - # hidraw = Path(dev['path'].decode('UTF-8')).name - # descriptor_path = Path('/sys/class/hidraw/') / hidraw / 'device/report_descriptor' - # report = descriptor_path.read_bytes() +def _is_console_hid(x): + return x['usage_page'] == 0xFF31 and x['usage'] == 0x0074 - # dev['usage_page'] = (report[2] << 8) + report[1]; - # dev['usage'] = report[4]; - return dev -def is_console_hid(x): - return x['usage_page'] == 0xFF31 and x['usage'] == 0x0074 +def _search(): + return list(filter(_is_console_hid, hid.enumerate())) + + +def list_devices(): + cli.log.info('Available devices:') + devices = _search() + for dev in devices: + cli.log.info(" %02x:%02x %s %s", dev['vendor_id'], dev['product_id'], dev['manufacturer_string'], dev['product_string']) + + +def state_search(loop): + print('.', end='', flush=True) + found = _search() + selected = found[0] if found[0:] else None + + if selected: + loop.call_later(1, state_connect, loop, selected) + else: + loop.call_later(1, state_search, loop) + + +def state_connect(loop, selected): + print() + print('Listening to %s:' % selected['path'].decode()) + device = hid.Device(path=selected['path']) + loop.call_soon(state_read, loop, device) + + +def state_read(loop, device): + print(device.read(32).decode('ascii'), end='') + loop.call_later(0.1, state_read, loop, device) + + +def state_exception(loop, context): + # print('Exception handler called') + # print(context) + print('Device disconnected.') + print('Waiting for new device:') + loop.call_soon(state_search, loop) -def search(): - return list(filter(is_console_hid, map(patch_linux, hid.enumerate()))) @cli.argument('-l', '--list', arg_only=True, action='store_true', help='List available hid_listen devices.') -@cli.argument('-i', '--index', default=0, type=int, help='Device index.') @cli.subcommand('kinda hid_listen ish.') def console(cli): """TODO: """ if cli.args.list: - cli.log.info('Available devices:') - devices = search() - for dev in devices: - cli.log.info("%02x:%02x %s %s", dev['vendor_id'], dev['product_id'], dev['manufacturer_string'], dev['product_string']) - return + return list_devices() + + print('Waiting for device:') + + loop = asyncio.get_event_loop() + + loop.add_signal_handler(signal.SIGINT, loop.stop) # Handle ctrl+c + loop.set_exception_handler(state_exception) # Handle disconnect/connect errors + loop.call_soon(state_search, loop) try: - print('Waiting for device:') - - while True: - selected = None - while selected is None: - found = search() - selected = found[cli.args.index] if found[cli.args.index:] else None - print('.', end = '', flush=True) - sleep(1) - - print() - print('Listening to %s:' % selected['path'].decode()) - device = hid.Device(path=selected['path']) - try: - while True: - print(device.read(32).decode('ascii'), end = '') - - except hid.HIDException: - print('Device disconnected.') - print('Waiting for new device:') - except KeyboardInterrupt: - pass + loop.run_forever() + finally: + loop.close() From ce5fded2e82099bbbdbefa9feb6656a4b550f3d6 Mon Sep 17 00:00:00 2001 From: zvecr Date: Mon, 15 Feb 2021 23:06:26 +0000 Subject: [PATCH 4/9] Tidy up slightly for review --- lib/python/qmk/cli/console.py | 53 ++++++++++++++++++++++------------- requirements-dev.txt | 1 + 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py index 856de7d44368..d53281aebb09 100644 --- a/lib/python/qmk/cli/console.py +++ b/lib/python/qmk/cli/console.py @@ -1,9 +1,24 @@ -"""hid_listen +"""Acquire debugging information from usb hid devices + +cli implementation of https://www.pjrc.com/teensy/hid_listen.html + +State machine is implemented as follows: + +-+ + +-+ More Data? + | +------------+ + | | | ++-----+-------+ +-----------------+ +-----+------+ | +| | | | | | | +| Search +----->+ Connect +------>+ Read +<----+ +| | | | | | ++-----^-------+ +-----------------+ +------+-----+ + | | + +-----------------------------------------------+ + Disconnect/Error """ import hid import asyncio import signal -from pathlib import Path from milc import cli @@ -23,41 +38,41 @@ def list_devices(): cli.log.info(" %02x:%02x %s %s", dev['vendor_id'], dev['product_id'], dev['manufacturer_string'], dev['product_string']) -def state_search(loop): +def state_search(state_machine): print('.', end='', flush=True) found = _search() selected = found[0] if found[0:] else None if selected: - loop.call_later(1, state_connect, loop, selected) + state_machine.call_later(1, state_connect, state_machine, selected) else: - loop.call_later(1, state_search, loop) + state_machine.call_later(1, state_search, state_machine) -def state_connect(loop, selected): +def state_connect(state_machine, selected): print() print('Listening to %s:' % selected['path'].decode()) device = hid.Device(path=selected['path']) - loop.call_soon(state_read, loop, device) + state_machine.call_soon(state_read, state_machine, device) -def state_read(loop, device): +def state_read(state_machine, device): print(device.read(32).decode('ascii'), end='') - loop.call_later(0.1, state_read, loop, device) + state_machine.call_later(0.1, state_read, state_machine, device) -def state_exception(loop, context): +def state_exception(state_machine, context): # print('Exception handler called') # print(context) print('Device disconnected.') print('Waiting for new device:') - loop.call_soon(state_search, loop) + state_machine.call_soon(state_search, state_machine) @cli.argument('-l', '--list', arg_only=True, action='store_true', help='List available hid_listen devices.') -@cli.subcommand('kinda hid_listen ish.') +@cli.subcommand('Acquire debugging information from usb hid devices.', hidden=False if cli.config.user.developer else True) def console(cli): - """TODO: + """Acquire debugging information from usb hid devices """ if cli.args.list: @@ -65,13 +80,13 @@ def console(cli): print('Waiting for device:') - loop = asyncio.get_event_loop() + state_machine = asyncio.get_event_loop() - loop.add_signal_handler(signal.SIGINT, loop.stop) # Handle ctrl+c - loop.set_exception_handler(state_exception) # Handle disconnect/connect errors - loop.call_soon(state_search, loop) + state_machine.add_signal_handler(signal.SIGINT, state_machine.stop) # Handle ctrl+c + state_machine.set_exception_handler(state_exception) # Handle disconnect/connect errors + state_machine.call_soon(state_search, state_machine) try: - loop.run_forever() + state_machine.run_forever() finally: - loop.close() + state_machine.close() diff --git a/requirements-dev.txt b/requirements-dev.txt index 1db3b6d73315..dd522d7641e9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,6 +2,7 @@ -r requirements.txt # Python development requirements +hid nose2 flake8 pep8-naming From 476e2f82faf8e11737b62e565996146bbb472d6f Mon Sep 17 00:00:00 2001 From: zvecr Date: Mon, 15 Feb 2021 23:15:45 +0000 Subject: [PATCH 5/9] Tidy up slightly for review --- lib/python/qmk/cli/console.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py index d53281aebb09..4ac5ae31da88 100644 --- a/lib/python/qmk/cli/console.py +++ b/lib/python/qmk/cli/console.py @@ -3,6 +3,7 @@ cli implementation of https://www.pjrc.com/teensy/hid_listen.html State machine is implemented as follows: + +-+ +-+ More Data? | +------------+ @@ -11,10 +12,10 @@ | | | | | | | | Search +----->+ Connect +------>+ Read +<----+ | | | | | | -+-----^-------+ +-----------------+ +------+-----+ - | | ++-----+-------+ +-----------------+ +------+-----+ + ^ | + | Disconnect/Error? | +-----------------------------------------------+ - Disconnect/Error """ import hid import asyncio From e44a1f8c41b100e92c62839596b9479436b5edfb Mon Sep 17 00:00:00 2001 From: zvecr Date: Mon, 15 Feb 2021 23:52:50 +0000 Subject: [PATCH 6/9] Bodge environment to make tests pass --- .github/workflows/cli.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 28c6bb367985..df727518e577 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -23,6 +23,6 @@ jobs: with: submodules: recursive - name: Install dependencies - run: pip3 install -r requirements.txt + run: pip3 install -r requirements-dev.txt - name: Run tests run: bin/qmk pytest From 05905707bd1fde235357109a9341f58afb56940c Mon Sep 17 00:00:00 2001 From: zvecr Date: Tue, 16 Feb 2021 23:37:15 +0000 Subject: [PATCH 7/9] Refactor away from asyncio due to windows issues --- lib/python/qmk/cli/console.py | 62 ++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py index 4ac5ae31da88..dac8094bce91 100644 --- a/lib/python/qmk/cli/console.py +++ b/lib/python/qmk/cli/console.py @@ -18,12 +18,35 @@ +-----------------------------------------------+ """ import hid -import asyncio -import signal +import queue +import time from milc import cli +class StateMachine(queue.Queue): + def transition(self, func, *args): + self.put(lambda: func(self, *args)) + + def transition_later(self, delay, func, *args): + time.sleep(delay) + self.put(lambda: func(self, *args)) + + def on_exception(self, func): + self._except_func = func + + def run_forever(self): + while True: + try: + f = self.get() + f() + except KeyboardInterrupt: + break + except BaseException as e: + if self._except_func: + self._except_func(e) + + def _is_console_hid(x): return x['usage_page'] == 0xFF31 and x['usage'] == 0x0074 @@ -39,35 +62,35 @@ def list_devices(): cli.log.info(" %02x:%02x %s %s", dev['vendor_id'], dev['product_id'], dev['manufacturer_string'], dev['product_string']) -def state_search(state_machine): +def state_search(sm): print('.', end='', flush=True) + found = _search() selected = found[0] if found[0:] else None if selected: - state_machine.call_later(1, state_connect, state_machine, selected) + sm.transition_later(0.5, state_connect, selected) else: - state_machine.call_later(1, state_search, state_machine) + sm.transition_later(1, state_search) -def state_connect(state_machine, selected): +def state_connect(sm, selected): print() print('Listening to %s:' % selected['path'].decode()) device = hid.Device(path=selected['path']) - state_machine.call_soon(state_read, state_machine, device) + sm.transition_later(1, state_read, device) -def state_read(state_machine, device): +def state_read(sm, device): print(device.read(32).decode('ascii'), end='') - state_machine.call_later(0.1, state_read, state_machine, device) + sm.transition(state_read, device) -def state_exception(state_machine, context): - # print('Exception handler called') - # print(context) +def state_exception(sm, context): + # print(str(e)) print('Device disconnected.') print('Waiting for new device:') - state_machine.call_soon(state_search, state_machine) + sm.transition(state_search) @cli.argument('-l', '--list', arg_only=True, action='store_true', help='List available hid_listen devices.') @@ -81,13 +104,8 @@ def console(cli): print('Waiting for device:') - state_machine = asyncio.get_event_loop() - - state_machine.add_signal_handler(signal.SIGINT, state_machine.stop) # Handle ctrl+c - state_machine.set_exception_handler(state_exception) # Handle disconnect/connect errors - state_machine.call_soon(state_search, state_machine) + sm = StateMachine() + sm.transition(state_search) + sm.on_exception(lambda e: sm.transition(state_exception, e)) - try: - state_machine.run_forever() - finally: - state_machine.close() + sm.run_forever() From 64ab391cc35d5f096958d8590b17e8970663c05d Mon Sep 17 00:00:00 2001 From: zvecr Date: Fri, 19 Feb 2021 21:35:45 +0000 Subject: [PATCH 8/9] Filter devices --- lib/python/qmk/cli/console.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py index dac8094bce91..bb2d23f59e28 100644 --- a/lib/python/qmk/cli/console.py +++ b/lib/python/qmk/cli/console.py @@ -51,8 +51,17 @@ def _is_console_hid(x): return x['usage_page'] == 0xFF31 and x['usage'] == 0x0074 +def _is_filtered_device(x): + name = "%04x:%04x" % (x['vendor_id'], x['product_id']) + return name.lower().startswith(cli.args.device.lower()) + + def _search(): - return list(filter(_is_console_hid, hid.enumerate())) + devices = filter(_is_console_hid, hid.enumerate()) + if cli.args.device: + devices = filter(_is_filtered_device, devices) + + return list(devices) def list_devices(): @@ -66,7 +75,7 @@ def state_search(sm): print('.', end='', flush=True) found = _search() - selected = found[0] if found[0:] else None + selected = found[cli.args.index] if found[cli.args.index:] else None if selected: sm.transition_later(0.5, state_connect, selected) @@ -82,7 +91,7 @@ def state_connect(sm, selected): def state_read(sm, device): - print(device.read(32).decode('ascii'), end='') + print(device.read(32, 1).decode('ascii'), end='') sm.transition(state_read, device) @@ -93,6 +102,8 @@ def state_exception(sm, context): sm.transition(state_search) +@cli.argument('-d', '--device', help='device to select - uses format :.') +@cli.argument('-i', '--index', default=0, help='device index to select.') @cli.argument('-l', '--list', arg_only=True, action='store_true', help='List available hid_listen devices.') @cli.subcommand('Acquire debugging information from usb hid devices.', hidden=False if cli.config.user.developer else True) def console(cli): From 95b4a9cfc57a3b338b39a16b5aa49b4a6ad7ebef Mon Sep 17 00:00:00 2001 From: zvecr Date: Fri, 19 Feb 2021 21:41:11 +0000 Subject: [PATCH 9/9] align vid/pid printing --- lib/python/qmk/cli/console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/python/qmk/cli/console.py b/lib/python/qmk/cli/console.py index bb2d23f59e28..8b73965f772d 100644 --- a/lib/python/qmk/cli/console.py +++ b/lib/python/qmk/cli/console.py @@ -68,7 +68,7 @@ def list_devices(): cli.log.info('Available devices:') devices = _search() for dev in devices: - cli.log.info(" %02x:%02x %s %s", dev['vendor_id'], dev['product_id'], dev['manufacturer_string'], dev['product_string']) + cli.log.info(" %04x:%04x %s %s", dev['vendor_id'], dev['product_id'], dev['manufacturer_string'], dev['product_string']) def state_search(sm):