From 32cec42678cce962e25de1e84d0dff3f6d6d11ac Mon Sep 17 00:00:00 2001 From: tdejoigny-ledger Date: Thu, 2 Apr 2026 19:48:29 +0200 Subject: [PATCH 1/2] add debugApp script to capture app PRINTF output over USB CDC --- doc/source/script_reference.rst | 10 +++ ledgerblue/debugApp.py | 112 ++++++++++++++++++++++++++++++++ pyproject.toml | 3 +- 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 ledgerblue/debugApp.py diff --git a/doc/source/script_reference.rst b/doc/source/script_reference.rst index 37b8cbb..cf90b85 100644 --- a/doc/source/script_reference.rst +++ b/doc/source/script_reference.rst @@ -11,6 +11,16 @@ checkGenuine.py :func: get_argparser :prog: python -m ledgerblue.checkGenuine +.. _debugApp.py: + +debugApp.py +----------- + +.. argparse:: + :module: ledgerblue.debugApp + :func: get_argparser + :prog: python -m ledgerblue.debugApp + .. _deleteApp.py: deleteApp.py diff --git a/ledgerblue/debugApp.py b/ledgerblue/debugApp.py new file mode 100644 index 0000000..5d2b390 --- /dev/null +++ b/ledgerblue/debugApp.py @@ -0,0 +1,112 @@ +""" +******************************************************************************* +* Ledger Blue +* (c) 2016 Ledger +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +******************************************************************************** +""" + +import argparse +import sys + +import serial +import serial.tools.list_ports + +LEDGER_VENDOR_ID = 0x2C97 + + +def get_argparser(): + parser = argparse.ArgumentParser( + description="Listen to debug output from a Ledger app compiled with DEBUG_OVER_USB." + ) + parser.add_argument( + "--port", "-p", + help="Serial port to use (e.g. COM3 on Windows, /dev/ttyACM0 on Linux). Auto-detected if omitted.", + type=str, + default=None, + ) + parser.add_argument( + "--baudrate", "-b", + help="Baud rate for the serial connection (default: 115200).", + type=int, + default=115200, + ) + parser.add_argument( + "--output", "-o", + help="Write debug output to a file in addition to stdout.", + type=str, + default=None, + ) + return parser + + +def find_ledger_cdc_port(port=None): + """Find the Ledger device's CDC (virtual serial) port. + + When DEBUG_OVER_USB is enabled in a Ledger app, the device exposes + an additional USB CDC interface that sends PRINTF debug output. + """ + if port: + return port + + for p in serial.tools.list_ports.comports(): + if p.vid == LEDGER_VENDOR_ID: + return p.device + + return None + + +if __name__ == "__main__": + args = get_argparser().parse_args() + + cdc_port = find_ledger_cdc_port(args.port) + if cdc_port is None: + print( + "No Ledger CDC debug port found.\n" + "Make sure your Ledger app was compiled with DEBUG_OVER_USB=1\n" + "and that the device is connected and the app is running.\n" + "You can also specify the port manually with --port." + ) + sys.exit(1) + + print("Listening on {} (baudrate={}) ...".format(cdc_port, args.baudrate)) + print("Press Ctrl+C to stop.\n") + + output_file = None + if args.output: + try: + output_file = open(args.output, "a", encoding="utf-8") + except OSError: + print("Unable to open file {} for writing.".format(args.output)) + sys.exit(1) + + try: + with serial.Serial(cdc_port, args.baudrate, timeout=1) as ser: + while True: + data = ser.readline() + if data: + text = data.decode("utf-8", errors="replace") + sys.stdout.write(text) + sys.stdout.flush() + if output_file: + output_file.write(text) + output_file.flush() + except serial.SerialException as e: + print("Serial error: {}".format(e)) + sys.exit(1) + except KeyboardInterrupt: + print("\nStopped.") + finally: + if output_file: + output_file.close() diff --git a/pyproject.toml b/pyproject.toml index 1badc33..2d02f46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,8 @@ dependencies = [ "nfcpy>=1.0.4", "bleak>=0.20.1", "pycryptodome>=3.18.0", - "python-gnupg>=0.5.0" + "python-gnupg>=0.5.0", + "pyserial>=3.5" ] [tool.setuptools] From 2b8d175032041786ed74308d5020750a36f80186 Mon Sep 17 00:00:00 2001 From: tdejoigny-ledger Date: Fri, 3 Apr 2026 16:40:35 +0200 Subject: [PATCH 2/2] scan until Ledger CDC port to appear (connection & disconnection) and add timestamp --- ledgerblue/debugApp.py | 86 +++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/ledgerblue/debugApp.py b/ledgerblue/debugApp.py index 5d2b390..4f0a9a7 100644 --- a/ledgerblue/debugApp.py +++ b/ledgerblue/debugApp.py @@ -19,6 +19,8 @@ import argparse import sys +import time +from datetime import datetime import serial import serial.tools.list_ports @@ -67,21 +69,36 @@ def find_ledger_cdc_port(port=None): return None -if __name__ == "__main__": - args = get_argparser().parse_args() +def wait_for_cdc_port(port=None, timeout=0): + """Wait for the Ledger CDC port to appear. + + If timeout is 0, wait indefinitely. + Returns the port name, or None if timeout expired. + """ + start = time.time() + first = True + while True: + cdc_port = find_ledger_cdc_port(port) + if cdc_port is not None: + if not first: + sys.stdout.write("\n") + sys.stdout.flush() + return cdc_port + if first: + print("Waiting for Ledger CDC debug port...", end="", flush=True) + first = False + else: + sys.stdout.write(".") + sys.stdout.flush() + if timeout and (time.time() - start) >= timeout: + sys.stdout.write("\n") + sys.stdout.flush() + return None + time.sleep(1) - cdc_port = find_ledger_cdc_port(args.port) - if cdc_port is None: - print( - "No Ledger CDC debug port found.\n" - "Make sure your Ledger app was compiled with DEBUG_OVER_USB=1\n" - "and that the device is connected and the app is running.\n" - "You can also specify the port manually with --port." - ) - sys.exit(1) - print("Listening on {} (baudrate={}) ...".format(cdc_port, args.baudrate)) - print("Press Ctrl+C to stop.\n") +if __name__ == "__main__": + args = get_argparser().parse_args() output_file = None if args.output: @@ -92,19 +109,36 @@ def find_ledger_cdc_port(port=None): sys.exit(1) try: - with serial.Serial(cdc_port, args.baudrate, timeout=1) as ser: - while True: - data = ser.readline() - if data: - text = data.decode("utf-8", errors="replace") - sys.stdout.write(text) - sys.stdout.flush() - if output_file: - output_file.write(text) - output_file.flush() - except serial.SerialException as e: - print("Serial error: {}".format(e)) - sys.exit(1) + while True: + cdc_port = wait_for_cdc_port(args.port) + if cdc_port is None: + print( + "No Ledger CDC debug port found.\n" + "Make sure your Ledger app was compiled with DEBUG_OVER_USB=1\n" + "and that the device is connected and the app is running.\n" + "You can also specify the port manually with --port." + ) + sys.exit(1) + + print("Connected on {} (baudrate={}).".format(cdc_port, args.baudrate)) + print("Press Ctrl+C to stop.\n") + + try: + with serial.Serial(cdc_port, args.baudrate, timeout=1) as ser: + while True: + data = ser.readline() + if data: + text = data.decode("utf-8", errors="replace") + timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] + for line in text.splitlines(True): + stamped = "[{}] {}".format(timestamp, line) + sys.stdout.write(stamped) + sys.stdout.flush() + if output_file: + output_file.write(stamped) + output_file.flush() + except serial.SerialException: + print("\nSerial connection lost. Waiting for device...") except KeyboardInterrupt: print("\nStopped.") finally: