Skip to content

Commit 8ed0405

Browse files
committed
Driver/Resource: Add Segger J-Link support
Add a driver and resource for the Segger J-Link debug probe. Export the J-Link resource over IP using the J-Link Remote Server. Signed-off-by: Paul Vittorino <[email protected]>
1 parent c3253c4 commit 8ed0405

File tree

10 files changed

+291
-1
lines changed

10 files changed

+291
-1
lines changed

CHANGES.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ New Features in 23.1
1010
- A new log level called ``CONSOLE`` has been added between the default
1111
``INFO`` and ``DEBUG`` levels. This level will show all reads and writes made
1212
to the serial console during testing.
13+
- Add JLinkDriver and JLinkDevice resource.
1314

1415
Bug fixes in 23.1
1516
~~~~~~~~~~~~~~~~~
@@ -437,7 +438,7 @@ New and Updated Drivers
437438
- The `SerialDriver` now supports using plain TCP instead of RFC 2217, which is
438439
needed from some console servers.
439440
- The `ShellDriver` has been improved:
440-
441+
441442
- It supports configuring the various timeouts used during the login process.
442443
- It can use xmodem to transfer file from and to the target.
443444

doc/configuration.rst

+30
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,21 @@ Arguments:
746746
Used by:
747747
- `OpenOCDDriver`_
748748

749+
JLinkDevice
750+
~~~~~~~~~~~
751+
A JLinkDevice resource describes a Segger J-Link Debug Probe.
752+
753+
.. code-block:: yaml
754+
755+
JLinkDevice:
756+
match:
757+
ID_SERIAL_SHORT: '000000123456'
758+
759+
- match (dict): key and value for a udev match, see `udev Matching`_
760+
761+
Used by:
762+
- `JLinkDriver`_
763+
749764
SNMPEthernetPort
750765
~~~~~~~~~~~~~~~~
751766
A SNMPEthernetPort resource describes a port on an Ethernet switch, which is
@@ -1751,6 +1766,21 @@ Arguments:
17511766
- board_config (str): optional, board config in the ``openocd/scripts/board/`` directory
17521767
- load_commands (list of str): optional, load commands to use instead of ``init``, ``bootstrap {filename}``, ``shutdown``
17531768

1769+
JLinkDriver
1770+
~~~~~~~~~~~~~
1771+
A JLinkDriver provides access to a Segger J-Link Debug Probe via `pylink https://github.com/square/pylink`_
1772+
1773+
Binds to:
1774+
interface:
1775+
- `JLinkDevice`_
1776+
- `NetworkJLinkDevice`_
1777+
1778+
Implements:
1779+
- None
1780+
1781+
Arguments:
1782+
- None
1783+
17541784
QuartusHPSDriver
17551785
~~~~~~~~~~~~~~~~
17561786
A QuartusHPSDriver controls the "Quartus Prime Programmer and Tools" to flash

labgrid/driver/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@
4646
from .usbtmcdriver import USBTMCDriver
4747
from .deditecrelaisdriver import DeditecRelaisDriver
4848
from .dediprogflashdriver import DediprogFlashDriver
49+
from .jlinkdriver import JLinkDriver

labgrid/driver/jlinkdriver.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from importlib import import_module
2+
import socket
3+
import attr
4+
5+
from ..factory import target_factory
6+
from ..resource.remote import NetworkJLinkDevice
7+
from ..util.proxy import proxymanager
8+
from .common import Driver
9+
10+
@target_factory.reg_driver
11+
@attr.s(eq=False)
12+
class JLinkDriver(Driver):
13+
bindings = {"jlink_device": {"JLinkDevice", "NetworkJLinkDevice"}, }
14+
15+
def __attrs_post_init__(self):
16+
super().__attrs_post_init__()
17+
self._module = import_module('pylink')
18+
self.jlink = None
19+
20+
def on_activate(self):
21+
self.jlink = self._module.JLink()
22+
23+
if isinstance(self.jlink_device, NetworkJLinkDevice):
24+
# we can only forward if the backend knows which port to use
25+
host, port = proxymanager.get_host_and_port(self.jlink_device)
26+
# The J-Link client software does not support host names
27+
ip_addr = socket.gethostbyname(host)
28+
29+
# Workaround for Debian's /etc/hosts entry
30+
# https://www.debian.org/doc/manuals/debian-reference/ch05.en.html#_the_hostname_resolution
31+
if ip_addr == "127.0.1.1":
32+
ip_addr = "127.0.0.1"
33+
self.jlink.open(ip_addr=f"{ip_addr}:{port}")
34+
else:
35+
self.jlink.open(serial_no=self.jlink_device.serial)
36+
37+
def on_deactivate(self):
38+
self.jlink.close()
39+
self.jlink = None
40+
41+
@Driver.check_active
42+
def get_interface(self):
43+
return self.jlink

labgrid/remote/exporter.py

+77
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
from pathlib import Path
1515
from typing import Dict, Type
1616
from socket import gethostname, getfqdn
17+
from pexpect import TIMEOUT
1718
import attr
1819
from autobahn.asyncio.wamp import ApplicationRunner, ApplicationSession
1920

2021
from .config import ResourceConfig
2122
from .common import ResourceEntry, enable_tcp_nodelay
2223
from ..util import get_free_port, labgrid_version
24+
from ..util import Timeout
2325

2426

2527
__version__ = labgrid_version()
@@ -500,6 +502,80 @@ def __attrs_post_init__(self):
500502
super().__attrs_post_init__()
501503
self.data['cls'] = f"Remote{self.cls}".replace("Network", "")
502504

505+
class USBJLinkExport(USBGenericExport):
506+
"""Export J-Link device using the J-Link Remote Server"""
507+
508+
def __attrs_post_init__(self):
509+
super().__attrs_post_init__()
510+
self.child = None
511+
self.port = None
512+
self.tool = '/opt/SEGGER/JLink/JLinkRemoteServer'
513+
514+
def _get_params(self):
515+
"""Helper function to return parameters"""
516+
return {
517+
'host': self.host,
518+
'port': self.port,
519+
'busnum': self.local.busnum,
520+
'devnum': self.local.devnum,
521+
'path': self.local.path,
522+
'vendor_id': self.local.vendor_id,
523+
'model_id': self.local.model_id,
524+
}
525+
526+
def __del__(self):
527+
if self.child is not None:
528+
self.stop()
529+
530+
def _start(self, start_params):
531+
"""Start ``JLinkRemoteServer`` subprocess"""
532+
assert self.local.avail
533+
assert self.child is None
534+
self.port = get_free_port()
535+
536+
cmd = [
537+
self.tool,
538+
"-Port",
539+
f"{self.port}",
540+
"-select",
541+
f"USB={self.local.serial}",
542+
]
543+
self.logger.info("Starting JLinkRemoteServer with: %s", " ".join(cmd))
544+
self.child = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
545+
546+
# Wait for the server to be ready for incoming connections
547+
# Waiting to open the socket with Python does not work
548+
timeout = Timeout(10.0)
549+
while not timeout.expired:
550+
line = self.child.stdout.readline().rstrip()
551+
self.logger.debug(line)
552+
if "Waiting for client connections..." in line:
553+
break
554+
555+
if timeout.expired:
556+
raise TIMEOUT(
557+
f"Timeout of {timeout.timeout} seconds exceeded during waiting for J-Link Remote Server startup"
558+
)
559+
self.logger.info("started JLinkRemoteServer for %s on port %d", self.local.serial, self.port)
560+
561+
def _stop(self, start_params):
562+
"""Stop ``JLinkRemoteServer`` subprocess"""
563+
assert self.child
564+
child = self.child
565+
self.child = None
566+
port = self.port
567+
self.port = None
568+
child.terminate()
569+
try:
570+
child.wait(2.0) # JLinkRemoteServer takes about a second to react
571+
except subprocess.TimeoutExpired:
572+
self.logger.warning("JLinkRemoteServer for %s still running after SIGTERM", self.local.serial)
573+
log_subprocess_kernel_stack(self.logger, child)
574+
child.kill()
575+
child.wait(1.0)
576+
self.logger.info("stopped JLinkRemoteServer for %s on port %d", self.local.serial, port)
577+
578+
503579
exports["AndroidFastboot"] = USBGenericExport
504580
exports["AndroidUSBFastboot"] = USBGenericRemoteExport
505581
exports["DFUDevice"] = USBGenericExport
@@ -512,6 +588,7 @@ def __attrs_post_init__(self):
512588
exports["USBSDMuxDevice"] = USBSDMuxExport
513589
exports["USBSDWireDevice"] = USBSDWireExport
514590
exports["USBDebugger"] = USBGenericExport
591+
exports["JLinkDevice"] = USBJLinkExport
515592

516593
exports["USBMassStorage"] = USBGenericExport
517594
exports["USBVideo"] = USBGenericExport

labgrid/resource/remote.py

+12
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,18 @@ def __attrs_post_init__(self):
305305
super().__attrs_post_init__()
306306

307307

308+
@target_factory.reg_resource
309+
@attr.s(eq=False)
310+
class NetworkJLinkDevice(RemoteUSBResource):
311+
"""The NetworkJLinkDevice describes a remotely accessible USB Segger J-Link device"""
312+
313+
port = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(int)))
314+
315+
def __attrs_post_init__(self):
316+
self.timeout = 10.0
317+
super().__attrs_post_init__()
318+
319+
308320
@target_factory.reg_resource
309321
@attr.s(eq=False)
310322
class NetworkDeditecRelais8(RemoteUSBResource):

labgrid/resource/suggest.py

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
HIDRelay,
2424
USBDebugger,
2525
USBPowerPort,
26+
JLinkDevice,
2627
)
2728
from ..util import dump
2829

@@ -56,6 +57,7 @@ def __init__(self, args):
5657
self.resources.append(HIDRelay(**args))
5758
self.resources.append(USBDebugger(**args))
5859
self.resources.append(USBPowerPort(**args, index=0))
60+
self.resources.append(JLinkDevice(**args))
5961

6062
def suggest_callback(self, resource, meta, suggestions):
6163
cls = type(resource).__name__

labgrid/resource/udev.py

+16
Original file line numberDiff line numberDiff line change
@@ -696,3 +696,19 @@ def filter_match(self, device):
696696
return False
697697

698698
return super().filter_match(device)
699+
700+
@target_factory.reg_resource
701+
@attr.s(eq=False)
702+
class JLinkDevice(USBResource):
703+
"""The JLinkDevice describes an attached Segger J-Link device,
704+
it is identified via USB using udev
705+
"""
706+
707+
def __attrs_post_init__(self):
708+
self.match["ID_VENDOR_ID"] = "1366"
709+
self.match["ID_MODEL_ID"] = "0101"
710+
super().__attrs_post_init__()
711+
712+
def update(self):
713+
super().update()
714+
self.serial = self.device.properties.get('ID_SERIAL_SHORT')

pyproject.toml

+4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ dynamic = ["version"] # via setuptools_scm
5454
doc = ["sphinx_rtd_theme>=1.0.0"]
5555
docker = ["docker>=5.0.2"]
5656
graph = ["graphviz>=0.17.0"]
57+
jlink = ["pylink-square>=1.0.0"]
5758
kasa = ["python-kasa>=0.4.0"]
5859
modbus = ["pyModbusTCP>=0.1.10"]
5960
modbusrtu = ["minimalmodbus>=1.0.2"]
@@ -91,6 +92,9 @@ dev = [
9192
# labgrid[graph]
9293
"graphviz>=0.17.0",
9394

95+
# labgrid[jlink]
96+
"pylink-square>=1.0.0",
97+
9498
# labgrid[kasa]
9599
"python-kasa>=0.4.0",
96100

tests/test_jlink.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import contextlib
2+
import io
3+
import pytest
4+
import subprocess
5+
from unittest.mock import MagicMock
6+
from unittest.mock import Mock
7+
from unittest.mock import patch
8+
9+
from labgrid.remote.exporter import USBJLinkExport
10+
from labgrid.resource.remote import NetworkJLinkDevice
11+
from labgrid.resource.udev import JLinkDevice
12+
from labgrid.driver.jlinkdriver import JLinkDriver
13+
14+
FAKE_SERIAL = 123456789
15+
MATCH = {"ID_SERIAL_SHORT": f"000{FAKE_SERIAL}"}
16+
17+
18+
class Popen_mock():
19+
"""Mock of Popen object to mimmic JLinkRemoteServer output"""
20+
21+
def __init__(self, args, **kwargs):
22+
assert "JLinkRemoteServer" in args[0]
23+
assert args[1] == "-Port"
24+
# Since args[2] is dynamic do not check it
25+
assert args[3] == "-select"
26+
assert args[4] == f"USB={FAKE_SERIAL}"
27+
self.wait_called = False
28+
29+
stdout = io.StringIO(
30+
"SEGGER J-Link Remote Server V7.84a\n"
31+
"Compiled Dec 22 2022 16:13:52\n"
32+
"\n"
33+
"'q' to quit '?' for help\n"
34+
"\n"
35+
f"Connected to J-Link with S/N {FAKE_SERIAL}\n"
36+
"\n"
37+
"Waiting for client connections...\n"
38+
)
39+
40+
def kill(self):
41+
pass
42+
43+
def poll(self):
44+
return 0
45+
46+
def terminate(self):
47+
pass
48+
49+
def wait(self, timeout=None):
50+
# Only timeout on the first call to exercise the error handling code.
51+
if not self.wait_called:
52+
self.wait_called = True
53+
raise subprocess.TimeoutExpired("JLinkRemoteServer", timeout)
54+
55+
56+
def test_jlink_resource(target):
57+
r = JLinkDevice(target, name=None, match=MATCH)
58+
59+
60+
@patch('subprocess.Popen', Popen_mock)
61+
def test_jlink_export_start(target):
62+
config = {'avail': True, 'cls': "JLinkDevice", 'params': {'match': MATCH}, }
63+
e = USBJLinkExport(config)
64+
e.local.avail = True
65+
e.local.serial = FAKE_SERIAL
66+
67+
e.start()
68+
# Exercise the __del__ method which also exercises stop()
69+
del e
70+
71+
72+
@patch('subprocess.Popen', Popen_mock)
73+
def test_jlink_driver(target):
74+
pytest.importorskip("pylink")
75+
device = JLinkDevice(target, name=None, match=MATCH)
76+
device.avail = True
77+
device.serial = FAKE_SERIAL
78+
driver = JLinkDriver(target, name=None)
79+
80+
with patch('pylink.JLink') as JLinkMock:
81+
instance = JLinkMock.return_value
82+
target.activate(driver)
83+
instance.open.assert_called_once_with(serial_no=FAKE_SERIAL)
84+
intf = driver.get_interface()
85+
assert(isinstance(intf, Mock))
86+
target.deactivate(driver)
87+
instance.close.assert_called_once_with()
88+
89+
90+
@patch('subprocess.Popen', Popen_mock)
91+
def test_jlink_driver_network_device(target):
92+
pytest.importorskip("pylink")
93+
device = NetworkJLinkDevice(target, None, host='127.0.1.1', port=12345, busnum=0, devnum=1, path='0:1', vendor_id=0x0, model_id=0x0,)
94+
device.avail = True
95+
driver = JLinkDriver(target, name=None)
96+
assert (isinstance(driver, JLinkDriver))
97+
98+
with patch('pylink.JLink') as JLinkMock:
99+
instance = JLinkMock.return_value
100+
# Call on_activate directly since activating the driver via the target does not work during testing
101+
driver.on_activate()
102+
instance.open.assert_called_once_with(ip_addr='127.0.0.1:12345')
103+
driver.on_deactivate()
104+
instance.close.assert_called_once_with()

0 commit comments

Comments
 (0)