Skip to content

Commit 8a656ef

Browse files
authored
Merge pull request #1345 from flxzt/sigrok-driver-improv
driver/sigrok: add blocking capture methods, misc improvements to sigrok driver
2 parents e522c70 + c8e51e5 commit 8a656ef

3 files changed

Lines changed: 256 additions & 29 deletions

File tree

doc/configuration.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3127,6 +3127,9 @@ Arguments:
31273127
The driver can be used in test cases by calling its ``capture()``, ``stop()``
31283128
and ``analyze()`` methods.
31293129

3130+
To capture for a certain predetermined amount of time or number of samples,
3131+
methods ``capture_for_time()`` and ``capture_samples()`` can be used.
3132+
31303133
SigrokPowerDriver
31313134
~~~~~~~~~~~~~~~~~
31323135
The :any:`SigrokPowerDriver` uses a `SigrokUSBSerialDevice`_ resource to

labgrid/driver/sigrokdriver.py

Lines changed: 222 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,39 @@ def _get_sigrok_prefix(self):
9494
return self.sigrok.command_prefix + prefix
9595

9696
@Driver.check_active
97-
@step(title='call', args=['args'])
97+
@step(title='call_with_driver', args=['args'])
9898
def _call_with_driver(self, *args):
9999
combined = self._get_sigrok_prefix() + list(args)
100100
self.logger.debug("Combined command: %s", " ".join(combined))
101101
self._process = subprocess.Popen(
102102
combined,
103103
stdout=subprocess.PIPE,
104104
stdin=subprocess.PIPE,
105-
stderr=subprocess.PIPE
105+
stderr=subprocess.PIPE,
106+
text=True
106107
)
107108

109+
@Driver.check_active
110+
@step(title='call_with_driver_blocking', args=['args'])
111+
def _call_with_driver_blocking(self, *args, log_output=False):
112+
combined = self._get_sigrok_prefix() + list(args)
113+
self.logger.debug("Combined command: %s", " ".join(combined))
114+
process = subprocess.Popen(
115+
combined,
116+
stdout=subprocess.PIPE,
117+
stdin=subprocess.PIPE,
118+
stderr=subprocess.PIPE,
119+
text=True
120+
)
121+
stdout, stderr = process.communicate()
122+
if log_output:
123+
self.logger.debug("stdout: %s", stdout)
124+
self.logger.debug("stderr: %s", stderr)
125+
if process.returncode != 0:
126+
raise OSError
127+
return stdout, stderr
128+
129+
108130
@Driver.check_active
109131
@step(title='call', args=['args'])
110132
def _call(self, *args):
@@ -137,8 +159,22 @@ class SigrokDriver(SigrokCommon):
137159

138160
@Driver.check_active
139161
def capture(self, filename, samplerate="200k"):
162+
"""
163+
Starts to capture samples, in the background.
164+
165+
Args:
166+
filename: the path to the file where the capture is saved.
167+
samplerate: the sample-rate of the capture
168+
169+
Raises:
170+
RuntimeError() if a capture is already running.
171+
"""
172+
if self._running:
173+
raise RuntimeError("capture is already running")
174+
140175
self._filename = filename
141176
self._basename = os.path.basename(self._filename)
177+
self._validate_save_dir_exists()
142178
self.logger.debug(
143179
"Saving to: %s with basename: %s", self._filename, self._basename
144180
)
@@ -149,6 +185,7 @@ def capture(self, filename, samplerate="200k"):
149185
filename = os.path.join(self._tmpdir, self._basename)
150186
cmd.append(filename)
151187
self._call_with_driver(*cmd)
188+
152189
args = self.sigrok.command_prefix + ['test', '-e', filename]
153190

154191
while subprocess.call(args):
@@ -161,60 +198,207 @@ def capture(self, filename, samplerate="200k"):
161198
self.logger.debug("sigrok-cli call terminated prematurely with non-zero return-code")
162199
self.logger.debug("stdout: %s", stdout)
163200
self.logger.debug("stderr: %s", stderr)
164-
raise ExecutionError(f"sigrok-cli call terminated prematurely with return-code '{ret}'.")
201+
raise ExecutionError(f"sigrok-cli call terminated prematurely, return-code '{ret}'.")
165202
sleep(0.1)
166203

167204
self._running = True
168205

206+
@Driver.check_active
207+
def capture_for_time(self, filename, time_ms, samplerate="200k"):
208+
"""
209+
Captures samples for a specified time (ms).
210+
211+
Blocks while capturing.
212+
213+
Args:
214+
filename: the path to the file where the capture is saved.
215+
time: time (in ms) for capture duration
216+
samplerate: the sample-rate of the capture
217+
218+
Returns:
219+
The capture as a list containing dict's with fields "Time"
220+
and the channel names
221+
222+
Raises:
223+
OSError() if the subprocess returned with non-zero return-code
224+
"""
225+
return self._capture_blocking(filename, ["--time", str(time_ms)], samplerate)
226+
227+
@Driver.check_active
228+
def capture_samples(self, filename, samples, samplerate="200k"):
229+
"""
230+
Captures a specified number of samples.
231+
232+
Blocks while capturing.
233+
234+
Args:
235+
filename: the path to the file where the capture is saved.
236+
samples: number of samples to capture
237+
samplerate: the sample-rate of the capture
238+
239+
Returns:
240+
The capture as a list containing dict's with fields "Time"
241+
and the channel names
242+
"""
243+
return self._capture_blocking(filename, ["--samples", str(samples)], samplerate)
244+
169245
@Driver.check_active
170246
def stop(self):
171-
assert self._running
247+
"""
248+
Stops the capture and returns recorded samples.
249+
250+
Note that this method might block for several seconds because it needs
251+
to wait for output, parse the capture file and prepare the list
252+
containing the samples.
253+
254+
Returns:
255+
The capture as a list containing dict's with fields "Time"
256+
and the channel names
257+
258+
Raises:
259+
RuntimeError() if capture has not been started
260+
"""
261+
if not self._running:
262+
raise RuntimeError("no capture started yet")
172263
self._running = False
173-
fnames = ['time']
174-
fnames.extend(self.sigrok.channels.split(','))
264+
175265
csv_filename = f'{os.path.splitext(self._basename)[0]}.csv'
176266

177267
# sigrok-cli can be quit through any keypress
178268
stdout, stderr = self._process.communicate(input=b"q")
179269
self.logger.debug("stdout: %s", stdout)
180270
self.logger.debug("stderr: %s", stderr)
181271

182-
# Convert from .sr to .csv
272+
self._convert_sr_csv(os.path.join(self._tmpdir, self._basename),
273+
os.path.join(self._tmpdir, csv_filename))
274+
self._transfer_tmp_file(csv_filename)
275+
return self._process_csv(csv_filename)
276+
277+
def _capture_blocking(self, filename, capture_args, samplerate):
278+
self._filename = filename
279+
self._basename = os.path.basename(self._filename)
280+
self._validate_save_dir_exists()
281+
csv_filename = f'{os.path.splitext(self._basename)[0]}.csv'
282+
self.logger.debug(
283+
"Saving to: %s with basename: %s", self._filename, self._basename
284+
)
285+
cmd = [
286+
"-l", "4", "--config", f"samplerate={samplerate}",
287+
*capture_args, "-o"
288+
]
289+
filename = os.path.join(self._tmpdir, self._basename)
290+
cmd.append(filename)
291+
self._call_with_driver_blocking(*cmd, log_output=True)
292+
293+
args = self.sigrok.command_prefix + ['test', '-e', filename]
294+
295+
while subprocess.call(args):
296+
sleep(0.1)
297+
298+
self._convert_sr_csv(os.path.join(self._tmpdir, self._basename),
299+
os.path.join(self._tmpdir, csv_filename))
300+
self._transfer_tmp_file(csv_filename)
301+
return self._process_csv(csv_filename)
302+
303+
def _convert_sr_csv(self, file_path_sr, file_path_scv):
183304
cmd = [
184305
'-i',
185-
os.path.join(self._tmpdir, self._basename), '-O', 'csv:time=true', '-o',
186-
os.path.join(self._tmpdir, csv_filename)
306+
file_path_sr, '-O', 'csv:time=true', '-o',
307+
file_path_scv
187308
]
188309
self._call(*cmd)
189310
stdout, stderr = self._process.communicate()
190311
self.logger.debug("stdout: %s", stdout)
191312
self.logger.debug("stderr: %s", stderr)
313+
314+
def _validate_save_dir_exists(self):
315+
if not os.path.exists(os.path.dirname(self._filename)):
316+
raise ExecutionError(
317+
f"Won't be able to save capture file to target directory '{os.path.dirname(self._filename)}', does not exist"
318+
)
319+
320+
def _transfer_tmp_file(self, csv_filename):
192321
if isinstance(self.sigrok, NetworkSigrokUSBDevice):
193-
subprocess.call([
194-
'scp', f'{self.sigrok.host}:{os.path.join(self._tmpdir, self._basename)}',
195-
os.path.abspath(self._filename)
196-
],
197-
stdin=subprocess.DEVNULL,
198-
stdout=subprocess.DEVNULL,
199-
stderr=subprocess.DEVNULL)
322+
sr_remote_path = os.path.join(self._tmpdir, self._basename)
323+
sr_local_path = os.path.abspath(self._filename)
324+
csv_remote_path = os.path.join(self._tmpdir, csv_filename)
325+
csv_local_path = os.path.join(self._local_tmpdir, csv_filename)
326+
self.logger.debug(
327+
"Transferring sigrok capture file from remote path '%s' to local path '%s'",
328+
sr_remote_path,
329+
sr_local_path
330+
)
331+
# TODO: use NetworkResource ssh base options here too
332+
ret = subprocess.run([
333+
'scp',
334+
"-o",
335+
"UserKnownHostsFile=/dev/null",
336+
"-o",
337+
"StrictHostKeyChecking=no",
338+
f'{self.sigrok.host}:{sr_remote_path}',
339+
sr_local_path,
340+
],
341+
stdin=subprocess.DEVNULL,
342+
stdout=subprocess.PIPE,
343+
stderr=subprocess.STDOUT
344+
)
345+
if ret.returncode != 0:
346+
self.logger.error(
347+
"Transferring Sigrok capture file failed, return-code '%i : %s'",
348+
ret.returncode,
349+
ret.stdout.decode()
350+
)
351+
raise ExecutionError(
352+
f"Transferring Sigrok Capture file failed, return-code '{ret.returncode}'",
353+
stdout=ret.stdout.decode().split("\n")
354+
)
355+
200356
# get csv from remote host
201-
subprocess.call([
202-
'scp', f'{self.sigrok.host}:{os.path.join(self._tmpdir, csv_filename)}',
203-
os.path.join(self._local_tmpdir, csv_filename)
204-
],
205-
stdin=subprocess.DEVNULL,
206-
stdout=subprocess.DEVNULL,
207-
stderr=subprocess.DEVNULL)
357+
self.logger.debug(
358+
"Transferring CSV file from remote path '%s' to local path '%s'",
359+
csv_remote_path,
360+
csv_local_path
361+
)
362+
# TODO: use NetworkResource ssh base options here too
363+
ret = subprocess.run([
364+
'scp',
365+
"-o",
366+
"UserKnownHostsFile=/dev/null",
367+
"-o",
368+
"StrictHostKeyChecking=no",
369+
f'{self.sigrok.host}:{csv_remote_path}',
370+
csv_local_path
371+
],
372+
stdin=subprocess.DEVNULL,
373+
stdout=subprocess.PIPE,
374+
stderr=subprocess.STDOUT)
375+
if ret.returncode != 0:
376+
self.logger.error(
377+
"Transferring Sigrok CSV file failed, return-code '%i : %s'",
378+
ret.returncode,
379+
ret.stdout.decode()
380+
)
381+
raise ExecutionError(
382+
f"Transferring Sigrok CSV file failed, return-code '{ret.returncode}'",
383+
stdout=ret.stdout.decode().split("\n")
384+
)
385+
else:
386+
shutil.copyfile(
387+
os.path.join(self._tmpdir, self._basename), self._filename
388+
)
389+
390+
def _process_csv(self, csv_filename):
391+
fnames = ['time']
392+
fnames.extend(self.sigrok.channels.split(','))
393+
394+
if isinstance(self.sigrok, NetworkSigrokUSBDevice):
208395
with open(os.path.join(self._local_tmpdir,
209396
csv_filename)) as csv_file:
210397
# skip first 5 lines of the csv output, contains metadata and fieldnames
211398
for _ in range(0, 5):
212399
next(csv_file)
213400
return list(csv.DictReader(csv_file, fieldnames=fnames))
214401
else:
215-
shutil.copyfile(
216-
os.path.join(self._tmpdir, self._basename), self._filename
217-
)
218402
with open(os.path.join(self._tmpdir, csv_filename)) as csv_file:
219403
# skip first 5 lines of the csv output, contains metadata and fieldnames
220404
for _ in range(0, 5):
@@ -223,6 +407,15 @@ def stop(self):
223407

224408
@Driver.check_active
225409
def analyze(self, args, filename=None):
410+
"""
411+
Analyzes captured data through `sigrok-cli`'s command line interface
412+
413+
Args:
414+
args: args to `sigrok-cli`
415+
416+
Returns:
417+
A dictionary containing the matched groups.
418+
"""
226419
annotation_regex = re.compile(r'(?P<startnum>\d+)-(?P<endnum>\d+) (?P<decoder>[\w\-]+): (?P<annotation>[\w\-]+): (?P<data>".*)') # pylint: disable=line-too-long
227420
if not filename and self._filename:
228421
filename = os.path.join(self._tmpdir, self._basename)
@@ -412,12 +605,15 @@ def stop(self):
412605
413606
Raises:
414607
RuntimeError() if capture has not been started
608+
OSError() if the subprocess returned with non-zero return-code
415609
"""
416610
if not self._running:
417611
raise RuntimeError("no capture started yet")
418612
while not self._timeout.expired:
419613
if self._process.poll() is not None:
420614
# process has finished. no need to wait for the timeout
615+
if self._process.returncode != 0:
616+
raise OSError
421617
break
422618
time.sleep(0.1)
423619
else:

0 commit comments

Comments
 (0)