Skip to content

Commit 651f8de

Browse files
authored
Merge pull request #103 from DCC-Lab/fix/oceaninsight-getspectrum-hang
Fix intermittent hang in OISpectrometer.getSpectrum
2 parents 0e1e996 + 56a6d28 commit 651f8de

2 files changed

Lines changed: 84 additions & 13 deletions

File tree

hardwarelibrary/spectrometers/oceaninsight.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -473,18 +473,29 @@ def getSpectrumData(self):
473473
"""
474474
raise NotImplementedError('You must implemented getSpectrumData for your subclass.')
475475

476-
def getSpectrum(self, integrationTime=None):
476+
def getSpectrum(self, integrationTime=None, maxRequests=4, maxWait=2.0):
477477
""" Obtain a spectrum from the spectrometer. This implies:
478478
1- changing the integration time if needed.
479479
2- requesting a spectrum,
480-
3- waiting until ready, then
480+
3- waiting until ready, then
481481
4- actually retrieving and returning the data.
482-
482+
483+
The wait is bounded: at most `maxRequests` requests are issued, each
484+
awaited up to `maxWait` seconds. Rather than looping forever when the
485+
'spectrum ready' flag never rises (a transient USB glitch can leave it
486+
stuck), SpectrumRequestTimeoutError is raised in bounded time. Transient
487+
USB errors raised by requestSpectrum()/isSpectrumReady() are absorbed
488+
and retried instead of propagating on the first hiccup.
489+
483490
Parameters
484491
----------
485-
integrationTime: int, default None
492+
integrationTime: int, default None
486493
integration time in milliseconds if not the currently configured
487494
time.
495+
maxRequests: int, default 4
496+
maximum number of spectrum requests before giving up.
497+
maxWait: float, default 2.0
498+
seconds to wait for the 'spectrum ready' flag after each request.
488499
489500
Returns
490501
-------
@@ -496,15 +507,22 @@ def getSpectrum(self, integrationTime=None):
496507
if integrationTime is not None:
497508
self.setIntegrationTime(integrationTime)
498509

499-
self.requestSpectrum()
500-
timeOut = time.time() + 1
501-
while not self.isSpectrumReady():
502-
time.sleep(0.001)
503-
if time.time() > timeOut:
504-
self.requestSpectrum() # makes no sense, let's request another one
505-
timeOut = time.time() + 1
506-
507-
return self.getSpectrumData()
510+
lastError = None
511+
for _ in range(maxRequests):
512+
try:
513+
self.requestSpectrum()
514+
deadline = time.time() + maxWait
515+
while time.time() < deadline:
516+
if self.isSpectrumReady():
517+
return self.getSpectrumData()
518+
time.sleep(0.001)
519+
except usb.core.USBError as err: # includes USBTimeoutError
520+
lastError = err
521+
time.sleep(0.02)
522+
523+
raise SpectrumRequestTimeoutError(
524+
"Spectrum never became ready after {0} requests "
525+
"(last USB error: {1})".format(maxRequests, lastError))
508526

509527
def sendCommand(self, cmdBytes, payloadBytes=None):
510528
""" Main entry point to write to the device in order to have
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import env
2+
import unittest
3+
import time
4+
5+
from hardwarelibrary.spectrometers.oceaninsight import (
6+
OISpectrometer, SpectrumRequestTimeoutError,
7+
)
8+
9+
10+
class MockOISpectrometer(OISpectrometer):
11+
"""Hardware-free OISpectrometer for exercising getSpectrum() without USB.
12+
13+
Bypasses the USB-connecting __init__ and stubs the three hooks getSpectrum
14+
relies on. `spectrumReady` controls whether the 'spectrum ready' flag rises.
15+
"""
16+
17+
def __init__(self, spectrumReady=False):
18+
self.spectrumReady = spectrumReady
19+
self.requestCount = 0
20+
21+
def requestSpectrum(self):
22+
self.requestCount += 1
23+
24+
def isSpectrumReady(self):
25+
return self.spectrumReady
26+
27+
def getSpectrumData(self):
28+
return "spectrum"
29+
30+
31+
class TestGetSpectrumBounded(unittest.TestCase):
32+
def testRaisesInsteadOfBlockingWhenNeverReady(self):
33+
device = MockOISpectrometer(spectrumReady=False)
34+
start = time.time()
35+
with self.assertRaises(SpectrumRequestTimeoutError):
36+
device.getSpectrum(maxRequests=2, maxWait=0.05)
37+
elapsed = time.time() - start
38+
self.assertLess(elapsed, 10.0)
39+
40+
def testStopsAfterMaxRequests(self):
41+
device = MockOISpectrometer(spectrumReady=False)
42+
with self.assertRaises(SpectrumRequestTimeoutError):
43+
device.getSpectrum(maxRequests=2, maxWait=0.05)
44+
self.assertEqual(device.requestCount, 2)
45+
46+
def testReturnsDataWhenReady(self):
47+
device = MockOISpectrometer(spectrumReady=True)
48+
self.assertEqual(device.getSpectrum(maxRequests=2, maxWait=0.05), "spectrum")
49+
self.assertEqual(device.requestCount, 1)
50+
51+
52+
if __name__ == "__main__":
53+
unittest.main()

0 commit comments

Comments
 (0)