diff --git a/bliss/controllers/ah401.py b/bliss/controllers/ah401.py index 52287f3a6a6c3bae35f55a26ce5f6aef2d5b06c2..e67ad49981fa1cb1864138e804bdf8a538a144d6 100644 --- a/bliss/controllers/ah401.py +++ b/bliss/controllers/ah401.py @@ -6,7 +6,6 @@ # Distributed under the GNU LGPLv3. See LICENSE for more info. -import gevent import numpy import functools import enum @@ -18,7 +17,10 @@ from bliss.common.logtools import log_debug from bliss.config.settings import HashObjSetting from bliss.common.counter import SamplingCounter, SamplingMode from bliss.controllers.bliss_controller import BlissController -from bliss.controllers.counter import SamplingCounterController +from bliss.controllers.counter import ( + SamplingCounterController, + IntegratingCounterAcquisitionSlave, +) from bliss.scanning.acquisition.counter import SamplingCounterAcquisitionSlave @@ -33,6 +35,17 @@ class CountingMode(enum.IntEnum): AUTO = enum.auto() +@enum.unique +class TriggerMode(enum.IntEnum): + """CountingMode modes: + * SOFTWARE: 0 + * HARDWARE: 1 + """ + + SOFTWARE = 0 + HARDWARE = 1 + + def lazy_init(func): @functools.wraps(func) def func_wrapper(self, *args, **kwargs): @@ -95,7 +108,6 @@ class Ah401Device: VERSION = "AH401D" WEOL = "\r" REOL = "\r\n" - RTIMEOUT = 1 CMD2PARAM = { "ACQ": ("ON", "OFF"), @@ -117,20 +129,24 @@ class Ah401Device: def __init__(self, comconf): self.conf = comconf self.comm = None + self.saturation_warnings = True self._integration_time = None - self._data_offset = self.DEFAULT_OFFSET + self._trig_mode = TriggerMode.SOFTWARE + self._data_offset = numpy.array([self.DEFAULT_OFFSET] * 4) self._fsrange12 = None self._fsrange34 = None self._fsrval12 = None self._fsrval34 = None - self._bin_mode = None - self._acquiring = None - self._trig_mode = None + self._acquiring = False + self._bin_data_len = 12 + self._acq_stop_retry = 0 def __del__(self): self.__close__() def __close__(self): + if self._acquiring: + self.acquistion_stop() self._close_com() def _close_com(self): @@ -140,7 +156,7 @@ class Ah401Device: def _init_com(self): """Initialize communication or reset if already connected""" - self._close_com() + self._close_com() # close if already opened self.comm = get_comm( self.conf, ctype=TCP, eol=self.REOL, port=self.DEFAULT_PORT ) @@ -149,17 +165,13 @@ class Ah401Device: """Initialize/reset communication layer and synchronize with hardware""" self._init_com() - # stop possible running acq and clean com output - self._stop_acq(wtrig=False) - self._stop_acq(wtrig=True) - self._acquiring = False - self._trig_mode = False - # update internal params self.integration_time - self.bin_mode self.sum_mode + # force bin mode always + self.send_cmd("BIN", "ON") + if len(self.scale_range) == 2: self._model = "D" else: @@ -171,7 +183,9 @@ class Ah401Device: @data_offset.setter def data_offset(self, value): - self._data_offset = value + if not isinstance(value, (list, numpy.ndarray)): + raise ValueError("value must be a list of 4 items (one for each channel)") + self._data_offset = numpy.array(value) @property def baudrate(self): @@ -183,31 +197,6 @@ class Ah401Device: raise ValueError(f"baudrate must be in {self.CMD2PARAM['BDR']}") self.send_cmd("BDR", int(value)) - @property - def bin_mode(self): - """ - The purpose of this mode is to change the format of the digital data stream generated by the AH401D picoammeter. - The binary format ('bin_mode'=True) helps to improve the data rate transmission, as it avoids the overhead due to the ASCII format generation. - """ - if self.send_cmd("BIN", "?") == "ON": - self._bin_mode = True - else: - self._bin_mode = False - return self._bin_mode - - @bin_mode.setter - def bin_mode(self, enable): - """ - The purpose of this mode is to change the format of the digital data stream generated by the AH401D picoammeter. - The binary format ('bin_mode'=True) helps to improve the data rate transmission, as it avoids the overhead due to the ASCII format generation. - """ - if bool(enable): - self.send_cmd("BIN", "ON") - self._bin_mode = True - else: - self.send_cmd("BIN", "OFF") - self._bin_mode = False - @property def half_mode(self): """ @@ -300,6 +289,7 @@ class Ah401Device: raise ValueError( f"integration time must be in range {self.CMD2PARAM['ITM']} seconds" ) + raw_time = int(value * self.TIME_FACTOR) self.send_cmd("ITM", raw_time) self._integration_time = float(value) @@ -419,8 +409,10 @@ class Ah401Device: """ if self.send_cmd("SUM", "?") == "ON": self._sum_mode = True + self._bin_data_len = 16 else: self._sum_mode = False + self._bin_data_len = 12 return self._sum_mode @sum_mode.setter @@ -438,45 +430,80 @@ class Ah401Device: ) self.send_cmd("SUM", "ON") self._sum_mode = True + self._bin_data_len = 16 else: self.send_cmd("SUM", "OFF") self._sum_mode = False + self._bin_data_len = 12 @property def trigger_mode(self): """ - If the trigger mode is enabled (True) when staring an acquistion with the 'acquistion_start' command, - the Ah401 waits to receive a falling edge signal. As soon as this signal is detected, the AH401 starts to acquire data continuously. - - * If 'sample_number' == 0: - When a second signal is received, the acquisition is paused. Then, another signal will resume the acquisition (unpause). - This behavior is repeated until acquisition is stopped with the 'acquisition_stop' command. - - * If 'sample_number' != 0: - If the 'sample_number' is not zero, the acquisition automatically stops after the configured number of samples is acquired - and the instrument waits for a new TRIGGER signal. - Moreover, if the 'sum_mode' is enabled, only the summed values of the samples are returned. - This behaviour continues until acquisition is stopped with the 'acquisition_stop' command. + Return the current trigger mode: [SOFTWARE, HARDWARE] + + SOFTWARE: + + When staring an acquistion with the 'acquistion_start' command, the Ah401 starts to acquire data continuously. + + * If 'sample_number' == 0: + The Ah401 acquires data continuously until acquisition is stopped with the 'acquisition_stop' command. + + * If 'sample_number' != 0: + If the 'sample_number' is not zero, the acquisition automatically stops after the configured number of samples is acquired. + Moreover, if the 'sum_mode' is enabled, only the summed values of the samples are returned. + + HARDWARE: + + When staring an acquistion with the 'acquistion_start' command, the Ah401 waits to receive a falling edge signal. + As soon as this signal is detected, the AH401 starts to acquire data continuously. + + * If 'sample_number' == 0: + When a second signal is received, the acquisition is paused. Then, another signal will resume the acquisition (unpause). + This behavior is repeated until acquisition is stopped with the 'acquisition_stop' command. + + * If 'sample_number' != 0: + If the 'sample_number' is not zero, the acquisition automatically stops after the configured number of samples is acquired + and the instrument waits for a new TRIGGER signal. + Moreover, if the 'sum_mode' is enabled, only the summed values of the samples are returned. + This behaviour continues until acquisition is stopped with the 'acquisition_stop' command. """ return self._trig_mode @trigger_mode.setter - def trigger_mode(self, enable): + def trigger_mode(self, mode): """ - If the trigger mode is enabled (True) when staring an acquistion with the 'acquistion_start' command, - the Ah401 waits to receive a falling edge signal. As soon as this signal is detected, the AH401 starts to acquire data continuously. - - * If 'sample_number' == 0: - When a second signal is received, the acquisition is paused. Then, another signal will resume the acquisition (unpause). - This behavior is repeated until acquisition is stopped with the 'acquisition_stop' command. - - * If 'sample_number' != 0: - If the 'sample_number' is not zero, the acquisition automatically stops after the configured number of samples is acquired - and the instrument waits for a new TRIGGER signal. - Moreover, if the 'sum_mode' is enabled, only the summed values of the samples are returned. - This behaviour continues until acquisition is stopped with the 'acquisition_stop' command. + Set the trigger mode: [SOFTWARE, HARDWARE] + + SOFTWARE: + + When staring an acquistion with the 'acquistion_start' command, the Ah401 starts to acquire data continuously. + + * If 'sample_number' == 0: + The Ah401 acquires data continuously until acquisition is stopped with the 'acquisition_stop' command. + + * If 'sample_number' != 0: + If the 'sample_number' is not zero, the acquisition automatically stops after the configured number of samples is acquired. + Moreover, if the 'sum_mode' is enabled, only the summed values of the samples are returned. + + HARDWARE: + + When staring an acquistion with the 'acquistion_start' command, the Ah401 waits to receive a falling edge signal. + As soon as this signal is detected, the AH401 starts to acquire data continuously. + + * If 'sample_number' == 0: + When a second signal is received, the acquisition is paused. Then, another signal will resume the acquisition (unpause). + This behavior is repeated until acquisition is stopped with the 'acquisition_stop' command. + + * If 'sample_number' != 0: + If the 'sample_number' is not zero, the acquisition automatically stops after the configured number of samples is acquired + and the instrument waits for a new TRIGGER signal. + Moreover, if the 'sum_mode' is enabled, only the summed values of the samples are returned. + This behaviour continues until acquisition is stopped with the 'acquisition_stop' command. """ - self._trig_mode = bool(enable) + if isinstance(mode, TriggerMode): + self._trig_mode = mode + else: + self._trig_mode = TriggerMode[mode] @lazy_init def get_model(self): @@ -504,48 +531,115 @@ class Ah401Device: @lazy_init def get_info(self): + fsr12 = self.FULL_SCALE_RANGE[self._fsrange12] + fsr34 = self.FULL_SCALE_RANGE[self._fsrange34] + msg = f"\n=== AH401 Controller (model: {self._model}, version: {self.get_version()}) ===\n\n" # check_not_running done here ! - msg += f" Baudrate: {self.baudrate} \n" - msg += f" Binary mode: {self.bin_mode} \n" msg += f" Half mode: {self.half_mode} \n" msg += f" Sum mode: {self.sum_mode} \n" - msg += f" Trigger mode: {self.trigger_mode} \n" + msg += f" Trigger mode: {self.trigger_mode.name} \n" msg += f" Sample number: {self.sample_number} \n\n" msg += f" Integration time: {self.integration_time} s\n" - msg += f" Scale range ch1-ch2: {self.FULL_SCALE_RANGE[self._fsrange12][0]} {self.FULL_SCALE_RANGE[self._fsrange12][2]}\n" - msg += f" Scale range ch3-ch4: {self.FULL_SCALE_RANGE[self._fsrange34][0]} {self.FULL_SCALE_RANGE[self._fsrange34][2]}\n" + msg += f" Scale range ch1-ch2: {fsr12[0]} {fsr12[2]} (max {fsr12[0]/self._integration_time*fsr12[1]}A)\n" + msg += f" Scale range ch3-ch4: {fsr34[0]} {fsr34[2]} (max {fsr34[0]/self._integration_time*fsr34[1]}A)\n" msg += f" Acquisition status: {self.acquistion_status()}\n" return msg @lazy_init - def read_channels(self, timeout=None): + def read_channels(self, timeout=None, raw=False): + """Single readout of channels values. Use raw=True to return unconverted values.""" self._check_not_running() - with self.comm._lock: + with self.comm.lock: self._acquiring = True try: self.comm.write(b"GET ?\r") - values = self.read_data(timeout=timeout) + values = self.read_data(timeout=timeout, raw=raw) finally: self._acquiring = False return values + @lazy_init + def read_data(self, timeout=None, raw=False): + """Read channels data while an acquisition has been started with the 'acquistion_start' command. + Use raw=True to return unconverted values. + """ + rawdata = self._read_raw_data(timeout) + chanvals = self._raw2array(rawdata) + if self.saturation_warnings: + saturating_channels = list( + numpy.where(chanvals >= self.FULL_SCALE_MAX)[0] + 1 + ) + if saturating_channels: + print( + f"warning AH401 device is saturating on channels {saturating_channels} ({self})" + ) + if raw: + return chanvals + return self._convert_raw_data(chanvals) + + @lazy_init + def calibrate_data_offset(self, samples=10): + self._check_not_running() + curr_samp_num = self.sample_number + curr_sum_mode = self.sum_mode + data_offset = self.data_offset + try: + self.sample_number = samples + self.sum_mode = True + timeout = self._integration_time * samples * 1.5 + self.acquistion_start() + sum = self.read_data(timeout=timeout, raw=True) + data_offset = (sum / samples).astype(int) + finally: + self.acquistion_stop() + self.sample_number = curr_samp_num + self.sum_mode = curr_sum_mode + self.data_offset = data_offset + + @lazy_init def acquistion_start(self): self._check_not_running() - if self._trig_mode: + self._acq_stop_retry = 0 + log_debug( + self, f"Ah401 acquistion_start in {self._trig_mode.name} trigger mode" + ) + if self._trig_mode == TriggerMode.HARDWARE: self.send_cmd("TRG", "ON") else: self.send_cmd("ACQ", "ON") + self._acquiring = True def acquistion_stop(self): - if self._trig_mode: - self._stop_acq(wtrig=True) + if self._trig_mode == TriggerMode.HARDWARE: + msg = f"TRG OFF{self.WEOL}".encode() else: - self._stop_acq(wtrig=False) + msg = f"ACQ OFF{self.WEOL}".encode() + + ans = self.comm.write_readline(msg) + + if not ans.endswith(b"ACK"): + if ans.endswith(b"NAK") and self._acq_stop_retry < 4: + log_debug(self, f"Ah401 retry stop acquistion {self._acq_stop_retry}") + self._acq_stop_retry += 1 + return self.acquistion_stop() + + raise RuntimeError( + f'Error in acquistion_stop command with response "{ans}"' + ) + + if len(self.comm._data) != 0: + print( + f"Warning: unexpected remaining data have been flushed: {self.comm._data}" + ) + self.comm.flush() + self._acquiring = False + log_debug(self, "Ah401 acquistion_stopped") + return True def acquistion_status(self): if self._acquiring: @@ -580,100 +674,84 @@ class Ah401Device: raise RuntimeError(f"Error in command '{cmd}' with response '{ans}'") - @lazy_init - def read_data(self, timeout=None): + def _read_raw_data(self, timeout=None): timeout = timeout or self.comm._timeout # temp fix to handle modif in tcp.py - if not self._acquiring: - raise RuntimeError("start acquisition first") - - if self._bin_mode: - if self._sum_mode: - rawdata = self.comm.read(16, timeout=timeout) - else: - rawdata = self.comm.read(12, timeout=timeout) - else: - rawdata = self.comm.readline(timeout=timeout).decode() + return self.comm.read(self._bin_data_len, timeout=timeout) + + def _raw2array(self, rawdata): + if self._sum_mode: + return numpy.array( + [ + rawdata[0 + 4 * i] + + rawdata[1 + 4 * i] * 256 + + rawdata[2 + 4 * i] * 65536 + + rawdata[3 + 4 * i] * 16777216 + for i in range(4) + ], + dtype=float, + ) - return self._convert_raw_data(rawdata) + return numpy.array( + [ + rawdata[0 + 3 * i] + + rawdata[1 + 3 * i] * 256 + + rawdata[2 + 3 * i] * 65536 + for i in range(4) + ], + dtype=float, + ) def _convert_raw_data(self, rawdata): - if self._bin_mode: - if self._sum_mode: - chanvals = numpy.array( - [ - rawdata[0 + 4 * i] - + rawdata[1 + 4 * i] * 256 - + rawdata[2 + 4 * i] * 65536 - + rawdata[3 + 4 * i] * 16777216 - for i in range(4) - ], - dtype=float, - ) - else: - chanvals = numpy.array( - [ - rawdata[0 + 3 * i] - + rawdata[1 + 3 * i] * 256 - + rawdata[2 + 3 * i] * 65536 - for i in range(4) - ], - dtype=float, - ) - else: - chanvals = numpy.array(rawdata.split(" "), dtype=float) - - saturating_channels = list(numpy.where(chanvals >= self.FULL_SCALE_MAX)[0]) - if saturating_channels: - print( - f"warning AH401 device is saturating on channels {saturating_channels} ({self})" - ) - + """convert rawdata to a current value (A) + expect rawdata as a numpy array of 4 values (for each channel) + """ if self._model == "D": v1 = ( (self._fsrval12 / float(self.FULL_SCALE_MAX)) - * (chanvals[0:2] - self.data_offset) + * (rawdata[0:2] - self.data_offset[0:2]) / self._integration_time ) v2 = ( (self._fsrval34 / float(self.FULL_SCALE_MAX)) - * (chanvals[2:4] - self.data_offset) + * (rawdata[2:4] - self.data_offset[2:4]) / self._integration_time ) data = numpy.hstack((v1, v2)) else: data = ( (self._fsrval12 / float(self.FULL_SCALE_MAX)) - * (chanvals - self.data_offset) + * (rawdata - self.data_offset) / self._integration_time ) - log_debug(self, "convert_raw_data %s => %s", chanvals, data) - return data + def dump_data(self, wait_for_missing_chunk=True): + """Dump all data received until now. + If wait_for_missing_chunk is True and if current data buffer size is not + a multiple of bin_data_len, then it waits to receive the missing chunk. + """ + buf_len = len(self.comm._data) + to_dump = self._bin_data_len * (buf_len // self._bin_data_len) + if wait_for_missing_chunk: + if buf_len % self._bin_data_len != 0: + to_dump += self._bin_data_len + + dumpped = self.comm.read(to_dump) + log_debug(self, f"Ah401 dumpped {len(dumpped)}") + return dumpped + def _check_not_running(self): if self._acquiring: raise RuntimeError( "Cannot perform this action while acquisition is running, stop acquisition first" ) - def _stop_acq(self, wtrig=False, timeout=1): - if wtrig: - self.comm.write(b"TRG OFF\r") - else: - self.comm.write(b"ACQ OFF\r") - - ans = self.comm.readline(timeout=self.comm._timeout) - with gevent.Timeout(timeout): - while True: - if ans.endswith(b"ACK"): - break - ans += self.comm.readline(timeout=self.comm._timeout) - -class Ah401SCC(SamplingCounterController): +class Ah401CC(SamplingCounterController): def __init__(self, name, ah401): super().__init__(name, master_controller=None, register_counters=False) + self.max_sampling_frequency = 1000 self.ah401 = ah401 def read_all(self, *counters): @@ -687,12 +765,24 @@ class Ah401SCC(SamplingCounterController): values.append(data[cnt.channel - 1]) return values - def read(self, counter): - """Return the value of the given counter""" - return self.ah401.read_data()[counter.channel - 1] + def get_values(self, from_index, *counters): + cnt_values = [[] for cnt in counters] + + while len(self.ah401.comm._data) >= self.ah401._bin_data_len: + data = self.ah401.read_data() + for idx, cnt in enumerate(counters): + cnt_values[idx].append(data[cnt.channel - 1]) + + return cnt_values def get_acquisition_object(self, acq_params, ctrl_params, parent_acq_params): - return Ah401AcqSlave(self, ctrl_params=ctrl_params, **acq_params) + trigger_mode = acq_params.pop("trigger_mode", TriggerMode.SOFTWARE.name) + if trigger_mode == "SOFTWARE": + return Ah401SAS(self, ctrl_params=ctrl_params, **acq_params) + elif trigger_mode == "HARDWARE": + return Ah401IAS(self, ctrl_params=ctrl_params, **acq_params) + else: + raise ValueError(f"Unknown trigger mode {trigger_mode}") class Ah401Counter(SamplingCounter): @@ -711,17 +801,45 @@ class Ah401Counter(SamplingCounter): self.channel = int(channel) -class Ah401AcqSlave(SamplingCounterAcquisitionSlave): +class Ah401SAS(SamplingCounterAcquisitionSlave): def prepare_device(self): + self.device.ah401.trigger_mode = TriggerMode.SOFTWARE + self.device.ah401.sample_number = 0 + if self.device.ah401.counting_mode == CountingMode.AUTO: itime = self.count_time - itime = max(itime, self.device.CMD2PARAM["ITM"][0]) - itime = min(itime, self.device.CMD2PARAM["ITM"][1]) - self.device.integration_time = itime + itime = max(itime, self.device.ah401.CMD2PARAM["ITM"][0]) + itime = min(itime, self.device.ah401.CMD2PARAM["ITM"][1]) + self.device.ah401.integration_time = itime + elif self.count_time < self.device.ah401._integration_time: + raise ValueError( + f"count_time cannot be smaller than ah401 integration_time {self.device.ah401._integration_time}" + ) + + self.device.max_sampling_frequency = 1 / self.device.ah401._integration_time + + self.device.ah401.acquistion_start() def start_device(self): + pass # start in the prepare because timescan uses start_once=False + + def stop_device(self): + self.device.ah401.acquistion_stop() + + def trigger(self): + self.device.ah401.dump_data() + super().trigger() + + +class Ah401IAS(IntegratingCounterAcquisitionSlave): + def prepare_device(self): + self.device.ah401.trigger_mode = TriggerMode.HARDWARE + self.device.ah401.sample_number = 1 self.device.ah401.acquistion_start() + def start_device(self): + pass + def stop_device(self): self.device.ah401.acquistion_stop() @@ -772,22 +890,22 @@ class Ah401(Ah401Device, BlissController): - name: pico_ch1 channel: 1 mode: MEAN - unit: pA + unit: nA - name: pico_ch2 channel: 2 mode: MEAN - unit: pA + unit: nA - name: pico_ch3 channel: 3 mode: MEAN - unit: pA + unit: nA - name: pico_ch4 channel: 4 mode: MEAN - unit: pA + unit: nA """ @@ -796,10 +914,9 @@ class Ah401(Ah401Device, BlissController): BlissController.__init__(self, config) self._settings = HashObjSetting(f"{self.name}_settings") self._load_settings() - self._data_offset = self._get_setting("data_offset") + self.data_offset = self._get_setting("data_offset") - self._scc = Ah401SCC(self.name, self) - self._scc.max_sampling_frequency = 1 / self.integration_time + self._scc = Ah401CC(self.name, self) global_map.register(self, parents_list=["counters"]) @@ -875,6 +992,8 @@ class Ah401(Ah401Device, BlissController): elif unit == "nA": def conv_nA(x): + if isinstance(x, list): + return [i * 1e9 for i in x] return x * 1e9 convfunc = conv_nA @@ -927,14 +1046,18 @@ class Ah401(Ah401Device, BlissController): def __info__(self): """Return controller info as a string""" - return self.get_info() + txt = self.get_info() + txt += f" Counting mode: {self.counting_mode.name}\n" + return txt # === Persistant settings with caching (for minimal com with redis) ===================== def _load_settings(self): """Get from redis the persistent parameters (redis access)""" cached = {} - cached["data_offset"] = self._settings.get("data_offset", self.DEFAULT_OFFSET) + cached["data_offset"] = self._settings.get( + "data_offset", numpy.array([self.DEFAULT_OFFSET] * 4) + ) self._cached_settings = cached def _clear_settings(self): @@ -954,13 +1077,8 @@ class Ah401(Ah401Device, BlissController): @Ah401Device.data_offset.setter def data_offset(self, offset): - self._set_setting("data_offset", offset) Ah401Device.data_offset.fset(self, offset) - - @Ah401Device.integration_time.setter - def integration_time(self, value): - Ah401Device.integration_time.fset(self, value) - self._scc.max_sampling_frequency = 1 / self._integration_time + self._set_setting("data_offset", self.data_offset) @property def counting_mode(self): @@ -972,104 +1090,3 @@ class Ah401(Ah401Device, BlissController): self._counting_mode = mode else: self._counting_mode = CountingMode[mode] - - -class Ah401Mockup(Ah401): - def _init_com(self): - """Initialize communication or reset if already connected""" - self._close_com() - self.comm = FakeCom(self) - - self._params = { - "ACQ": "OFF", - "BDR": 9600, - "BIN": "OFF", - "HLF": "OFF", - "ITM": 100, # in hundreds of microsec (10ms) - "NAQ": 0, # 0=OFF - "RNG": "01", - "SUM": "OFF", - "TRG": "OFF", - "VER": "AH401D MockupVersion", - } - - -class FakeCom: - def __init__(self, ah401, **kwargs): - self._ah401 = ah401 - self._lock = gevent.lock.RLock() - self._buffer = gevent.queue.Queue() - self._eol = self._ah401.REOL - self._timeout = 5 - self._dtask = None - self._stop_event = gevent.event.Event() - - def close(self): - pass - - def read(self, size=1, timeout=None): - with self._lock: - with gevent.Timeout(timeout or self._timeout): - data = [str(self._buffer.get()) for i in range(size)] - return "".join(data).encode() - - def readline(self, eol=None, timeout=None): - with self._lock: - if eol is None: - eol = self._eol - - data = [] - leol = list(eol) - idx = len(leol) - with gevent.Timeout(timeout or self._timeout): - while data[-idx:] != leol: - data.append(str(self._buffer.get())) - - ans = "".join(data[:-idx]).encode() # remove eol like tcp.py does - return ans - - def write(self, msg): - with self._lock: - msg = msg.decode().strip() - cmd, arg = msg.split() - if arg == "?": - if cmd == "GET": - if self._ah401._bin_mode: - ans = b"\xcb\n\x00\xab\n\x00\xc8\n\x00\xef\n\x00" - else: - ans = "1111 2222 3333 4444\r\n" - else: - ans = f"{cmd} {self._ah401._params[cmd]}\r\n" - else: - ans = "ACK\r\n" - if cmd in ["ACQ", "TRG"] and arg == "OFF": - self._stop_data_task() # stop before ACK - - for x in ans: - self._buffer.put(x) - - if cmd in ["ACQ", "TRG"] and arg == "ON": - self._start_data_task() # start after ACK - - def write_readline(self, msg, write_synchro=None, eol=None, timeout=None): - with self._lock: - self.write(msg) - return self.readline(eol, timeout) - - def _start_data_task(self): - if not self._dtask: - self._stop_event.clear() - self._dtask = gevent.spawn(self._data_task) - - def _stop_data_task(self): - if self._dtask: - self._stop_event.set() - with gevent.Timeout(2.0): - self._dtask.join() - - def _data_task(self): - while not self._stop_event.is_set(): - data = "1111 2222 3333 4444\r\n" - for x in data: - self._buffer.put(x) - gevent.sleep(0.01) diff --git a/doc/docs/config_ah401.md b/doc/docs/config_ah401.md index ba8e35508b7646f7a45367c572424592050bc5a6..cb149f7884534d95c7a8db0469674fbca553db06 100644 --- a/doc/docs/config_ah401.md +++ b/doc/docs/config_ah401.md @@ -51,22 +51,22 @@ The AH401D uses a standard Ethernet TCP communication layer with the DEFAULT_POR - name: pico_ch1 channel: 1 mode: MEAN - unit: pA + unit: nA - name: pico_ch2 channel: 2 mode: MEAN - unit: pA + unit: nA - name: pico_ch3 channel: 3 mode: MEAN - unit: pA + unit: nA - name: pico_ch4 channel: 4 mode: MEAN - unit: pA + unit: nA ``` @@ -74,18 +74,12 @@ The AH401D uses a standard Ethernet TCP communication layer with the DEFAULT_POR ### Special modes and options -**bin_mode**: - -The purpose of this mode is to change the format of the digital data stream generated by the AH401D picoammeter. -The binary format ('bin_mode'=True) helps to improve the data rate transmission, as it avoids the overhead due to the ASCII format generation. - - **half_mode**: -The purpose of this mode is to select whether to process data from both integrator circuits (i.e. maximum speed, half_mode OFF) -or only from one integrator circuit (i.e. best noise performance, half_mode ON) of the AH401D. +The purpose of this mode is to select whether to process data from both integrator circuits (i.e. maximum speed, half_mode disabled) +or only from one integrator circuit (i.e. best noise performance, half_mode enabled) of the AH401D. -- If Half_mode is disabled (OFF): +- If `half_mode` is disabled (=False): The AH401D performs the current-to-voltage integration, continuously in time, using both channels (A and B) in parallel. when one of the integrator A is digitized by the ADC, the input channel is switched to the other integrator circuit (i.e. B) @@ -100,7 +94,7 @@ or only from one integrator circuit (i.e. best noise performance, half_mode ON) The drawback is a slightly higher noise level on the sampled values due to the integrator capacitors mismatch between A and B and to the charge injection introduced by the internal switches. -- If Half_mode is enabled (ON): +- If `half_mode` is enabled (=True): If lowest noise performance is of uttermost importance, the half mode mode must always be enabled. In this operation mode only the integrated current of one integrator (i.e. A) is sampled, digitized and sent to the host. @@ -109,18 +103,18 @@ or only from one integrator circuit (i.e. best noise performance, half_mode ON) then the value is digitized and sent to the host. During the following integration time (i.e. 100ms) no data is digitized (the value on the integrator B is discarded) and then the sequence repeats itself. Therefore a set of data is sent to the host every two integration times (i.e. 200ms). - The drawback of this mode is that only “half” sampled values are sent to the host and hence the sampling rate is halved. + The drawback of this mode is that only half sampled values are sent to the host and hence the sampling rate is halved. -Please note that the data rate throughput is directly related to the integration time and the “half mode” selection. -For example, setting the integration time to 10ms and the “half mode” to False (disabled) generates a data stream at 100Hz. -Whereas, setting the integration time to 10ms and the “half mode” to True (enabled), generates a data stream at 50Hz. +Please note that the data rate throughput is directly related to the integration time and the `half_mode` selection. +For example, setting the integration time to 10ms and the `half_mode` to False (disabled) generates a data stream at 100Hz. +Whereas, setting the integration time to 10ms and the `half_mode` to True (enabled), generates a data stream at 50Hz. **sample_number**: The purpose of the sample_number is to define a fixed number of samples to acquire after starting an acquisition with -the 'acquistion_start' command. Once the samples are acquired the acquisition stops automatically. +the `acquistion_start` command. Once the samples are acquired the acquisition stops automatically. The sample number should be in range [1, 2e7] or set to 0 to disable this behavior and allow continous data acquisition. -If a number of acquisitions larger than 4096 is set, the 'sum_mode' is automatically disabled. +If a number of acquisitions larger than 4096 is set, the `sum_mode` is automatically disabled. **scale_range**: @@ -132,30 +126,45 @@ If value has a single digit, it is applied to all channels. **sum_mode**: -The purpose of this mode is to add the values of “N” data samples configured with the 'sample_number' command +The purpose of this mode is to add the values of `N` data samples configured with the `sample_number` command and hence to get a single channel value representing the summed samples. In order to avoid data overflow, the sum mode cannot be enabled if the number of acquisitions -set via the 'sample_number' command is larger than 4096. +set via the `sample_number` command is larger than 4096. **trigger_mode**: -If the trigger mode is enabled (True) when staring an acquistion with the 'acquistion_start' command, -the Ah401 waits to receive a falling edge signal. As soon as this signal is detected, the AH401 starts to acquire data continuously. +- `SOFTWARE`: + + When staring an acquistion with the `acquistion_start` command, the Ah401 starts to acquire data continuously. + + * If `sample_number` == 0: -- If 'sample_number' == 0: + The Ah401 acquires data continuously until acquisition is stopped with the `acquisition_stop` command. - When a second signal is received, the acquisition is paused. Then, another signal will resume the acquisition (unpause). - This behavior is repeated until acquisition is stopped with the 'acquisition_stop' command. + * If `sample_number` != 0: -- If 'sample_number' != 0: + If the `sample_number` is not zero, the acquisition automatically stops after the configured number of samples is acquired. + Moreover, if the `sum_mode` is enabled, only the summed values of the samples are returned. - If the 'sample_number' is not zero, the acquisition automatically stops after the configured number of samples is acquired - and the instrument waits for a new TRIGGER signal. - Moreover, if the 'sum_mode' is enabled, only the summed values of the samples are returned. - This behaviour continues until acquisition is stopped with the 'acquisition_stop' command. +- `HARDWARE`: + + When staring an acquistion with the `acquistion_start` command, the Ah401 waits to receive a falling edge signal. + As soon as this signal is detected, the AH401 starts to acquire data continuously. + + * If `sample_number` == 0: + + When a second signal is received, the acquisition is paused. Then, another signal will resume the acquisition (unpause). + This behavior is repeated until acquisition is stopped with the `acquisition_stop` command. + + * If `sample_number` != 0: + + If the `sample_number` is not zero, the acquisition automatically stops after the configured number of samples is acquired + and the instrument waits for a new TRIGGER signal. + Moreover, if the `sum_mode` is enabled, only the summed values of the samples are returned. + This behaviour continues until acquisition is stopped with the `acquisition_stop` command. **counting_mode**: -If this mode is set to 'STD' the integration_time of the ah401 is decoupled from the count_time value passed to a scan command. -If this mode is set to 'AUTO', the count_time value of a scan command is applied as the ah401 integration time (if in the range [1ms, 1s]). \ No newline at end of file +If this mode is set to `STD` the integration_time of the ah401 is decoupled from the count_time value passed to a scan command. +If this mode is set to `AUTO`, the count_time value of a scan command is applied as the ah401 integration time (if in the range [1ms, 1s]). \ No newline at end of file diff --git a/doc/mkdocs.yml b/doc/mkdocs.yml index 758c604c07e683c95eb5c1148a3696c20edc0432..2846000f1a16c7ef30450f09616e2b929ecc244c 100755 --- a/doc/mkdocs.yml +++ b/doc/mkdocs.yml @@ -28,7 +28,6 @@ nav: - Actuators: config_actuator.md - AH401: config_ah401.md - Andeen-Hagerling 2550A capacitance bridge: config_ah2550a.md - - AH401 picoammeter: config_ah401.md - APC: config_apc.md - AutoFilter: config_autofilter.md - BCDU8: config_bcdu8.md diff --git a/tests/controllers_sw/test_ah401.py b/tests/controllers_sw/test_ah401.py deleted file mode 100644 index 5d3fc3da951dc1ef2cc654b41894e5de2ceb9735..0000000000000000000000000000000000000000 --- a/tests/controllers_sw/test_ah401.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of the bliss project -# -# Copyright (c) 2015-2022 Beamline Control Unit, ESRF -# Distributed under the GNU LGPLv3. See LICENSE for more info. - -from bliss.common.scans import loopscan - - -def test_ah401(session): - p1 = session.config.get( - "pico_ch1" - ) # check loading the counter also init the ah401 controller - ah = p1._counter_controller.ah401 - - # check params and com via get_info - print(ah.get_info()) - - # check simple reading of the 4 channels - data = ah.read_channels() - assert len(data) == 4 - - # check std scans - loopscan(3, 0.1, ah) - loopscan(3, 0.1, p1) - - ah._close_com() diff --git a/tests/test_configuration/ah401.yml b/tests/test_configuration/ah401.yml deleted file mode 100644 index 2eba67ca2fe2c8a7b1ef72bedcca21f542c35e81..0000000000000000000000000000000000000000 --- a/tests/test_configuration/ah401.yml +++ /dev/null @@ -1,34 +0,0 @@ -controller: - - - name: ah401 - class: Ah401Mockup - plugin: generic - module: ah401 - - tcp: - #url: 'bm32pico2:10001' - #url: '160.103.123.129:10001' - url: 'xxx' - - counting_mode: STD - - counters: - - name: pico_ch1 - channel: 1 - mode: MEAN - unit: pA - - - name: pico_ch2 - channel: 2 - mode: MEAN - unit: pA - - - name: pico_ch3 - channel: 3 - mode: MEAN - unit: pA - - - name: pico_ch4 - channel: 4 - mode: MEAN - unit: pA