Skip to content

Commit 6f050c1

Browse files
committed
Resource: add Segger J-Link support
Add a 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 2f00b39 commit 6f050c1

File tree

6 files changed

+202
-0
lines changed

6 files changed

+202
-0
lines changed

doc/configuration.rst

+12
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,18 @@ 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+
749761
SNMPEthernetPort
750762
~~~~~~~~~~~~~~~~
751763
A SNMPEthernetPort resource describes a port on an Ethernet switch, which is

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')

tests/test_jlink.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import contextlib
2+
import io
3+
import subprocess
4+
from unittest.mock import MagicMock
5+
6+
from labgrid.remote.exporter import USBJLinkExport
7+
from labgrid.resource.udev import JLinkDevice
8+
9+
FAKE_SERIAL = 123456789
10+
MATCH = {"ID_SERIAL_SHORT": f"000{FAKE_SERIAL}"}
11+
12+
13+
class Popen_mock(object):
14+
"""Mock of Popen object to mimmic JLinkRemoteServer output"""
15+
16+
def __init__(self, args, **kwargs):
17+
self.wait_called = False
18+
assert "JLinkRemoteServer" in args[0]
19+
assert args[1] == "-Port"
20+
# Since args[2] is dynamic do not check it
21+
assert args[3] == "-select"
22+
assert args[4] == f"USB={FAKE_SERIAL}"
23+
24+
stdout = io.StringIO(
25+
"SEGGER J-Link Remote Server V7.84a\n"
26+
"Compiled Dec 22 2022 16:13:52\n"
27+
"\n"
28+
"'q' to quit '?' for help\n"
29+
"\n"
30+
f"Connected to J-Link with S/N {FAKE_SERIAL}\n"
31+
"\n"
32+
"Waiting for client connections...\n"
33+
)
34+
35+
def kill(self):
36+
pass
37+
38+
def poll(self):
39+
return 0
40+
41+
def terminate(self):
42+
pass
43+
44+
def wait(self, timeout):
45+
# Only timeout on the first call to wait to exercise the error handling code.
46+
if not self.wait_called:
47+
self.wait_called = True
48+
raise subprocess.TimeoutExpired("JLinkRemoteServer", timeout)
49+
50+
51+
@contextlib.contextmanager
52+
def subprocess_mock():
53+
"""Mock of subprocess. Only implements Popen"""
54+
import subprocess
55+
56+
original = subprocess.Popen
57+
58+
def run(args, **kwargs):
59+
assert "JLinkRemoteServer" in args[0]
60+
assert args[1] == "-Port"
61+
# Since args[2] is dynamic do not check it
62+
assert args[3] == "-select"
63+
assert args[4] == f"USB={FAKE_SERIAL}"
64+
return Popen_mock()
65+
66+
subprocess.Popen = Popen_mock
67+
yield MagicMock()
68+
subprocess.Popen = original
69+
70+
71+
def test_jlink_resource(target):
72+
r = JLinkDevice(target, name=None, match=MATCH)
73+
74+
def test_jlink_export_start(target):
75+
config = {'avail': True, 'cls': "JLinkDevice", 'params': {'match': MATCH}, }
76+
e = USBJLinkExport(config)
77+
e.local.avail = True
78+
e.local.serial = FAKE_SERIAL
79+
80+
with subprocess_mock():
81+
e.start()
82+
# Exercise the __del__ method which also exercises stop()
83+
del e

0 commit comments

Comments
 (0)