Skip to content

Commit 8ac97a9

Browse files
committed
QuartusPGM: revisit and cleanup implementation
- udev.py/remote.py: cleanup attributes - exporter.py: - move classes to be able to use generic usb class - use USBGenericExport and remove duplicated code - use tempfile for jtagd config file - remove unnecessary env copy - use labgrids udev device matching infrastructure - cleanup logging and printouts - cleanup kill of jtagd - add jtag_conf into resource info as driver/client information - client.py: - cleanup exception handling - quartuspgmdriver.py: - remove unnecessary env copy - use jtag_conf resource info for temporary jtag.conf - cleanup error handling also some pylint cleanups
1 parent 1d2e282 commit 8ac97a9

5 files changed

Lines changed: 135 additions & 153 deletions

File tree

labgrid/driver/quartuspgmdriver.py

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
# pylint: disable=no-member
22
import subprocess
33
import os
4-
import re
54
import attr
65
import importlib
6+
import logging
77
import tempfile
88
from pathlib import Path
99

1010
from ..factory import target_factory
1111
from ..step import step
1212
from .common import Driver
13-
from .exception import ExecutionError
14-
from ..util.helper import processwrapper
15-
from ..util.managedfile import ManagedFile
16-
import logging
1713

1814
JTAG_CONF_INTEL = """
1915
Remote1 {
@@ -56,41 +52,37 @@ def erase(self, filename=None, devnum=1):
5652

5753
@Driver.check_active
5854
@step(args=['filename', 'operation', 'devnum'])
59-
def operate(self, filename=None, operation="P", devnum=1):
55+
def operate(self, filename=None, operation="P", devnum=1) -> tuple[str, str]:
6056
if filename is None and self.image is not None:
6157
filename = self.target.env.config.get_image_path(self.image)
6258

6359
log = logging.getLogger("QPGM_Driver")
6460

65-
try:
66-
lib_path = importlib.machinery.PathFinder.find_spec('libfilsel').origin
67-
except Exception as e:
68-
return False, "could not find libfilsel!", str(e)
69-
70-
my_env = os.environ.copy()
71-
my_env["LD_PRELOAD"] = os.pathsep.join(filter(None, [lib_path, os.environ.get('LD_PRELOAD')]))
72-
my_env["FILSEL_ORG_PATH"] = str((Path(os.path.expanduser('~')) / ".jtag.conf").resolve())
61+
lib_path = importlib.machinery.PathFinder.find_spec('libfilsel').origin
7362

74-
cable = f"--cable=\"{self.interface.device_name} on " + \
75-
f"{self.interface.host}:{self.interface.jtagd_port} " + \
76-
f"{self.interface.device_port}\""
63+
ld_preload = [lib_path, os.getenv('LD_PRELOAD', "")]
64+
os.environ["LD_PRELOAD"] = os.pathsep.join(ld_preload)
65+
os.environ["FILSEL_ORG_PATH"] = str((Path(os.path.expanduser('~')) / ".jtag.conf").resolve())
7766

78-
operation = f"--operation=\"{operation};{filename}@{str(devnum)}\""
79-
cmd = f"{self.tool} {cable} --mode=JTAG {operation}"
67+
cable = f"'{self.interface.device_name} on {self.interface.host}:{self.interface.jtagd_port} {self.interface.device_port}'"
68+
operation = f"'{operation};{filename}@{str(devnum)}'"
69+
cmd = f"{self.tool} -c {cable} -m JTAG -o {operation}"
8070

8171
with tempfile.NamedTemporaryFile() as conf_temp:
8272

83-
cfg = JTAG_CONF_INTEL.replace("HOST", self.interface.host + ":" + str(self.interface.jtagd_port))\
84-
.replace("PASSWORD", self.interface.jtagd_password)
73+
cfg = self.interface.extra['jtag_conf']
8574
conf_temp.write(cfg.encode("utf-8"))
8675
conf_temp.flush()
87-
log.info("Flashing with command: " + str(cmd))
88-
my_env["FILSEL_DEST_PATH"] = conf_temp.name
76+
log.info("Flashing with command: %s", cmd)
77+
os.environ["FILSEL_DEST_PATH"] = conf_temp.name
8978

90-
stdout, stderr = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=my_env).communicate()
79+
process = subprocess.Popen(cmd, shell=True,
80+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
81+
stdout, stderr = process.communicate()
9182

9283
if "Quartus Prime Programmer was successful." in stdout.decode("utf-8"):
93-
return True, stdout.decode("utf-8"), stderr.decode("utf-8")
84+
return stdout.decode("utf-8"), stderr.decode("utf-8")
9485
else:
95-
return False, stdout.decode("utf-8"), stderr.decode("utf-8")
86+
raise subprocess.CalledProcessError(process.returncode,
87+
cmd, output=stdout, stderr=stderr)
9688

labgrid/remote/client.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,6 @@ class InteractiveCommandError(Error):
6666
pass
6767

6868

69-
class QuartusPgmDriverError(Exception):
70-
"""Exception raised for errors in the Quartus PGM Driver.
71-
"""
72-
73-
def __init__(self, stdout, stderr):
74-
self.stdout = stdout
75-
self.stderr = stderr
76-
self.message = "QuartusPGM failed with stdout: " + stdout + " and stderr: " + stderr
77-
super().__init__(self.message)
78-
79-
8069
class ClientSession(ApplicationSession):
8170
"""The ClientSession encapsulates all the actions a Client can Invoke on
8271
the coordinator."""
@@ -1354,12 +1343,10 @@ def _get_quartus(self, name):
13541343
def intel_program_bitstream(self):
13551344
drv = self._get_quartus(self.args.name)
13561345
processwrapper.enable_print()
1357-
ret, stdout, stderr = drv.flash(self.args.bitstream)
1358-
if not ret:
1359-
print("Flashing failed with:")
1360-
print(stdout)
1361-
print(stderr)
1362-
raise QuartusPgmDriverError(stdout, stderr)
1346+
try:
1347+
drv.flash(self.args.bitstream)
1348+
except subprocess.CalledProcessError as e:
1349+
raise UserError(f"programming failed with {e.returncode}\n{(e.stdout + e.stderr).decode('utf-8')}")
13631350
processwrapper.disable_print()
13641351

13651352
def write_image(self):

labgrid/remote/exporter.py

Lines changed: 109 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33

44
import argparse
55
import asyncio
6+
import importlib
67
import json
78
import logging
89
import sys
910
import os
1011
import os.path
1112
import signal
13+
import tempfile
1214
import time
1315
import traceback
1416
import shutil
@@ -19,7 +21,6 @@
1921
from socket import gethostname, getfqdn
2022
import attr
2123
from autobahn.asyncio.wamp import ApplicationRunner, ApplicationSession
22-
import importlib
2324

2425
from .config import ResourceConfig
2526
from .common import ResourceEntry, enable_tcp_nodelay, monkey_patch_max_msg_payload_size_ws_option
@@ -301,75 +302,140 @@ def _stop(self, start_params):
301302

302303

303304
@attr.s(eq=False)
304-
class QuartusServerExport(ResourceExport):
305+
class NetworkInterfaceExport(ResourceExport):
306+
"""ResourceExport for a network interface"""
307+
308+
def __attrs_post_init__(self):
309+
super().__attrs_post_init__()
310+
if self.cls == "NetworkInterface":
311+
from ..resource.base import NetworkInterface
312+
313+
self.local = NetworkInterface(target=None, name=None, **self.local_params)
314+
elif self.cls == "USBNetworkInterface":
315+
from ..resource.udev import USBNetworkInterface
316+
317+
self.local = USBNetworkInterface(target=None, name=None, **self.local_params)
318+
self.data["cls"] = "RemoteNetworkInterface"
319+
320+
def _get_params(self):
321+
"""Helper function to return parameters"""
322+
params = {
323+
"host": self.host,
324+
"ifname": self.local.ifname,
325+
}
326+
if self.cls == "USBNetworkInterface":
327+
params["extra"] = {
328+
"state": self.local.if_state,
329+
}
330+
331+
return params
332+
333+
334+
exports["USBNetworkInterface"] = NetworkInterfaceExport
335+
exports["NetworkInterface"] = NetworkInterfaceExport
336+
337+
338+
@attr.s(eq=False)
339+
class USBGenericExport(ResourceExport):
340+
"""ResourceExport for USB devices accessed directly from userspace"""
341+
342+
def __attrs_post_init__(self):
343+
super().__attrs_post_init__()
344+
local_cls_name = self.cls
345+
self.data["cls"] = f"Network{self.cls}"
346+
from ..resource import udev
347+
348+
local_cls = getattr(udev, local_cls_name)
349+
self.local = local_cls(target=None, name=None, **self.local_params)
350+
351+
def _get_params(self):
352+
"""Helper function to return parameters"""
353+
return {
354+
"host": self.host,
355+
"busnum": self.local.busnum,
356+
"devnum": self.local.devnum,
357+
"path": self.local.path,
358+
"vendor_id": self.local.vendor_id,
359+
"model_id": self.local.model_id,
360+
}
361+
362+
363+
@attr.s(eq=False)
364+
class QuartusServerExport(USBGenericExport):
305365
""" ResourceExport for a QuartusUSBJTAG via ``jtagd``"""
306366

307367
def __attrs_post_init__(self):
308368
super().__attrs_post_init__()
309-
self.data['cls'] = "NetworkQuartusUSBJTAG"
310-
from ..resource.udev import QuartusUSBJTAG
311-
self.local = QuartusUSBJTAG(target=None, name=None, **self.local_params)
312369
self.child = None
370+
self.jtagd_port = self.local.jtagd_port
371+
self.cfg_tempfile = None
313372

314373
def __del__(self):
315374
if self.child is not None:
316-
self.release()
317-
318-
def _get_custom_config_file_name(self):
319-
serialNumber = self.local.device_serial
320-
return os.path.join(self.local.jtagd_file_locations, f"jtagd_cfg_{serialNumber}_{self.local.jtagd_port}.conf")
375+
self.stop()
321376

322-
def acquire(self, *args, **kwargs):
377+
def _start(self, start_params):
323378
"""Start ``jtagd`` subprocess"""
324379
assert self.local.avail
380+
assert self.child is None
325381

326-
cfgFileName = self._get_custom_config_file_name()
382+
if not self.jtagd_port:
383+
self.jtagd_port = get_free_port()
327384

328-
with open(cfgFileName, 'w+') as file:
329-
file.write(f"Password = \"{self.local.jtagd_password}\";")
385+
self.cfg_tempfile = tempfile.NamedTemporaryFile()
386+
self.cfg_tempfile.write(
387+
f"Password = \"{self.local.jtagd_password}\";".encode('utf-8'))
388+
self.cfg_tempfile.flush()
330389

331-
#find the right path to the library
390+
# find the right path to the library and modify the env
332391
lib_path = importlib.machinery.PathFinder.find_spec('libhwsf').origin
392+
ld_preload = [lib_path, os.getenv('LD_PRELOAD', "")]
393+
os.environ["LD_PRELOAD"] = os.pathsep.join(ld_preload)
394+
os.environ["HWSF_DEV"] = "path:" + self.local.device.sys_name
333395

334-
#get the usb path from the device serial number
335-
serialNumber = self.local.device_serial
336-
dev_grep = "grep {SERIAL} /sys/bus/usb/devices/*/serial | cut -d '/' -f6".format(SERIAL=serialNumber)
337-
dev_path, _ = subprocess.Popen(dev_grep, shell=True, stdout=subprocess.PIPE).communicate()
338-
339-
my_env = os.environ.copy()
340-
my_env["LD_PRELOAD"] = os.pathsep.join(filter(None, [lib_path, os.environ.get('LD_PRELOAD')]))
341-
my_env["HWSF_DEV"] = "path:" + dev_path.decode("utf-8").replace("\n","")
396+
cmd = f"{self.local.jtagd_cmd} --foreground --port {self.jtagd_port} --config {self.cfg_tempfile.name}"
342397

343-
cmd = " ".join([f"{self.local.jtagd_cmd}",
344-
f"--foreground",
345-
f"--port {self.local.jtagd_port}",
346-
f"--config {self._get_custom_config_file_name()}"])
398+
self.logger.info("starting jtagd for %s on port %s with command LD_PRELOAD+=%s HWSF_DEV=%s %s",
399+
self.local.device.sys_name, self.jtagd_port, lib_path,
400+
os.environ['HWSF_DEV'], cmd)
401+
self.child = subprocess.Popen(cmd, shell=True, preexec_fn=os.setsid)
347402

348-
self.logger.info("Starting jtagd with command: " + cmd)
349-
self.logger.info("Starting jtagd with LD_PRELOAD=" + lib_path + " HWSF_DEV=" + my_env["HWSF_DEV"])
350-
351-
self.child = subprocess.Popen(cmd, shell=True, preexec_fn=os.setsid, env=my_env)
352-
353-
def release(self, *args, **kwargs):
403+
def _stop(self, start_params):
354404
"""Stop ``jtagd`` subprocess"""
355405
assert self.child
356-
357-
os.remove(self._get_custom_config_file_name())
358-
os.killpg(os.getpgid(self.child.pid), signal.SIGTERM)
406+
child = self.child
359407
self.child = None
360-
self.logger.info(f"stopped jtagd at {self.local.jtagd_port}")
408+
self.cfg_tempfile = None
409+
child.terminate()
410+
try:
411+
child.wait(2.0)
412+
except subprocess.TimeoutExpired:
413+
self.logger.warning("jtagd for %s still running after SIGTERM",
414+
self.local.device.sys_name)
415+
child.kill()
416+
child.wait(1.0)
417+
418+
self.logger.info("stopped jtagd for %s on port %s",
419+
self.local.device.sys_name, self.jtagd_port)
361420

362421
def _get_params(self):
363422
"""Helper function to return parameters"""
364423
return {
365-
'host': self.host,
366-
'jtagd_password': self.local.jtagd_password,
367-
'jtagd_port': self.local.jtagd_port,
368-
'jtagd_cmd': self.local.jtagd_cmd,
369-
'device_name': self.local.device_name,
370-
'device_port': self.local.device_port,
424+
**super()._get_params(),
425+
"jtagd_cmd": self.local.jtagd_cmd,
426+
"jtagd_port": self.jtagd_port,
427+
"jtagd_password": self.local.jtagd_password,
428+
"device_name": self.local.device_name,
429+
"device_port": self.local.device_port,
430+
"extra": {
431+
"jtag_conf": f"""Remote1 {{
432+
Host = \"{self.host}:{self.jtagd_port}\";
433+
Password = \"{self.local.jtagd_password}\";
434+
}}"""
435+
}
371436
}
372437

438+
373439
exports["QuartusUSBJTAG"] = QuartusServerExport
374440

375441

@@ -445,65 +511,6 @@ def _stop(self, start_params):
445511
exports["XilinxUSBJTAG"] = VivadoHWServerExport
446512

447513

448-
@attr.s(eq=False)
449-
class NetworkInterfaceExport(ResourceExport):
450-
"""ResourceExport for a network interface"""
451-
452-
def __attrs_post_init__(self):
453-
super().__attrs_post_init__()
454-
if self.cls == "NetworkInterface":
455-
from ..resource.base import NetworkInterface
456-
457-
self.local = NetworkInterface(target=None, name=None, **self.local_params)
458-
elif self.cls == "USBNetworkInterface":
459-
from ..resource.udev import USBNetworkInterface
460-
461-
self.local = USBNetworkInterface(target=None, name=None, **self.local_params)
462-
self.data["cls"] = "RemoteNetworkInterface"
463-
464-
def _get_params(self):
465-
"""Helper function to return parameters"""
466-
params = {
467-
"host": self.host,
468-
"ifname": self.local.ifname,
469-
}
470-
if self.cls == "USBNetworkInterface":
471-
params["extra"] = {
472-
"state": self.local.if_state,
473-
}
474-
475-
return params
476-
477-
478-
exports["USBNetworkInterface"] = NetworkInterfaceExport
479-
exports["NetworkInterface"] = NetworkInterfaceExport
480-
481-
482-
@attr.s(eq=False)
483-
class USBGenericExport(ResourceExport):
484-
"""ResourceExport for USB devices accessed directly from userspace"""
485-
486-
def __attrs_post_init__(self):
487-
super().__attrs_post_init__()
488-
local_cls_name = self.cls
489-
self.data["cls"] = f"Network{self.cls}"
490-
from ..resource import udev
491-
492-
local_cls = getattr(udev, local_cls_name)
493-
self.local = local_cls(target=None, name=None, **self.local_params)
494-
495-
def _get_params(self):
496-
"""Helper function to return parameters"""
497-
return {
498-
"host": self.host,
499-
"busnum": self.local.busnum,
500-
"devnum": self.local.devnum,
501-
"path": self.local.path,
502-
"vendor_id": self.local.vendor_id,
503-
"model_id": self.local.model_id,
504-
}
505-
506-
507514
@attr.s(eq=False)
508515
class USBSigrokExport(USBGenericExport):
509516
"""ResourceExport for USB devices accessed directly from userspace"""

labgrid/resource/remote.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,9 @@ def __attrs_post_init__(self):
179179

180180
@target_factory.reg_resource
181181
@attr.s(eq=False)
182-
class NetworkQuartusUSBJTAG(NetworkResource, ManagedResource):
183-
host = attr.ib(default="")
182+
class NetworkQuartusUSBJTAG(RemoteUSBResource):
184183
jtagd_password = attr.ib(default="password1234")
185-
jtagd_port = attr.ib(default=3109)
184+
jtagd_port = attr.ib(factory=int)
186185
jtagd_cmd = attr.ib(default="jtagd")
187186
device_name = attr.ib(default="Arrow-USB-Blaster")
188187
device_port = attr.ib(default="")

0 commit comments

Comments
 (0)