Skip to content

Commit c645262

Browse files
committed
Add support for HyperBB truncated serial data output
+ Add option to select HyperBB firmware v1 or v2 + v1: full output (older firmware) + v2: truncated output (newer firmware) + Adjusted HyperBB data parser + Add support for any type of content in "combobox_" from Setup modal + Fix bug in Widget Select Active Timeseries Variables preventing user to adequately select variables after setting up the instrument from the setup button (it would work if the setup was never saved during the current the session). Affected HyperBB, LISST, and Satlantic (e.g. HyperNav) instruments.
1 parent b427e39 commit c645262

File tree

5 files changed

+112
-49
lines changed

5 files changed

+112
-49
lines changed

inlinino/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import numpy as np
99

1010

11-
__version__ = '2.9.14.gamma'
11+
__version__ = '2.9.14.delta'
1212

1313
# Setup Logger
1414
logging.basicConfig(level=logging.DEBUG)

inlinino/gui.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,8 @@ def on_new_ts_data(self, data, timestamp, reset=False):
469469
self.reset_ts(data, reset)
470470
n = len(data)
471471
# Update buffers
472+
if n == 0: # Nothing to update (won't trigger reset)
473+
return
472474
self._buffer_timestamp.extend(timestamp)
473475
for i in range(n):
474476
self._buffer_data[i].extend(data[i])
@@ -889,8 +891,18 @@ def act_save(self):
889891
elif field_prefix == 'combobox':
890892
if getattr(self, f).currentText() == 'on':
891893
self.cfg[field_name] = True
892-
else:
894+
elif getattr(self, f).currentText() == 'off':
893895
self.cfg[field_name] = False
896+
else:
897+
value = getattr(self, f).currentText()
898+
try:
899+
value = int(value)
900+
except ValueError:
901+
try:
902+
value = float(value)
903+
except ValueError:
904+
pass
905+
self.cfg[field_name] = value
894906
elif field_prefix == 'cb':
895907
self.cfg[field_name] = getattr(self, f).isChecked()
896908
if self.cfg['module'] == 'dataq':
@@ -1140,19 +1152,23 @@ def __init__(self, uuid, parent=None):
11401152
else:
11411153
getattr(self, 'te_' + k).setPlainText(v)
11421154
elif hasattr(self, 'combobox_' + k):
1143-
if v:
1144-
getattr(self, 'combobox_' + k).setCurrentIndex(0)
1155+
cb = getattr(self, 'combobox_' + k)
1156+
if isinstance(v, bool):
1157+
cb.setCurrentIndex(0 if v else 1)
11451158
else:
1146-
getattr(self, 'combobox_' + k).setCurrentIndex(1)
1159+
try:
1160+
cb.setCurrentIndex([cb.itemText(i) for i in range(cb.count())].index(str(v)))
1161+
except ValueError:
1162+
logger.warning(f'Value {v} not available in GUI for parameter {k}. GUI set to GUI default.')
11471163
elif hasattr(self, 'dsb_' + k):
11481164
# double spin box
1149-
getattr(self, 'dsb_' + k).setValue(self.cfg[k])
1165+
getattr(self, 'dsb_' + k).setValue(v)
11501166
elif hasattr(self, 'sb_' + k):
11511167
# spin box
1152-
getattr(self, 'sb_' + k).setValue(self.cfg[k])
1168+
getattr(self, 'sb_' + k).setValue(v)
11531169
elif hasattr(self, 'cb_' + k):
11541170
# check boxes
1155-
getattr(self, 'cb_' + k).setChecked(self.cfg[k])
1171+
getattr(self, 'cb_' + k).setChecked(v)
11561172
# Populate special fields specific to each module
11571173
if self.cfg['module'] == 'dataq':
11581174
for c in self.cfg['channels_enabled']:

inlinino/instruments/hyperbb.py

+86-41
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def __init__(self, uuid, cfg, signal, *args, **kwargs):
2121
# Instrument Specific Attributes
2222
self._parser: Optional[HyperBBParser] = None
2323
self.signal_reconstructed = None
24+
self.invalid_packet_alarm_triggered = False
2425
# Default serial communication parameters
2526
self.default_serial_baudrate = 9600
2627
self.default_serial_timeout = 1
@@ -49,7 +50,9 @@ def setup(self, cfg):
4950
raise ValueError('Missing calibration plaque file (*.mat)')
5051
if 'temperature_file' not in cfg.keys():
5152
raise ValueError('Missing calibration temperature file (*.mat)')
52-
self._parser = HyperBBParser(cfg['plaque_file'], cfg['temperature_file'])
53+
if 'firmware_version' not in cfg.keys():
54+
cfg['firmware_version'] = 1
55+
self._parser = HyperBBParser(cfg['plaque_file'], cfg['temperature_file'], cfg['firmware_version'])
5356
self.signal_reconstructed = np.empty(len(self._parser.wavelength)) * np.nan
5457
# Overload cfg with received data
5558
prod_var_names = ['beta_u', 'bb']
@@ -65,13 +68,25 @@ def setup(self, cfg):
6568
self.spectrum_plot_x_values = [self._parser.wavelength]
6669
# Update Active Timeseries Variables
6770
self.widget_active_timeseries_variables_names = ['beta(%d)' % x for x in self._parser.wavelength]
71+
self.widget_active_timeseries_variables_selected = []
6872
self.active_timeseries_wavelength = np.zeros(len(self._parser.wavelength), dtype=bool)
6973
for wl in np.arange(450, 700, 50):
7074
channel_name = 'beta(%d)' % self._parser.wavelength[np.argmin(np.abs(self._parser.wavelength - wl))]
7175
self.update_active_timeseries_variables(channel_name, True)
76+
# Reset Alarm
77+
self.invalid_packet_alarm_triggered = False
7278

7379
def parse(self, packet):
74-
return self._parser.parse(packet)
80+
if len(packet) == 0: # Empty lines on firmware v2 at end of wavelength scan
81+
return []
82+
data = self._parser.parse(packet)
83+
if len(data) == 0:
84+
self.signal.packet_corrupted.emit()
85+
if self.invalid_packet_alarm_triggered is False:
86+
self.invalid_packet_alarm_triggered = True
87+
self.logger.warning('Unable to parse frame. Check firmware version.')
88+
self.signal.alarm_custom.emit('Unable to parse frame.', 'Check HyperBB firmware version in "Setup".')
89+
return data
7590

7691
def handle_data(self, raw, timestamp):
7792
beta_u, bb, wl, gain, net_ref_zero_flag = self._parser.calibrate(np.array([raw], dtype=float))
@@ -122,27 +137,44 @@ def update_active_timeseries_variables(self, name, state):
122137
['beta(%d)' % wl for wl in self._parser.wavelength[self.active_timeseries_wavelength]]
123138

124139

125-
class MetaHyperBBParser(type):
126-
def __init__(cls, name, bases, dct):
127-
cls.FRAME_VARIABLES = ['ScanIdx', 'DataIdx', 'Date', 'Time', 'StepPos', 'wl', 'LedPwr', 'PmtGain', 'NetSig1',
128-
'SigOn1', 'SigOn1Std', 'RefOn', 'RefOnStd', 'SigOff1', 'SigOff1Std', 'RefOff',
129-
'RefOffStd', 'SigOn2', 'SigOn2Std', 'SigOn3', 'SigOn3Std', 'SigOff2', 'SigOff2Std',
130-
'SigOff3', 'SigOff3Std', 'LedTemp', 'WaterTemp', 'Depth', 'Debug1', 'zDistance']
131-
cls.FRAME_TYPES = [int, int, str, str, int, int, int, int, int,
132-
float, float, float, float, float, float, float,
133-
float, float, float, float, float, float, float,
134-
float, float, float, float, float, int, int]
135-
# FRAME_PRECISIONS = ['%d', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%d',
136-
# '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f',
137-
# '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f',
138-
# '%.1f', '%.1f', '%.2f', '%.2f', '%.2f', '%d', '%d']
139-
cls.FRAME_PRECISIONS = ['%s'] * len(cls.FRAME_VARIABLES)
140-
for x in cls.FRAME_VARIABLES:
141-
setattr(cls, f'idx_{x}', cls.FRAME_VARIABLES.index(x))
142-
140+
class HyperBBParser():
141+
def __init__(self, plaque_cal_file, temperature_cal_file, firmware_version=1):
142+
# Frame Parser
143+
self.firmware_version = firmware_version
144+
if firmware_version == 1:
145+
self.FRAME_VARIABLES = ['ScanIdx', 'DataIdx', 'Date', 'Time', 'StepPos', 'wl', 'LedPwr', 'PmtGain', 'NetSig1',
146+
'SigOn1', 'SigOn1Std', 'RefOn', 'RefOnStd', 'SigOff1', 'SigOff1Std', 'RefOff',
147+
'RefOffStd', 'SigOn2', 'SigOn2Std', 'SigOn3', 'SigOn3Std', 'SigOff2', 'SigOff2Std',
148+
'SigOff3', 'SigOff3Std', 'LedTemp', 'WaterTemp', 'Depth', 'Debug1', 'zDistance']
149+
self.FRAME_TYPES = [int, int, str, str, int, int, int, int, int,
150+
float, float, float, float, float, float, float,
151+
float, float, float, float, float, float, float,
152+
float, float, float, float, float, int, int]
153+
# FRAME_PRECISIONS = ['%d', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%d',
154+
# '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f',
155+
# '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f',
156+
# '%.1f', '%.1f', '%.2f', '%.2f', '%.2f', '%d', '%d']
157+
self.FRAME_PRECISIONS = ['%s'] * len(self.FRAME_VARIABLES)
158+
for x in self.FRAME_VARIABLES:
159+
setattr(self, f'idx_{x}', self.FRAME_VARIABLES.index(x))
160+
elif firmware_version == 2:
161+
self.FRAME_VARIABLES = ['ScanIdx', 'Date', 'Time', 'wl', 'PmtGain',
162+
'NetRef', 'NetSig1', 'NetSig2', 'NetSig3',
163+
'LedTemp', 'WaterTemp', 'Depth', 'SupplyVolt', 'ChSaturated']
164+
self.FRAME_TYPES = [int, str, str, int, int,
165+
float, float, float, float, float,
166+
float, float, float, float, float]
167+
# FRAME_PRECISIONS = ['%d', '%d', '%s', '%s', '%d', '%d', '%d', '%d', '%d',
168+
# '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f',
169+
# '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f', '%.1f',
170+
# '%.1f', '%.1f', '%.2f', '%.2f', '%.2f', '%d', '%d']
171+
self.FRAME_PRECISIONS = ['%s'] * len(self.FRAME_VARIABLES)
172+
for x in self.FRAME_VARIABLES:
173+
setattr(self, f'idx_{x}', self.FRAME_VARIABLES.index(x))
174+
else:
175+
raise ValueError('Firmware version not supported.')
143176

144-
class HyperBBParser(metaclass=MetaHyperBBParser):
145-
def __init__(self, plaque_cal_file, temperature_cal_file):
177+
# Instrument Specific Attributes
146178
self._theta = float('nan')
147179
self.Xp = float('nan')
148180

@@ -229,22 +261,29 @@ def calibrate(self, raw):
229261
raw = np.delete(raw, sel, axis=0)
230262
# Shortcuts
231263
wl = raw[:, self.idx_wl]
232-
# Remove saturated reading
233-
raw[raw[:, self.idx_SigOn1] > self.saturation_level, self.idx_SigOn1] = np.nan
234-
raw[raw[:, self.idx_SigOn2] > self.saturation_level, self.idx_SigOn2] = np.nan
235-
raw[raw[:, self.idx_SigOn3] > self.saturation_level, self.idx_SigOn3] = np.nan
236-
raw[raw[:, self.idx_SigOff1] > self.saturation_level, self.idx_SigOff1] = np.nan
237-
raw[raw[:, self.idx_SigOff2] > self.saturation_level, self.idx_SigOff2] = np.nan
238-
raw[raw[:, self.idx_SigOff3] > self.saturation_level, self.idx_SigOff3] = np.nan
239-
# Calculate net signal for ref, low gain (2), high gain (3)
240-
net_ref = raw[:, self.idx_RefOn] - raw[:, self.idx_RefOff]
241-
net_sig2 = raw[:, self.idx_SigOn2] - raw[:, self.idx_SigOff2]
242-
net_sig3 = raw[:, self.idx_SigOn3] - raw[:, self.idx_SigOff3]
243-
net_ref_zero_flag = np.any(net_ref == 0)
244-
net_ref[net_ref == 0] = np.nan
245-
scat1 = raw[:, self.idx_NetSig1] / net_ref
246-
scat2 = net_sig2 / net_ref
247-
scat3 = net_sig3 / net_ref
264+
if self.firmware_version == 1:
265+
# Remove saturated reading
266+
raw[raw[:, self.idx_SigOn1] > self.saturation_level, self.idx_SigOn1] = np.nan
267+
raw[raw[:, self.idx_SigOn2] > self.saturation_level, self.idx_SigOn2] = np.nan
268+
raw[raw[:, self.idx_SigOn3] > self.saturation_level, self.idx_SigOn3] = np.nan
269+
raw[raw[:, self.idx_SigOff1] > self.saturation_level, self.idx_SigOff1] = np.nan
270+
raw[raw[:, self.idx_SigOff2] > self.saturation_level, self.idx_SigOff2] = np.nan
271+
raw[raw[:, self.idx_SigOff3] > self.saturation_level, self.idx_SigOff3] = np.nan
272+
# Calculate net signal for ref, low gain (2), high gain (3)
273+
net_ref = raw[:, self.idx_RefOn] - raw[:, self.idx_RefOff]
274+
net_sig2 = raw[:, self.idx_SigOn2] - raw[:, self.idx_SigOff2]
275+
net_sig3 = raw[:, self.idx_SigOn3] - raw[:, self.idx_SigOff3]
276+
net_ref_zero_flag = np.any(net_ref == 0)
277+
net_ref[net_ref == 0] = np.nan
278+
scat1 = raw[:, self.idx_NetSig1] / net_ref
279+
scat2 = net_sig2 / net_ref
280+
scat3 = net_sig3 / net_ref
281+
else: # Assume firmware 2
282+
net_ref_zero_flag = np.any(raw[:, self.idx_NetRef] == 0)
283+
# TODO Check if need to flag saturated (ChSaturated)
284+
scat1 = raw[:, self.idx_NetSig1] / raw[:, self.idx_NetRef]
285+
scat2 = raw[:, self.idx_NetSig2] / raw[:, self.idx_NetRef]
286+
scat3 = raw[:, self.idx_NetSig3] / raw[:, self.idx_NetRef]
248287
# Subtract dark offset
249288
scat1_dark_removed = scat1 - self.f_dark_cal_scat_1(raw[:, self.idx_PmtGain], wl)
250289
scat2_dark_removed = scat2 - self.f_dark_cal_scat_2(raw[:, self.idx_PmtGain], wl)
@@ -264,9 +303,15 @@ def calibrate(self, raw):
264303
scatx_corrected[np.isnan(scatx_corrected)] = scat2_t_corrected[np.isnan(scatx_corrected)] # otherwise low gain
265304
scatx_corrected[np.isnan(scatx_corrected)] = scat1_t_corrected[np.isnan(scatx_corrected)] # otherwise raw pmt
266305
# Keep gain setting
267-
gain = np.ones((len(raw), 1)) * 3
268-
gain[np.isnan(raw[:, self.idx_SigOn3])] = 2
269-
gain[np.isnan(raw[:, self.idx_SigOn2])] = 1
306+
if self.firmware_version == 1:
307+
gain = np.ones((len(raw), 1)) * 3
308+
gain[np.isnan(raw[:, self.idx_SigOn3])] = 2
309+
gain[np.isnan(raw[:, self.idx_SigOn2])] = 1
310+
else:
311+
# TODO Check if method is correct based on documentation
312+
gain = np.ones((len(raw), 1)) * 3
313+
gain[raw[:, self.idx_ChSaturated] == 3] = 2
314+
gain[raw[:, self.idx_ChSaturated] == 2] = 1
270315
# Calculate beta
271316
uwl = np.unique(wl)
272317
# mu = pchip_interpolate(self.wavelength, self.mu, uwl) # Optimized as no need of interpolation as same wavelength as calibration

inlinino/instruments/lisst.py

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def setup(self, cfg):
6868
self.spectrum_plot_x_values = [np.log10(self._parser.angles)]
6969
# Update Active Timeseries Variables
7070
self.widget_active_timeseries_variables_names = ['beta(%.5f)' % x for x in self._parser.angles]
71+
self.widget_active_timeseries_variables_selected = []
7172
self.active_timeseries_angles = np.zeros(len(self._parser.angles), dtype=bool)
7273
for theta in [0.08, 0.32, 1.28, 5.12]:
7374
channel_name = 'beta(%.5f)' % self._parser.angles[np.argmin(np.abs(self._parser.angles - theta))]

inlinino/instruments/satlantic.py

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def setup(self, cfg):
133133
# TODO if no core variables disable spectrum plot widget
134134
# Update Active Timeseries Variables
135135
self.widget_active_timeseries_variables_names = []
136+
self.widget_active_timeseries_variables_selected = []
136137
self.active_timeseries_variables = []
137138
for head, cal in self._parser.cal.items():
138139
self.widget_active_timeseries_variables_names += [f'{head}_{k}' for k in cal.key if k not in self.KEYS_TO_IGNORE]

0 commit comments

Comments
 (0)