pyglaze 0.2.2__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyglaze/device/ampcom.py CHANGED
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
7
7
  from enum import Enum
8
8
  from functools import cached_property
9
9
  from math import modf
10
- from typing import TYPE_CHECKING, Callable, ClassVar, overload
10
+ from typing import TYPE_CHECKING, Callable, ClassVar
11
11
 
12
12
  import numpy as np
13
13
  import serial
@@ -16,7 +16,6 @@ from serial import serialutil
16
16
 
17
17
  from pyglaze.device.configuration import (
18
18
  DeviceConfiguration,
19
- ForceDeviceConfiguration,
20
19
  Interval,
21
20
  LeDeviceConfiguration,
22
21
  )
@@ -24,12 +23,8 @@ from pyglaze.devtools.mock_device import _mock_device_factory
24
23
  from pyglaze.helpers.utilities import LOGGER_NAME, _BackoffRetry
25
24
 
26
25
  if TYPE_CHECKING:
27
- from pyglaze.devtools.mock_device import (
28
- ForceMockDevice,
29
- LeMockDevice,
30
- MockDevice,
31
- )
32
- from pyglaze.helpers.types import FloatArray
26
+ from pyglaze.devtools.mock_device import LeMockDevice
27
+ from pyglaze.helpers._types import FloatArray
33
28
 
34
29
 
35
30
  class DeviceComError(Exception):
@@ -39,186 +34,6 @@ class DeviceComError(Exception):
39
34
  super().__init__(message)
40
35
 
41
36
 
42
- @dataclass
43
- class _ForceAmpCom:
44
- config: ForceDeviceConfiguration
45
- CONT_SCAN_UPDATE_FREQ: float = 1 # seconds
46
- __ser: ForceMockDevice | serial.Serial = field(init=False)
47
-
48
- ENCODING: ClassVar[str] = "utf-8"
49
- OK_RESPONSE: ClassVar[str] = "!A,OK"
50
- N_POINTS: ClassVar[int] = 10000
51
- DAC_BITWIDTH: ClassVar[int] = 65535 # bit-width of amp DAC
52
- # DO NOT change - antennas will break.
53
- MIN_ALLOWED_MOD_VOLTAGE: ClassVar[float] = -1.0
54
- MAX_ALLOWED_MOD_VOLTAGE: ClassVar[float] = 0.5
55
-
56
- @cached_property
57
- def scanning_points(self: _ForceAmpCom) -> int:
58
- time_pr_point = (
59
- self.config.integration_periods / self.config.modulation_frequency
60
- )
61
- return int(self.config.sweep_length_ms * 1e-3 / time_pr_point)
62
-
63
- @cached_property
64
- def _squished_intervals(self: _ForceAmpCom) -> list[Interval]:
65
- """Intervals squished into effective DAC range."""
66
- return _squish_intervals(
67
- intervals=self.config.scan_intervals or [Interval(lower=0.0, upper=1.0)],
68
- lower_bound=self.config.dac_lower_bound,
69
- upper_bound=self.config.dac_upper_bound,
70
- bitwidth=self.DAC_BITWIDTH,
71
- )
72
-
73
- @cached_property
74
- def times(self: _ForceAmpCom) -> FloatArray:
75
- return _delay_from_intervals(
76
- delayunit=lambda x: x,
77
- intervals=self.config.scan_intervals,
78
- points_per_interval=_points_per_interval(
79
- self.scanning_points, self._squished_intervals
80
- ),
81
- )
82
-
83
- @cached_property
84
- def scanning_list(self: _ForceAmpCom) -> list[float]:
85
- scanning_list: list[float] = []
86
- for interval, n_points in zip(
87
- self._squished_intervals,
88
- _points_per_interval(self.N_POINTS, self._squished_intervals),
89
- ):
90
- scanning_list.extend(
91
- np.linspace(interval.lower, interval.upper, n_points, endpoint=False)
92
- )
93
-
94
- return scanning_list
95
-
96
- @property
97
- def datapoints_per_update(self: _ForceAmpCom) -> int:
98
- return int(
99
- self.CONT_SCAN_UPDATE_FREQ
100
- / (self.config.integration_periods / self.config.modulation_frequency)
101
- )
102
-
103
- def __post_init__(self: _ForceAmpCom) -> None:
104
- self.__ser = _serial_factory(self.config)
105
-
106
- def __del__(self: _ForceAmpCom) -> None:
107
- """Closes connection when class instance goes out of scope."""
108
- self.disconnect()
109
-
110
- def write_all(self: _ForceAmpCom) -> list[str]:
111
- responses = []
112
- responses.append(self.write_period_and_frequency())
113
- responses.append(self.write_sweep_length())
114
- responses.append(self.write_waveform())
115
- responses.append(self.write_modulation_voltage())
116
- responses.extend(self.write_list())
117
- return responses
118
-
119
- def write_period_and_frequency(self: _ForceAmpCom) -> str:
120
- s = f"!set timing,{self.config.integration_periods},{self.config.modulation_frequency}\r"
121
- return self._encode_send_response(s)
122
-
123
- def write_sweep_length(self: _ForceAmpCom) -> str:
124
- s = f"!set sweep length,{self.config.sweep_length_ms}\r"
125
- return self._encode_send_response(s)
126
-
127
- def write_waveform(self: _ForceAmpCom) -> str:
128
- s = f"!set wave,{self.config.modulation_waveform}\r"
129
- return self._encode_send_response(s)
130
-
131
- def write_modulation_voltage(self: _ForceAmpCom) -> str:
132
- min_v = self.config.min_modulation_voltage
133
- max_v = self.config.max_modulation_voltage
134
- crit1 = self.MIN_ALLOWED_MOD_VOLTAGE <= min_v <= self.MAX_ALLOWED_MOD_VOLTAGE
135
- crit2 = self.MIN_ALLOWED_MOD_VOLTAGE <= max_v <= self.MAX_ALLOWED_MOD_VOLTAGE
136
-
137
- if crit1 and crit2:
138
- s = f"!set generator,{min_v},{max_v}\r"
139
- return self._encode_send_response(s)
140
-
141
- msg = f"Modulation voltages min: {min_v:.1f}, max: {max_v:.1f} not allowed."
142
- raise ValueError(msg)
143
-
144
- def write_list(self: _ForceAmpCom) -> list[str]:
145
- for iteration, entry in enumerate(self.scanning_list):
146
- string = f"!lut,{iteration},{entry}\r"
147
- self._encode_and_send(string)
148
- return self._get_response().split("\r")
149
-
150
- def start_scan(self: _ForceAmpCom) -> tuple[str, np.ndarray]:
151
- start_command = "!s,\r"
152
- self._encode_and_send(start_command)
153
- responses = self._get_response().split("\r")
154
- output_array = np.zeros((self.scanning_points, 3))
155
- output_array[:, 0] = self.times
156
- iteration = 0
157
- for entry in responses:
158
- if "!R" in entry:
159
- radius, angle = self._format_output(entry)
160
- output_array[iteration, 1] = radius
161
- output_array[iteration, 2] = angle
162
- iteration += 1
163
- elif "!D" in entry:
164
- break
165
- return start_command, output_array
166
-
167
- def start_continuous_scan(self: _ForceAmpCom) -> tuple[str, list[str]]:
168
- start_command = "!dat,1\r"
169
- self._encode_and_send(start_command)
170
- # Call self._read_until() twice, because amp returns !A,OK twice for
171
- # continuous output (for some unknown reason)
172
- responses = [self._read_until(expected=b"\r") for _ in range(2)]
173
- return start_command, responses
174
-
175
- def stop_continuous_scan(self: _ForceAmpCom) -> tuple[str, str]:
176
- start_command = "!dat,0\r"
177
- self._encode_and_send(start_command)
178
- response = self._read_until(expected=b"!A,OK\r")
179
- return start_command, response
180
-
181
- def read_continuous_data(self: _ForceAmpCom) -> FloatArray:
182
- output_array = np.zeros((self.datapoints_per_update, 3))
183
- output_array[:, 0] = np.linspace(0, 1, self.datapoints_per_update)
184
- for iteration in range(self.datapoints_per_update):
185
- amp_output = self._read_until(expected=b"\r")
186
- radius, angle = self._format_output(amp_output)
187
- output_array[iteration, 1] = radius
188
- output_array[iteration, 2] = angle
189
- return output_array
190
-
191
- def disconnect(self: _ForceAmpCom) -> None:
192
- """Closes connection."""
193
- with contextlib.suppress(AttributeError):
194
- # If the serial device does not exist, self.__ser is never created - hence catch
195
- self.__ser.close()
196
-
197
- def _encode_send_response(self: _ForceAmpCom, command: str) -> str:
198
- self._encode_and_send(command)
199
- return self._get_response()
200
-
201
- def _encode_and_send(self: _ForceAmpCom, command: str) -> None:
202
- self.__ser.write(command.encode(self.ENCODING))
203
-
204
- @_BackoffRetry(backoff_base=0.2, logger=logging.getLogger(LOGGER_NAME))
205
- def _get_response(self: _ForceAmpCom) -> str:
206
- r = self.__ser.readline().decode(self.ENCODING).strip()
207
- if r[: len(self.OK_RESPONSE)] != self.OK_RESPONSE:
208
- msg = f"Expected response '{self.OK_RESPONSE}', received: '{r}'"
209
- raise serialutil.SerialException(msg)
210
-
211
- return r
212
-
213
- def _read_until(self: _ForceAmpCom, expected: bytes) -> str:
214
- return self.__ser.read_until(expected=expected).decode(self.ENCODING).strip()
215
-
216
- def _format_output(self: _ForceAmpCom, amp_output: str) -> tuple[float, float]:
217
- """Format output from Force LIA to radius and angle."""
218
- response_list = amp_output.split(",")
219
- return float(response_list[1]), float(response_list[2])
220
-
221
-
222
37
  @dataclass
223
38
  class _LeAmpCom:
224
39
  config: LeDeviceConfiguration
@@ -233,6 +48,8 @@ class _LeAmpCom:
233
48
  STATUS_COMMAND: ClassVar[str] = "H"
234
49
  SEND_LIST_COMMAND: ClassVar[str] = "L"
235
50
  SEND_SETTINGS_COMMAND: ClassVar[str] = "S"
51
+ SERIAL_NUMBER_COMMAND: ClassVar[str] = "s"
52
+ FIRMWARE_VERSION_COMMAND: ClassVar[str] = "v"
236
53
 
237
54
  @cached_property
238
55
  def scanning_points(self: _LeAmpCom) -> int:
@@ -263,6 +80,14 @@ class _LeAmpCom:
263
80
  """
264
81
  return self.scanning_points * 12
265
82
 
83
+ @property
84
+ def serial_number_bytes(self: _LeAmpCom) -> int:
85
+ """Number of bytes to receive for a serial number.
86
+
87
+ Serial number has the form "<CHARACTER>-<4_DIGITS>, hence expect 6 bytes."
88
+ """
89
+ return 6
90
+
266
91
  def __post_init__(self: _LeAmpCom) -> None:
267
92
  self.__ser = _serial_factory(self.config)
268
93
 
@@ -302,6 +127,17 @@ class _LeAmpCom:
302
127
  # If the serial device does not exist, self.__ser is never created - hence catch
303
128
  self.__ser.close()
304
129
 
130
+ def get_serial_number(self: _LeAmpCom) -> str:
131
+ """Get the serial number of the connected device."""
132
+ return "X-9999"
133
+ # self._encode_and_send(self.SERIAL_NUMBER_COMMAND) # noqa: ERA001
134
+ # return self.__ser.read(self.serial_number_bytes).decode(self.ENCODING) # noqa: ERA001
135
+
136
+ def get_firmware_version(self: _LeAmpCom) -> str:
137
+ """Get the firmware version of the connected device."""
138
+ self._encode_and_send(self.FIRMWARE_VERSION_COMMAND)
139
+ return self.__ser.read_until().decode(self.ENCODING).strip()
140
+
305
141
  @cached_property
306
142
  def _intervals(self: _LeAmpCom) -> list[Interval]:
307
143
  """Intervals squished into effective DAC range."""
@@ -398,17 +234,7 @@ class _LeStatus(Enum):
398
234
  IDLE = "ACK: Idle."
399
235
 
400
236
 
401
- @overload
402
- def _serial_factory(
403
- config: ForceDeviceConfiguration,
404
- ) -> serial.Serial | ForceMockDevice: ...
405
-
406
-
407
- @overload
408
- def _serial_factory(config: LeDeviceConfiguration) -> serial.Serial | LeMockDevice: ...
409
-
410
-
411
- def _serial_factory(config: DeviceConfiguration) -> serial.Serial | MockDevice:
237
+ def _serial_factory(config: DeviceConfiguration) -> serial.Serial | LeMockDevice:
412
238
  if "mock_device" in config.amp_port:
413
239
  return _mock_device_factory(config)
414
240
 
@@ -74,91 +74,6 @@ class DeviceConfiguration(ABC):
74
74
  """Load a DeviceConfiguration from a file."""
75
75
 
76
76
 
77
- @dataclass
78
- class ForceDeviceConfiguration(DeviceConfiguration):
79
- """Represents a configuration that can be sent to the lock-in amp for scans.
80
-
81
- Args:
82
- amp_port: The name of the serial port the amp is connected to.
83
- sweep_length_ms: The length of the sweep in milliseconds.
84
- scan_intervals: The intervals to scan.
85
- integration_periods: The number of integration periods to use.
86
- modulation_frequency: The frequency of the modulation in Hz.
87
- dac_lower_bound: The lower bound of the modulation voltage in bits.
88
- dac_upper_bound: The upper bound of the modulation voltage in bits.
89
- min_modulation_voltage: The minimum modulation voltage in volts.
90
- max_modulation_voltage: The maximum modulation voltage in volts.
91
- modulation_waveform: The waveform to use for modulation.
92
- amp_timeout_seconds: The timeout for the amp in seconds.
93
- """
94
-
95
- amp_port: str
96
- sweep_length_ms: float
97
- scan_intervals: list[Interval] = field(default_factory=lambda: [Interval(0.0, 1.0)])
98
- integration_periods: int = 100
99
- modulation_frequency: int = 10000 # Hz
100
- dac_lower_bound: int = 6400
101
- dac_upper_bound: int = 59300
102
- min_modulation_voltage: float = -1.0 # V
103
- max_modulation_voltage: float = 0.5 # V
104
- modulation_waveform: str = "square"
105
- amp_timeout_seconds: float = 0.05
106
-
107
- amp_baudrate: ClassVar[int] = 1200000 # bit/s
108
-
109
- @property
110
- def _sweep_length_ms(self: ForceDeviceConfiguration) -> float:
111
- return self.sweep_length_ms
112
-
113
- def save(self: ForceDeviceConfiguration, path: Path) -> str:
114
- """Save a DeviceConfiguration to a file.
115
-
116
- Args:
117
- path: The path to save the configuration to.
118
-
119
- Returns:
120
- str: Final path component of the saved file, without the extension.
121
-
122
- """
123
- with path.open("w") as f:
124
- json.dump(asdict(self), f, indent=4, sort_keys=True)
125
-
126
- return path.stem
127
-
128
- @classmethod
129
- def from_dict(
130
- cls: type[ForceDeviceConfiguration], amp_config: dict
131
- ) -> ForceDeviceConfiguration:
132
- """Create a DeviceConfiguration from a dict.
133
-
134
- Args:
135
- amp_config: An amp configuration in dict form.
136
-
137
- Raises:
138
- ValueError: If the dictionary is empty.
139
-
140
- Returns:
141
- DeviceConfiguration: A DeviceConfiguration object.
142
- """
143
- return _config_w_intervals_from_dict(cls, amp_config)
144
-
145
- @classmethod
146
- def load(
147
- cls: type[ForceDeviceConfiguration], file_path: Path
148
- ) -> ForceDeviceConfiguration:
149
- """Load a DeviceConfiguration from a file.
150
-
151
- Args:
152
- file_path: The path to the file to load.
153
-
154
- Returns:
155
- DeviceConfiguration: A DeviceConfiguration object.
156
- """
157
- with file_path.open() as f:
158
- configuration_dict = json.load(f)
159
- return cls.from_dict(configuration_dict)
160
-
161
-
162
77
  @dataclass
163
78
  class LeDeviceConfiguration(DeviceConfiguration):
164
79
  """Represents a configuration that can be sent to a Le-type lock-in amp for scans.
@@ -220,7 +135,13 @@ class LeDeviceConfiguration(DeviceConfiguration):
220
135
  Returns:
221
136
  DeviceConfiguration: A DeviceConfiguration object.
222
137
  """
223
- return _config_w_intervals_from_dict(cls, amp_config)
138
+ if not amp_config:
139
+ msg = "'amp_config' is empty."
140
+ raise ValueError(msg)
141
+
142
+ config = cls(**amp_config)
143
+ config.scan_intervals = [Interval.from_dict(d) for d in config.scan_intervals] # type: ignore[arg-type]
144
+ return config
224
145
 
225
146
  @classmethod
226
147
  def load(
@@ -237,16 +158,3 @@ class LeDeviceConfiguration(DeviceConfiguration):
237
158
  with file_path.open() as f:
238
159
  configuration_dict = json.load(f)
239
160
  return cls.from_dict(configuration_dict)
240
-
241
-
242
- C = TypeVar("C", LeDeviceConfiguration, ForceDeviceConfiguration)
243
-
244
-
245
- def _config_w_intervals_from_dict(cls: type[C], amp_config: dict) -> C:
246
- if amp_config:
247
- config = cls(**amp_config)
248
- config.scan_intervals = [Interval.from_dict(d) for d in config.scan_intervals] # type: ignore[arg-type]
249
- return config
250
-
251
- msg = "'amp_config' is empty."
252
- raise ValueError(msg)
@@ -1,21 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
3
  import struct
5
4
  import time
6
5
  from abc import ABC, abstractmethod
7
6
  from enum import Enum, auto
8
- from pathlib import Path
9
- from time import sleep
10
7
 
11
8
  import numpy as np
12
- from serial import serialutil
13
9
 
14
- from pyglaze.device.configuration import (
15
- DeviceConfiguration,
16
- ForceDeviceConfiguration,
17
- LeDeviceConfiguration,
18
- )
10
+ from pyglaze.device.configuration import DeviceConfiguration, LeDeviceConfiguration
19
11
 
20
12
 
21
13
  class MockDevice(ABC):
@@ -33,143 +25,6 @@ class MockDevice(ABC):
33
25
  pass
34
26
 
35
27
 
36
- class ForceMockDevice(MockDevice):
37
- """Mock device for devices using a FORCE lockin for testing purposes."""
38
-
39
- CONFIG_PATH = Path(__file__).parents[1] / "devtools" / "mockup_config.json"
40
- ENCODING: str = "utf-8"
41
-
42
- def __init__(
43
- self: ForceMockDevice,
44
- fail_after: float = np.inf,
45
- n_fails: float = np.inf,
46
- *,
47
- instant_response: bool = False,
48
- ) -> None:
49
- self.fail_after = fail_after
50
- self.fails_wanted = n_fails
51
- self.n_failures = 0
52
- self.n_scans = 0
53
- self.rng = np.random.default_rng()
54
- self.valid_input = True
55
- self.experiment_running = False
56
- self.instant_response = instant_response
57
-
58
- self._periods = None
59
- self._frequency = None
60
- self._sweep_length = None
61
- if not self.CONFIG_PATH.exists():
62
- conf = {"periods": None, "frequency": None, "sweep_length_ms": None}
63
- self.__save(conf)
64
-
65
- @property
66
- def periods(self: ForceMockDevice) -> int:
67
- """Number of integration periods."""
68
- return int(self.__get_val("periods"))
69
-
70
- @periods.setter
71
- def periods(self: ForceMockDevice, value: int) -> None:
72
- self.__set_val("periods", value)
73
-
74
- @property
75
- def frequency(self: ForceMockDevice) -> int:
76
- """Modulation frequency in Hz."""
77
- return int(self.__get_val("frequency"))
78
-
79
- @frequency.setter
80
- def frequency(self: ForceMockDevice, value: int) -> None:
81
- self.__set_val("frequency", value)
82
-
83
- @property
84
- def sweep_length(self: ForceMockDevice) -> int:
85
- """Total sweep length for one pulse."""
86
- return int(self.__get_val("sweep_length"))
87
-
88
- @sweep_length.setter
89
- def sweep_length(self: ForceMockDevice, value: int) -> None:
90
- self.__set_val("sweep_length", value)
91
-
92
- def close(self: ForceMockDevice) -> None:
93
- """Mock-close the serial connection."""
94
-
95
- def write(self: ForceMockDevice, input_string: bytes) -> None:
96
- """Mock-write to the serial connection."""
97
- decoded_input_string = input_string.decode("utf-8")
98
- split_input_string = decoded_input_string.split(",")
99
- cmd = split_input_string[0]
100
- if cmd == "!set timing":
101
- self.periods = int(split_input_string[1])
102
- self.frequency = int(split_input_string[2])
103
- elif cmd == "!set sweep length":
104
- self.sweep_length = int(split_input_string[1])
105
- elif cmd == "!s":
106
- time_pr_point = self.periods / self.frequency
107
- self.in_waiting = int(self.sweep_length * 1e-3 / time_pr_point)
108
- self.experiment_running = True
109
- elif cmd in ["!lut", "!set wave", "!set generator"]:
110
- pass
111
- elif cmd != "!dat":
112
- self.valid_input = False
113
-
114
- def readline(self: ForceMockDevice) -> bytes:
115
- """Mock-readline from the serial connection."""
116
- if self.valid_input:
117
- return self.__run_experiment() if self.experiment_running else b"!A,OK\r"
118
-
119
- return b"A,FAULT\r"
120
-
121
- def read_until(self: ForceMockDevice, expected: bytes = b"\r") -> bytes: # noqa: ARG002
122
- """Mock-read_until from the serial connection."""
123
- random_datapoint = self.__create_random_datapoint
124
- return random_datapoint.encode(self.ENCODING)
125
-
126
- def __run_experiment(self: ForceMockDevice) -> bytes:
127
- return_string = "!A,OK\\r"
128
- return_string += (
129
- f"!S,ip: {self.periods}, "
130
- f"Freq: {self.frequency}, "
131
- f"sl: {self.sweep_length}, "
132
- f"from: 0.0000, "
133
- f"to:1.0000\r"
134
- )
135
- for _ in range(self.in_waiting):
136
- return_string += self.__create_random_datapoint
137
- return_string += "!D,DONE\\r"
138
- if not self.instant_response:
139
- sleep(self.sweep_length * 1e-3)
140
- self.n_scans += 1
141
- if self.n_scans > self.fail_after and self.n_failures < self.fails_wanted:
142
- self.n_failures += 1
143
- msg = "MOCK_DEVICE: scan failed"
144
- raise serialutil.SerialException(msg)
145
-
146
- return return_string.encode("utf-8")
147
-
148
- @property
149
- def __create_random_datapoint(self: ForceMockDevice) -> str:
150
- radius = self.rng.random() * 10
151
- theta = self.rng.random() * 360
152
- return f"!R,{radius},{theta}\r"
153
-
154
- def __save(self: ForceMockDevice, conf: dict) -> None:
155
- with self.CONFIG_PATH.open("w") as f:
156
- json.dump(conf, f)
157
-
158
- def __get_val(self: ForceMockDevice, key: str) -> int:
159
- val: int = getattr(self, f"_{key}")
160
- if not val:
161
- with self.CONFIG_PATH.open() as f:
162
- val = json.load(f)[key]
163
- return val
164
-
165
- def __set_val(self: ForceMockDevice, key: str, val: str | float) -> None:
166
- with self.CONFIG_PATH.open() as f:
167
- config = json.load(f)
168
- config[key] = val
169
- setattr(self, f"_{key}", val)
170
- self.__save(config)
171
-
172
-
173
28
  class _LeMockState(Enum):
174
29
  IDLE = auto()
175
30
  WAITING_FOR_SETTINGS = auto()
@@ -177,6 +32,8 @@ class _LeMockState(Enum):
177
32
  RECEIVED_SETTINGS = auto()
178
33
  RECEIVED_LIST = auto()
179
34
  RECEIVED_STATUS_REQUEST = auto()
35
+ RECEIVED_SERIAL_NUMBER_REQUEST = auto()
36
+ RECEIVED_FIRMWARE_VERSION_REQUEST = auto()
180
37
  STARTING_SCAN = auto()
181
38
  SCANNING = auto()
182
39
 
@@ -235,6 +92,9 @@ class LeMockDevice(MockDevice):
235
92
  return self._create_scan_bytes(n_bytes=0)
236
93
  if self.state == _LeMockState.IDLE:
237
94
  return self._create_scan_bytes(n_bytes=size)
95
+ if self.state == _LeMockState.RECEIVED_SERIAL_NUMBER_REQUEST:
96
+ self.state = _LeMockState.IDLE
97
+ return "M-9999".encode(self.ENCODING)
238
98
  raise NotImplementedError
239
99
 
240
100
  def read_until(self: LeMockDevice, _: bytes = b"\r") -> bytes: # noqa: PLR0911
@@ -255,6 +115,9 @@ class LeMockDevice(MockDevice):
255
115
  self.state = _LeMockState.SCANNING
256
116
  self.is_scanning = True
257
117
  return "ACK: Scan started.".encode(self.ENCODING)
118
+ if self.state == _LeMockState.RECEIVED_FIRMWARE_VERSION_REQUEST:
119
+ self.state = _LeMockState.IDLE
120
+ return "v0.1.0".encode(self.ENCODING)
258
121
  if self.state == _LeMockState.RECEIVED_STATUS_REQUEST:
259
122
  if self._scan_has_finished():
260
123
  self.state = _LeMockState.IDLE
@@ -291,6 +154,10 @@ class LeMockDevice(MockDevice):
291
154
  self._scan_start_time = time.time()
292
155
  elif msg == "R":
293
156
  self._scan_has_finished()
157
+ elif msg == "s":
158
+ self.state = _LeMockState.RECEIVED_SERIAL_NUMBER_REQUEST
159
+ elif msg == "v":
160
+ self.state = _LeMockState.RECEIVED_FIRMWARE_VERSION_REQUEST
294
161
  else:
295
162
  msg = f"Unknown message: {msg}"
296
163
  raise NotImplementedError(msg)
@@ -376,7 +243,7 @@ def list_mock_devices() -> list[str]:
376
243
  ]
377
244
 
378
245
 
379
- def _mock_device_factory(config: DeviceConfiguration) -> MockDevice:
246
+ def _mock_device_factory(config: DeviceConfiguration) -> LeMockDevice:
380
247
  mock_class = _get_mock_class(config)
381
248
  if config.amp_port == "mock_device_scan_should_fail":
382
249
  return mock_class(fail_after=0)
@@ -393,9 +260,7 @@ def _mock_device_factory(config: DeviceConfiguration) -> MockDevice:
393
260
  raise ValueError(msg)
394
261
 
395
262
 
396
- def _get_mock_class(config: DeviceConfiguration) -> type[MockDevice]:
397
- if isinstance(config, ForceDeviceConfiguration):
398
- return ForceMockDevice
263
+ def _get_mock_class(config: DeviceConfiguration) -> type[LeMockDevice]:
399
264
  if isinstance(config, LeDeviceConfiguration):
400
265
  return LeMockDevice
401
266
 
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import cast
3
+ from typing import TYPE_CHECKING, cast
4
4
 
5
5
  import numpy as np
6
6
 
7
- from pyglaze.helpers.types import FloatArray
7
+ if TYPE_CHECKING:
8
+ from pyglaze.helpers._types import FloatArray
8
9
 
9
10
 
10
11
  def gaussian_derivative_pulse(
@@ -32,4 +33,4 @@ def gaussian_derivative_pulse(
32
33
  scale=1.0 / signal_to_noise, size=len(signal)
33
34
  )
34
35
  )
35
- return cast(FloatArray, signal / np.max(signal) + noise)
36
+ return cast("FloatArray", signal / np.max(signal) + noise)
@@ -8,11 +8,11 @@ from typing import TYPE_CHECKING, Callable, cast
8
8
  import serial
9
9
  import serial.tools.list_ports
10
10
 
11
- from pyglaze.helpers.types import P, T
12
-
13
11
  if TYPE_CHECKING:
14
12
  import logging
15
13
 
14
+ from pyglaze.helpers._types import P, T
15
+
16
16
  APP_NAME = "Glaze"
17
17
  LOGGER_NAME = "glaze-logger"
18
18
 
@@ -59,17 +59,17 @@ class _BackoffRetry:
59
59
  def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
60
60
  for tries in range(self.max_tries - 1):
61
61
  try:
62
- return cast(T, func(*args, **kwargs))
62
+ return cast("T", func(*args, **kwargs))
63
63
  except (KeyboardInterrupt, SystemExit):
64
64
  raise
65
65
  except Exception as e: # noqa: BLE001
66
66
  self._log(
67
- f"{func.__name__} failed {tries+1} time(s) with: '{e}'. Trying again"
67
+ f"{func.__name__} failed {tries + 1} time(s) with: '{e}'. Trying again"
68
68
  )
69
69
  backoff = min(self.backoff_base * 2**tries, self.max_backoff)
70
70
  time.sleep(backoff)
71
- self._log(f"{func.__name__}: Last try ({tries+2}).")
72
- return cast(T, func(*args, **kwargs))
71
+ self._log(f"{func.__name__}: Last try ({tries + 2}).")
72
+ return cast("T", func(*args, **kwargs))
73
73
 
74
74
  return wrapper
75
75
 
@@ -1,3 +1,3 @@
1
- from .interpolation import ws_interpolate
1
+ from .interpolation import cubic_spline_interpolate, ws_interpolate
2
2
 
3
- __all__ = ["ws_interpolate"]
3
+ __all__ = ["cubic_spline_interpolate", "ws_interpolate"]