pulsepal.synth_pal
Python interface for Synth Pal, a waveform synthesizer for Pulse Pal 3.
Synth Pal is alternative firmware for Pulse Pal 3 hardware. Each output channel plays a sine, triangle, square or sawtooth wave or white noise, or steps to a fixed voltage, when it is triggered: by a TTL pulse on a trigger channel, from software, or from the thumb joystick. Each channel has its own waveform, peak to peak voltage, mean voltage, resting voltage, play duration, and on and off ramps, and one frequency, 1 Hz to 20 kHz in steps of 0.01 Hz, applies to all four.
Everything is accessed through SynthPalDevice. Import it, connect to
the device's serial port, set the waveforms, and trigger, e.g.
from pulsepal import SynthPalDevice
with SynthPalDevice("COM3") as S:
S.frequency = 440 # Hz, all channels
S.waveform[1] = "Sine"
S.peak_to_peak[1] = 4 # volts: swings from -2 V to 2 V
S.play_duration[1] = 0.5 # seconds
S.trigger(1)
The device needs Synth Pal firmware, which is in /Firmware/SynthPal in the Pulse Pal repository. Its USB protocol is documented in PROTOCOL.md.
Channel settings
Settings that apply to one output channel, such as
SynthPalDevice.peak_to_peak, are lists indexed by channel number: index
0 is unused and holds None, and indices 1 to 4 hold the settings of
output channels 1-4, as in pulsepal.PulsePalDevice. Setting an element
or a slice programs the device at once. Assigning a whole list sets all
four channels, and takes one value per channel: a single value raises an
error, because it does not say which channels it is meant for.
S.peak_to_peak[2] = 5 # channel 2 only
S.play_duration[1:5] = [1, 2, 3, 4]
S.waveform = ["Square"] * 4 # all four channels
SynthPalDevice.trigger_mode is indexed the same way by trigger
channel number, 1 or 2.
To set several of one channel's settings at once, use
SynthPalDevice.configure:
S.configure(2, waveform="Triangle", peak_to_peak=20, mean_voltage=0)
Units and levels
Voltages are in volts, times in seconds, and frequencies in Hz. A periodic
waveform, or white noise, swings peak_to_peak / 2 above and below its
mean_voltage, and must stay within -10 V to 10 V:
abs(mean_voltage) + peak_to_peak / 2 <= 10. A "Fixed Voltage" channel
steps to its fixed_voltage, -10 V to 10 V, for its play duration. Between
playbacks, a channel outputs its resting_voltage, and its
on_ramp_duration and off_ramp_duration fade it in from there and back to
it (see "Ramps" below).
Ramps
A channel's on ramp follows each trigger, and fades its waveform in from
the resting voltage: its peak to peak voltage rises linearly from 0 to
its full value, and its mean from the resting voltage to the mean voltage
(a fixed voltage ramps from the resting voltage to its voltage). The play
duration follows at full amplitude, then the off ramp fades back to the
resting voltage. The off ramp also follows a stop: stop(), a toggle or
gated trigger, or the joystick. So the ramps lengthen playback: from a
trigger to rest takes on_ramp_duration + play_duration +
off_ramp_duration. During its off ramp, a channel counts as stopping: a
trigger fades it in again from where it is, without restarting its
waveform's cycle, and plays its play duration again. A channel stopped
during its on ramp fades out from where it is, at the off ramp's rate.
The output never jumps.
Param sync
"Param Sync", Pulse Pal's param sync trigger mode, lets the settings of
the next trial be sent during the current one, and applied the instant it
starts. Change settings in a SynthPalDevice.batch block: they are kept
here, and sent all at once when it ends. While a trigger channel is in
param sync mode, the device stores the whole set, and that channel's next
rising edge loads it.
S.trigger_mode[2] = "Param Sync" # sent at once
with S.batch():
S.frequency = 880
S.peak_to_peak[1] = 2
# Stored for trigger channel 2's next rising edge
See SynthPalDevice.trigger_mode for what the edge does.
License
This file is part of the Sanworks PulsePal repository. Copyright (C) Sanworks LLC, Rochester, New York, USA
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
This program is distributed WITHOUT ANY WARRANTY and without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.
1""" 2Python interface for Synth Pal, a waveform synthesizer for Pulse Pal 3. 3 4Synth Pal is alternative firmware for Pulse Pal 3 hardware. Each output 5channel plays a sine, triangle, square or sawtooth wave or white noise, or 6steps to a fixed voltage, when it is triggered: by a TTL pulse on a trigger 7channel, from software, or from the thumb joystick. Each channel has its own 8waveform, peak to peak voltage, mean voltage, resting voltage, play 9duration, and on and off ramps, and one frequency, 1 Hz to 20 kHz in steps 10of 0.01 Hz, applies to all four. 11 12Everything is accessed through `SynthPalDevice`. Import it, connect to 13the device's serial port, set the waveforms, and trigger, e.g. 14 15```python 16from pulsepal import SynthPalDevice 17 18with SynthPalDevice("COM3") as S: 19 S.frequency = 440 # Hz, all channels 20 S.waveform[1] = "Sine" 21 S.peak_to_peak[1] = 4 # volts: swings from -2 V to 2 V 22 S.play_duration[1] = 0.5 # seconds 23 S.trigger(1) 24``` 25 26The device needs Synth Pal firmware, which is in 27[/Firmware/SynthPal](https://github.com/sanworks/PulsePal/tree/develop/Firmware/SynthPal) in the 28Pulse Pal repository. Its USB protocol is documented in 29[PROTOCOL.md](https://github.com/sanworks/PulsePal/blob/develop/Firmware/SynthPal/PROTOCOL.md). 30 31## Channel settings 32 33Settings that apply to one output channel, such as 34`SynthPalDevice.peak_to_peak`, are lists indexed by channel number: index 350 is unused and holds `None`, and indices 1 to 4 hold the settings of 36output channels 1-4, as in `pulsepal.PulsePalDevice`. Setting an element 37or a slice programs the device at once. Assigning a whole list sets all 38four channels, and takes one value per channel: a single value raises an 39error, because it does not say which channels it is meant for. 40 41```python 42S.peak_to_peak[2] = 5 # channel 2 only 43S.play_duration[1:5] = [1, 2, 3, 4] 44S.waveform = ["Square"] * 4 # all four channels 45``` 46 47`SynthPalDevice.trigger_mode` is indexed the same way by trigger 48channel number, 1 or 2. 49 50To set several of one channel's settings at once, use 51`SynthPalDevice.configure`: 52 53```python 54S.configure(2, waveform="Triangle", peak_to_peak=20, mean_voltage=0) 55``` 56 57## Units and levels 58 59Voltages are in volts, times in seconds, and frequencies in Hz. A periodic 60waveform, or white noise, swings `peak_to_peak / 2` above and below its 61`mean_voltage`, and must stay within -10 V to 10 V: 62`abs(mean_voltage) + peak_to_peak / 2 <= 10`. A `"Fixed Voltage"` channel 63steps to its `fixed_voltage`, -10 V to 10 V, for its play duration. Between 64playbacks, a channel outputs its `resting_voltage`, and its 65`on_ramp_duration` and `off_ramp_duration` fade it in from there and back to 66it (see "Ramps" below). 67 68## Ramps 69 70A channel's on ramp follows each trigger, and fades its waveform in from 71the resting voltage: its peak to peak voltage rises linearly from 0 to 72its full value, and its mean from the resting voltage to the mean voltage 73(a fixed voltage ramps from the resting voltage to its voltage). The play 74duration follows at full amplitude, then the off ramp fades back to the 75resting voltage. The off ramp also follows a stop: `stop()`, a toggle or 76gated trigger, or the joystick. So the ramps lengthen playback: from a 77trigger to rest takes `on_ramp_duration + play_duration + 78off_ramp_duration`. During its off ramp, a channel counts as stopping: a 79trigger fades it in again from where it is, without restarting its 80waveform's cycle, and plays its play duration again. A channel stopped 81during its on ramp fades out from where it is, at the off ramp's rate. 82The output never jumps. 83 84## Param sync 85 86`"Param Sync"`, Pulse Pal's param sync trigger mode, lets the settings of 87the next trial be sent during the current one, and applied the instant it 88starts. Change settings in a `SynthPalDevice.batch` block: they are kept 89here, and sent all at once when it ends. While a trigger channel is in 90param sync mode, the device stores the whole set, and that channel's next 91rising edge loads it. 92 93```python 94S.trigger_mode[2] = "Param Sync" # sent at once 95with S.batch(): 96 S.frequency = 880 97 S.peak_to_peak[1] = 2 98# Stored for trigger channel 2's next rising edge 99``` 100 101See `SynthPalDevice.trigger_mode` for what the edge does. 102 103## License 104 105This file is part of the Sanworks PulsePal repository. 106Copyright (C) Sanworks LLC, Rochester, New York, USA 107 108This program is free software: you can redistribute it and/or modify 109it under the terms of the GNU General Public License as published by 110the Free Software Foundation, version 3. 111 112This program is distributed WITHOUT ANY WARRANTY and without even the 113implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 114See the GNU General Public License for more details. 115 116You should have received a copy of the GNU General Public License 117along with this program. If not, see <http://www.gnu.org/licenses/>. 118""" 119 120from dataclasses import dataclass 121import math 122import numbers 123import struct 124import warnings 125 126import numpy as np 127import serial 128 129from . import _common 130from ._common import ChannelSettings, PulsePalError, to_bool, to_name 131 132__all__ = ["SynthPalDevice", "DeviceInfo", "DeviceStatus", "WAVEFORMS", "TRIGGER_MODES"] 133__docformat__ = "google" 134 135WAVEFORMS = ("Sine", "Triangle", "Square", "Sawtooth", "Fixed Voltage", "White Noise") 136"""Names of the waveforms, in order of their code on the device.""" 137 138FIXED_VOLTAGE = "Fixed Voltage" 139 140TRIGGER_MODES = ("Normal", "Toggle", "Gated", "Param Sync") 141"""Names of the trigger modes, in order of their code on the device. These 142are Pulse Pal's trigger modes, with the same codes.""" 143 144# The output ranges the device chooses from, in order of their index on the 145# device, with their limits in volts. See "Output ranges" in 146# /Firmware/SynthPal/PROTOCOL.md. 147OUTPUT_RANGES = { 148 "0V:5V": (0.0, 5.0), 149 "0V:10V": (0.0, 10.0), 150 "-5V:5V": (-5.0, 5.0), 151 "-10V:10V": (-10.0, 10.0), 152} 153 154_MAX_VOLTAGE_UV = 10_000_000 # Every output voltage stays within +/-10 V 155 156# The settings sync_to_device() sends, in the class's order 157_CHANNEL_SETTINGS = ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage", "resting_voltage", 158 "play_duration", "on_ramp_duration", "off_ramp_duration", "trigger_mode", 159 "link_trigger_channel1", "link_trigger_channel2") 160_SETTING_NAMES = ("frequency", *_CHANNEL_SETTINGS, "auto_sync") # The names a script assigns 161# The ops whose reply _write_command() does not track: those with no reply ('P', 'X', 81 and 89), and the handshake 162# (72), whose reply read_handshake_reply() reads until the device is quiet 163_OPS_WITHOUT_TRACKED_REPLY = frozenset({72, 81, 89, ord("P"), ord("X")}) 164 165 166def _samples_per_cycle(centihz): 167 """Samples in one cycle at a frequency, as the device works it out (see SynthPalDevice.samples_per_cycle).""" 168 return 4 * (2_500_000 // centihz) 169 170 171class _Rejected(PulsePalError): 172 """The device replied 0: it rejected a command, and changed nothing.""" 173 174 175def _is_valid_level(waveform, amplitude_uv, mean_uv): 176 """Whether the device accepts an output channel's waveform, amplitude and mean voltage together, as 177 isValidOutputLevel() in /Firmware/SynthPal/Settings.ino checks them. On the device, a channel has one 178 amplitude: a fixed voltage's voltage, or a periodic waveform's peak to peak voltage.""" 179 if waveform == FIXED_VOLTAGE: 180 return abs(amplitude_uv) <= _MAX_VOLTAGE_UV 181 return 0 <= amplitude_uv and 2 * abs(mean_uv) + amplitude_uv <= 2 * _MAX_VOLTAGE_UV 182 183 184def _microvolts(volts): 185 return round(volts * 1e6) 186 187 188@dataclass 189class DeviceInfo: 190 """Properties of the connected Synth Pal. 191 192 Populated when `SynthPalDevice` connects, and available as 193 `SynthPalDevice.info`. 194 """ 195 196 firmware_version: int = None 197 """Synth Pal firmware version running on the device.""" 198 199 hardware_version: int = None 200 """Pulse Pal hardware version, e.g. `3`.""" 201 202 circuit_board_revision: int = None 203 """Revision of the device's circuit board, 0-31. 204 205 The board encodes its revision on five pins, which the firmware reads 206 at startup. A board that encodes none reports `0`. To record exactly 207 which hardware ran an experiment, copy `hardware_version` and 208 `circuit_board_revision` to your data. 209 """ 210 211 serial_number: str = None 212 """USB serial number of the device, e.g. `"20345420"`: the Teensy's, 213 unique to each Pulse Pal 3, and the same whichever firmware it runs. 214 215 Connect by it, wherever the device is plugged in, with 216 `SynthPalDevice(serial_number="20345420")`. To record exactly which unit ran an 217 experiment, copy it to your data. 218 """ 219 220 n_channels: int = None 221 """Number of output channels.""" 222 223 min_frequency: float = None 224 """Lowest frequency, in Hz.""" 225 226 max_frequency: float = None 227 """Highest frequency, in Hz.""" 228 229 max_sampling_rate: int = None 230 """Highest sampling rate, in Hz. See `SynthPalDevice.sampling_rate`.""" 231 232 timer_clock_hz: int = None 233 """Clock that the sample clock counts, in Hz.""" 234 235 max_play_duration: float = None 236 """Longest play duration, in seconds. See 237 `SynthPalDevice.play_duration`.""" 238 239 waveforms: tuple = WAVEFORMS 240 """Names of the waveforms, accepted by `SynthPalDevice.waveform`.""" 241 242 trigger_modes: tuple = TRIGGER_MODES 243 """Names of the trigger modes, accepted by 244 `SynthPalDevice.trigger_mode`.""" 245 246 247@dataclass 248class DeviceStatus: 249 """A snapshot of the device's playback state, from 250 `SynthPalDevice.status`.""" 251 252 playing: list 253 """Numbers of the output channels that are playing, e.g. `[1, 3]`. 254 255 A channel in its off ramp counts as playing until it reaches its 256 resting voltage.""" 257 258 samples_per_cycle: int 259 """Samples in one cycle of the waveform. See 260 `SynthPalDevice.samples_per_cycle`.""" 261 262 output_ranges: list 263 """The output range the device chose for each channel, indexed by 264 channel number, with index 0 unused, e.g. `"-5V:5V"`. 265 266 Each channel uses the range with the finest voltage steps that holds 267 its whole waveform and its resting voltage: `"0V:5V"` (76 uV steps), 268 then `"0V:10V"` or `"-5V:5V"` (153 uV), then `"-10V:10V"` (305 uV). 269 """ 270 271 longest_interrupt_us: float 272 """Longest run of the device's sample clock interrupt since the 273 previous call to `SynthPalDevice.status`, in microseconds. 274 275 It must stay below the sample period, `1e6 / sampling_rate`. 276 """ 277 278 late_updates: int 279 """Output updates since the previous call to `SynthPalDevice.status` 280 that may have come later than their fixed time after a sample clock 281 tick. It is normally 0; a change of frequency during playback can 282 make one update late. See "Timing" in the 283 [Synth Pal protocol](https://github.com/sanworks/PulsePal/blob/develop/Firmware/SynthPal/PROTOCOL.md#timing). 284 """ 285 286 287class SynthPalDevice: 288 """A class to control a Synth Pal on a USB serial port. 289 290 Creating an instance opens the serial port, checks that the device 291 runs Synth Pal firmware, reads its properties into 292 `SynthPalDevice.info`, shows "PYTHON Connected" on the device's 293 screen, stops any playback and programs the default settings (see 294 `SynthPalDevice.set_default_params`). 295 296 ```python 297 from pulsepal import SynthPalDevice 298 299 S = SynthPalDevice("COM3") 300 S.waveform[1] = "Triangle" 301 S.trigger(1) 302 S.close() 303 ``` 304 305 Replace "COM3" with the device's USB serial port name, which 306 `SynthPalDevice.serialportlist` lists. `SynthPalDevice` is also a 307 context manager, which closes the port on exit. 308 309 Closing the connection stops playback, as it does on Pulse Pal (each 310 channel over its off ramp), and puts the device's own name back on its 311 screen. The device keeps its settings, so TTL triggers still play the 312 channels. 313 """ 314 315 port: "serial.Serial" 316 """The open `serial.Serial` port connected to the device.""" 317 318 info: DeviceInfo 319 """Properties of the connected device. See `DeviceInfo`.""" 320 321 _CURRENT_FIRMWARE_VERSION = 1 322 _BAUD_RATE = 12000000 # USB serial ignores the baud rate 323 324 _OP_MENU_BYTE = _common.OP_MENU_BYTE 325 _OP_HANDSHAKE = _common.OP_HANDSHAKE 326 _OP_DISCONNECT = 81 327 _OP_SET_CLIENT_NAME = 89 328 _OP_SET_SCREEN_SAVER = 99 329 _OP_HARDWARE_REVISION = 100 330 _OP_HARDWARE_INFO = ord("N") 331 _OP_SET_FREQUENCY = ord("F") 332 _OP_SET_WAVEFORM = ord("W") 333 _OP_SET_AMPLITUDE = ord("A") 334 _OP_SET_RESTING_VOLTAGE = ord("V") 335 _OP_SET_MEAN_VOLTAGE = ord("M") 336 _OP_SET_PLAY_DURATION = ord("D") 337 _OP_SET_ON_RAMP_DURATION = ord("B") 338 _OP_SET_OFF_RAMP_DURATION = ord("E") 339 _OP_SET_TRIGGER_LINKS = ord("I") 340 _OP_SET_TRIGGER_MODE = ord("T") 341 _OP_SET_ALL_SETTINGS = ord("U") 342 _OP_GET_ALL_SETTINGS = ord("R") 343 # Every setting, as ops 85 ('U') and 82 ('R') carry them: frequency (centiHz), waveform codes, amplitudes, 344 # mean and resting voltages (uV), play, on ramp and off ramp durations (us), links, trigger modes 345 _SETTINGS_FORMAT = "<I4B4i4i4i4I4I4I8B2B" 346 _OP_TRIGGER = ord("P") 347 _OP_STOP = ord("X") 348 _OP_GET_STATUS = ord("G") 349 _OP_GET_PLAYBACK_CHECKSUMS = ord("Z") 350 351 _HANDSHAKE_REPLY = 83 # 'S' 352 _HARDWARE_INFO_FORMAT = "<BBIIIII" 353 _STATUS_FORMAT = "<BI4BII" 354 _ALL_CHANNELS = 0x0F 355 # The ops that change settings: if the reply to one is lost, they are read back (_discard_lost_reply()) 356 _SETTING_OPS = frozenset(map(ord, "FWAVMDBEITU")) 357 358 _reply_pending = None # The op code of a command whose reply has not been read in full yet (see _write_command()) 359 _unsent = frozenset() # The settings assigned with auto_sync off since the settings were last sent or read 360 361 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 362 """Open a connection to a Synth Pal. 363 364 Args: 365 port_name: USB serial port of the device, such as `COM3` on 366 Windows or `/dev/ttyACM0` on Linux. 367 serial_number: Instead of `port_name`, the device's USB serial 368 number (`DeviceInfo.serial_number`), which finds its port 369 wherever it is plugged in. 370 timeout: Serial read timeout, in seconds. 371 372 Firmware newer than this module knows is used with a warning: new 373 firmware only adds commands. Update the package to use what is new. 374 375 Raises: 376 PulsePalError: If the device does not reply to the handshake, or 377 runs other firmware. 378 serial.SerialException: If the serial port cannot be opened, 379 e.g. because another program has it open (on Linux and 380 macOS, another session of these classes, which lock it). 381 """ 382 self._closed = True 383 self._auto_sync = True 384 self.info = DeviceInfo() 385 self._frequency = None 386 self._samples_per_cycle = None 387 # What the device holds for each output channel's waveform, amplitude (in microvolts: a periodic 388 # waveform's peak to peak voltage, or a fixed voltage) and mean voltage (in microvolts), as far as this 389 # object has programmed them. _send_levels() changes them in an order the device accepts. 390 self._device_waveform = ["Sine"] * 4 391 self._device_amplitude_uv = [5_000_000] * 4 392 self._device_mean_uv = [0] * 4 393 self._waveform = ChannelSettings( 394 "waveform", ["Sine"] * 4, self, "_apply_waveform") 395 self._peak_to_peak = ChannelSettings( 396 "peak_to_peak", [5.0] * 4, self, "_apply_peak_to_peak") 397 self._fixed_voltage = ChannelSettings( 398 "fixed_voltage", [5.0] * 4, self, "_apply_fixed_voltage") 399 self._mean_voltage = ChannelSettings( 400 "mean_voltage", [0.0] * 4, self, "_apply_mean_voltage") 401 self._resting_voltage = ChannelSettings( 402 "resting_voltage", [0.0] * 4, self, "_apply_resting_voltage") 403 self._play_duration = ChannelSettings( 404 "play_duration", [1.0] * 4, self, "_apply_play_duration") 405 self._on_ramp_duration = ChannelSettings( 406 "on_ramp_duration", [0.0] * 4, self, "_apply_on_ramp_duration") 407 self._off_ramp_duration = ChannelSettings( 408 "off_ramp_duration", [0.0] * 4, self, "_apply_off_ramp_duration") 409 self._trigger_mode = ChannelSettings( 410 "trigger_mode", ["Normal"] * 2, self, "_apply_trigger_mode") 411 self._link_trigger_channel1 = ChannelSettings( 412 "link_trigger_channel1", [False] * 4, self, 413 "_apply_trigger_channel1_links") 414 self._link_trigger_channel2 = ChannelSettings( 415 "link_trigger_channel2", [False] * 4, self, 416 "_apply_trigger_channel2_links") 417 418 port_name = _common.port_name_from(port_name, serial_number, "SynthPalDevice") 419 self.port = serial.Serial( 420 port_name, 421 self._BAUD_RATE, 422 timeout=timeout, 423 rtscts=True, 424 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 425 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 426 # it, such as another of these classes, gets an error instead 427 exclusive=True, 428 ) 429 self._closed = False 430 try: 431 # Discard anything left in the buffer by an earlier session 432 self.port.reset_input_buffer() 433 self._handshake() 434 self._read_hardware_info() 435 # Client name op + "PYTHON" in ASCII, shown as "PYTHON Connected" 436 self._write_command(self._OP_SET_CLIENT_NAME, b"PYTHON") 437 self.stop() 438 self.set_default_params() 439 except BaseException: 440 # Op 81 means something else to other devices, so it is sent 441 # only once the device has identified itself as a Synth Pal 442 self._close(send_disconnect=self.info.firmware_version is not None) 443 raise 444 445 @staticmethod 446 def serialportlist(ports_to_list="available"): 447 """Return the names of the USB serial ports on this computer. 448 449 Called on the class, without connecting to a device, to find the 450 port name to pass to `SynthPalDevice`. 451 452 Args: 453 ports_to_list: `available` to list only the ports that are 454 not already in use, or `all` to list every USB serial 455 port. Not case sensitive. 456 457 Returns: 458 Sorted list of port names, such as `["COM3", "COM7"]`. 459 460 Raises: 461 PulsePalError: If `ports_to_list` is not `available` or `all`. 462 """ 463 return _common.serialportlist(ports_to_list) 464 465 # ------------------------------------------------------------------ 466 # Settings 467 # ------------------------------------------------------------------ 468 469 def set_default_params(self): 470 """Program the default settings on the device. 471 472 The defaults are a frequency of 100 Hz, and on every output 473 channel a sine wave of 5 V peak to peak around a mean voltage of 474 0 V, resting at 0 V, played for 1 second with no ramps, and a 475 fixed voltage of 5 V for when the waveform is "Fixed Voltage". 476 Both trigger channels are in normal mode, and no output channel is 477 linked to a trigger channel, so a TTL starts nothing until a script 478 links it. They match the settings the device starts with. They are sent at once, 479 also while `SynthPalDevice.auto_sync` is off. 480 """ 481 auto_sync = self._auto_sync 482 self._auto_sync = True 483 try: 484 self.frequency = 100 485 # In this order, each is valid whatever the device holds: a mean of 0 V goes with any 486 # amplitude, 5 V is then a valid amplitude for any waveform, and a sine wave is then valid 487 self._send_levels_in_order((("M", [0] * 4), ("A", [5_000_000] * 4), ("W", ["Sine"] * 4)), 488 force=True) 489 for settings, value in ((self._waveform, "Sine"), (self._peak_to_peak, 5.0), 490 (self._fixed_voltage, 5.0), (self._mean_voltage, 0.0)): 491 settings._store([value] * 4) 492 self.resting_voltage = [0] * 4 493 self.play_duration = [1] * 4 494 self.on_ramp_duration = [0] * 4 495 self.off_ramp_duration = [0] * 4 496 self.trigger_mode = ["Normal"] * 2 497 self._set_trigger_links([False] * 4, [False] * 4) 498 self._unsent = frozenset() # Every setting has been sent 499 finally: 500 self._auto_sync = auto_sync 501 502 @property 503 def frequency(self): 504 """Frequency of all output channels, in Hz. 505 506 1 to 20000 Hz, rounded to 0.01 Hz. It can be changed during 507 playback: playing channels carry on from the same point in their 508 cycle, at the new frequency, and keep the time they have left to 509 play. The frequency played is exact: the device's sample clock is a 510 whole multiple of it (see `SynthPalDevice.sampling_rate`). For 511 `"White Noise"` it sets only the sampling rate. 512 """ 513 return self._frequency 514 515 @frequency.setter 516 def frequency(self, value): 517 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real) \ 518 or not math.isfinite(value): 519 raise PulsePalError( 520 f"frequency must be a number of Hz. Received {value!r}.") 521 centihz = round(value * 100) 522 low = round(self.info.min_frequency * 100) 523 high = round(self.info.max_frequency * 100) 524 if not low <= centihz <= high: 525 raise PulsePalError( 526 f"frequency must be {self.info.min_frequency:g} to " 527 f"{self.info.max_frequency:g} Hz. Received {value!r}." 528 ) 529 if self._auto_sync: 530 self._write_command(self._OP_SET_FREQUENCY, 531 struct.pack("<I", centihz)) 532 confirmed, samples_per_cycle = struct.unpack( 533 "<BI", self._read_raw(5)) 534 if confirmed != 1: 535 raise PulsePalError( 536 f"Synth Pal rejected the frequency {value!r} Hz.") 537 else: 538 samples_per_cycle = _samples_per_cycle(centihz) 539 self._unsent = self._unsent | {"frequency"} # Until sync_to_device() sends it (see auto_sync) 540 self._frequency = centihz / 100 541 self._samples_per_cycle = samples_per_cycle 542 543 @property 544 def auto_sync(self): 545 """Whether assigning a setting programs the device at once. 546 547 `True` (the default): each assignment, such as 548 `S.peak_to_peak[1] = 2`, programs the device at once, as the 549 device's own op for that setting does, also while a trigger 550 channel is in param sync mode. 551 552 `False`: assignments change only this object's copy of the settings, 553 checking each value; `SynthPalDevice.sync_to_device` then sends all 554 of them at once. Use it to store the next trial's settings for a 555 param sync edge (see `SynthPalDevice.trigger_mode`), or to change 556 several settings in one command. Turning it back on does not send 557 changes made meanwhile, so it raises `pulsepal.PulsePalError` while 558 there are any: call `SynthPalDevice.sync_to_device` first to send 559 them, or `SynthPalDevice.sync_from_device` to discard them. A 560 `SynthPalDevice.batch` block does it all for you. `close()` warns of 561 changes never sent. `trigger`, `stop`, `status` and 562 `set_default_params` act at once either way. 563 """ 564 return self._auto_sync 565 566 @auto_sync.setter 567 def auto_sync(self, value): 568 value = to_bool(value, "auto_sync") 569 if value and self._unsent: 570 # Turned on, auto_sync would leave them unsent, and the device playing its old settings 571 raise PulsePalError( 572 f"Changes made with auto_sync off have not been sent to the device: {self._unsent_names()}. Call " 573 "sync_to_device() to send them, or sync_from_device() to discard them, before turning auto_sync on." 574 ) 575 self._auto_sync = value 576 577 def batch(self): 578 """Change several settings, and send them in one command. 579 580 Returns a context manager. In its block, assignments change only 581 this object's copy of the settings (as with 582 `SynthPalDevice.auto_sync` off), and when the block ends, 583 `SynthPalDevice.sync_to_device` sends all of them at once. If the 584 block raises an error, nothing is sent, and the settings go back 585 to what they were. 586 587 ```python 588 with S.batch(): 589 S.frequency = 880 590 S.waveform = ["Triangle"] * 4 591 S.peak_to_peak = [2, 4, 6, 8] 592 ``` 593 594 In param sync mode, the device stores the settings for the next 595 rising edge on the param sync channel (see 596 `SynthPalDevice.trigger_mode`). 597 """ 598 return _common.batch(self) 599 600 def sync_from_device(self): 601 """Read every setting from the device into this object. 602 603 Use it after settings were changed with the device's joystick, for 604 example. The device holds one amplitude per channel: a Fixed 605 Voltage channel's goes to `SynthPalDevice.fixed_voltage`, and any 606 other channel's to `SynthPalDevice.peak_to_peak`. This object keeps 607 the other one, reduced if need be to suit the mean voltage read 608 back. In param sync mode, it reads the settings the device plays 609 now, not a set waiting for the next edge. Changes made with 610 `SynthPalDevice.auto_sync` off and not yet sent are discarded. 611 612 Raises: 613 PulsePalError: If the device does not reply. 614 """ 615 self._write_command(self._OP_GET_ALL_SETTINGS) 616 values = struct.unpack(self._SETTINGS_FORMAT, self._read_raw(struct.calcsize(self._SETTINGS_FORMAT))) 617 centihz = values[0] 618 waveforms = [WAVEFORMS[code] for code in values[1:5]] 619 amplitudes, means = list(values[5:9]), list(values[9:13]) 620 peak_to_peak, fixed_voltage = list(self._peak_to_peak)[1:], list(self._fixed_voltage)[1:] 621 for i, (name, amplitude, mean) in enumerate(zip(waveforms, amplitudes, means)): 622 if name == FIXED_VOLTAGE: 623 fixed_voltage[i] = amplitude / 1e6 624 # This object's own peak to peak voltage must still suit the mean voltage (see _check_levels()) 625 peak_to_peak[i] = min(peak_to_peak[i], (2 * _MAX_VOLTAGE_UV - 2 * abs(mean)) / 1e6) 626 else: 627 peak_to_peak[i] = amplitude / 1e6 628 self._frequency = centihz / 100 629 self._samples_per_cycle = _samples_per_cycle(centihz) 630 self._waveform._store(waveforms) 631 self._peak_to_peak._store(peak_to_peak) 632 self._fixed_voltage._store(fixed_voltage) 633 self._mean_voltage._store([mean / 1e6 for mean in means]) 634 self._resting_voltage._store([rest / 1e6 for rest in values[13:17]]) 635 self._play_duration._store([d / 1e6 for d in values[17:21]]) 636 self._on_ramp_duration._store([d / 1e6 for d in values[21:25]]) 637 self._off_ramp_duration._store([d / 1e6 for d in values[25:29]]) 638 self._link_trigger_channel1._store([bool(link) for link in values[29:33]]) 639 self._link_trigger_channel2._store([bool(link) for link in values[33:37]]) 640 self._trigger_mode._store([TRIGGER_MODES[code] for code in values[37:39]]) 641 self._device_waveform, self._device_amplitude_uv, self._device_mean_uv = waveforms, amplitudes, means 642 self._unsent = frozenset() # Every setting is read 643 644 def export_params(self): 645 """Return every setting, as a dict of plain values and lists. 646 647 Keyed by setting name: `"frequency"`, then the channel settings, 648 each with one value per channel and no unused index 0. It holds 649 only numbers, booleans and names, so it can be saved with `json` 650 and logged with your data, to record exactly what the device 651 played. 652 653 ```python 654 import json 655 with open("trial_settings.json", "w") as f: 656 json.dump(S.export_params(), f) 657 ``` 658 """ 659 params = {"frequency": self._frequency} 660 params.update({name: list(getattr(self, name))[1:] for name in _CHANNEL_SETTINGS}) 661 return params 662 663 def import_params(self, params): 664 """Program the device with settings exported by `SynthPalDevice.export_params`. 665 666 All of them are checked first, and then sent in one command, as 667 at the end of a `SynthPalDevice.batch` block: in param sync mode, 668 the device stores them for the next sync edge. The levels 669 (`waveform`, `peak_to_peak`, `fixed_voltage`, `mean_voltage`) are 670 checked together, so their order in `params` does not matter. 671 Settings missing from `params` keep their values. A name that is 672 not a setting is skipped with a warning, so that settings exported 673 by a newer version of this package, which may have more, can still 674 be imported. 675 676 ```python 677 import json 678 with open("trial_settings.json") as f: 679 S.import_params(json.load(f)) 680 ``` 681 682 Args: 683 params: A dict of setting name to its value: a number for 684 `"frequency"`, and one value per channel (a list, tuple or 685 NumPy array) for the others, as `export_params` returns. 686 687 Raises: 688 PulsePalError: If a value is invalid. Nothing is sent, and the 689 settings keep the values they had. 690 """ 691 names = {"frequency", *_CHANNEL_SETTINGS} 692 unknown = [name for name in params if name not in names] 693 if unknown: 694 warnings.warn(f"import_params(): skipped {', '.join(map(repr, unknown))}, which this version of " 695 "SynthPalDevice does not have.", stacklevel=2) 696 levels = ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage") 697 with self.batch(): 698 for name, values in params.items(): 699 if name in levels: 700 # Each is checked alone here, and the levels together when the block ends (_check_sync()): 701 # an assignment would check each against the others' old values 702 getattr(self, name)._store( 703 self._normalize(name, _common.one_value_per_channel(name, values, 4))) 704 elif name in names: 705 setattr(self, name, values) 706 707 def sync_to_device(self): 708 """Send every setting to the device in one command. 709 710 While a trigger channel is in param sync mode, the device stores 711 the set, replacing any set stored before, and that channel's next 712 rising edge loads it (see `SynthPalDevice.trigger_mode`). Otherwise 713 the device applies it at once. The settings sent are this object's: 714 `frequency`, and for each channel `waveform`, `peak_to_peak` or 715 `fixed_voltage`, `mean_voltage`, `resting_voltage`, 716 `play_duration`, `on_ramp_duration`, `off_ramp_duration`, 717 `link_trigger_channel1` and `link_trigger_channel2`, and 718 `trigger_mode`. They read as sent, including while a set waits for 719 its edge. 720 721 Raises: 722 PulsePalError: If the device rejects the set. 723 """ 724 self._check_sync() 725 self._send_sync() 726 727 def configure(self, channels, *, waveform=None, peak_to_peak=None, fixed_voltage=None, 728 mean_voltage=None, resting_voltage=None, play_duration=None, 729 on_ramp_duration=None, off_ramp_duration=None): 730 """Set several of the output channels' settings at once. 731 732 Settings left out keep their values. The new settings are checked 733 together, so their order does not matter: for example, a channel 734 whose sine wave swings 2 V around a mean voltage of 9 V can go 735 straight to 20 V peak to peak around 0 V. With 736 `SynthPalDevice.auto_sync` on, the class programs the device in an 737 order it accepts at every step. 738 739 ```python 740 S.configure(1, waveform="Sine", peak_to_peak=20, mean_voltage=0) 741 S.configure([2, 3], waveform="Fixed Voltage", fixed_voltage=-2.5, 742 play_duration=0.1) 743 ``` 744 745 Args: 746 channels: Output channel number 1-4, or several as a list, 747 tuple or NumPy array. 748 waveform, peak_to_peak, fixed_voltage, mean_voltage, 749 resting_voltage, play_duration, on_ramp_duration, 750 off_ramp_duration: New values, as for the settings of the 751 same names: one value for every channel given, or a list 752 with one value per channel given, in the same order. 753 754 Raises: 755 PulsePalError: If a value is out of range, or the settings do 756 not go together (nothing is sent), or the device rejects 757 them. 758 """ 759 channels = _common.channel_numbers(channels) 760 if len(set(channels)) != len(channels): 761 raise PulsePalError(f"configure(): channels must be distinct. Received {channels!r}.") 762 requested = {"waveform": waveform, "peak_to_peak": peak_to_peak, "fixed_voltage": fixed_voltage, 763 "mean_voltage": mean_voltage, "resting_voltage": resting_voltage, 764 "play_duration": play_duration, "on_ramp_duration": on_ramp_duration, 765 "off_ramp_duration": off_ramp_duration} 766 new_values = {} 767 for name, value in requested.items(): 768 if value is None: 769 continue 770 values = list(value) if isinstance(value, (list, tuple, np.ndarray)) else [value] 771 if len(values) == 1: 772 values = values * len(channels) 773 if len(values) != len(channels): 774 raise PulsePalError( 775 f"configure(): {name} has {len(values)} values for {len(channels)} channels. Give one " 776 "value, or one value per channel." 777 ) 778 settings = list(getattr(self, name))[1:] 779 for channel, item in zip(channels, values): 780 settings[channel - 1] = item 781 new_values[name] = self._normalize(name, settings) 782 levels = {name: new_values.get(name, list(getattr(self, name))[1:]) 783 for name in ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage")} 784 self._check_levels(levels["peak_to_peak"], levels["mean_voltage"], "configure") 785 if self._auto_sync: 786 self._send_levels(*self._device_levels(**levels)) 787 else: 788 self._unsent = self._unsent | set(new_values) # Until sync_to_device() sends them (see auto_sync) 789 for name in levels: 790 if name in new_values: 791 getattr(self, name)._store(new_values[name]) 792 ops = {"resting_voltage": self._OP_SET_RESTING_VOLTAGE, "play_duration": self._OP_SET_PLAY_DURATION, 793 "on_ramp_duration": self._OP_SET_ON_RAMP_DURATION, 794 "off_ramp_duration": self._OP_SET_OFF_RAMP_DURATION} 795 for name, op in ops.items(): 796 if name in new_values: 797 if self._auto_sync: 798 self._send_setting(name, op, new_values[name]) 799 getattr(self, name)._store(new_values[name]) 800 801 @property 802 def samples_per_cycle(self): 803 """Samples in one cycle of the waveform, at `frequency`. 804 805 The largest multiple of 4 whose sampling rate is at most 806 `DeviceInfo.max_sampling_rate` (100 kHz), so that a sample falls 807 on every edge, peak and trough of every waveform. For example, 808 332 at 300 Hz and 4 at 20 kHz. 809 """ 810 return self._samples_per_cycle 811 812 @property 813 def sampling_rate(self): 814 """The rate at which the device plays samples, in Hz. 815 816 `samples_per_cycle` times `frequency`: 100 kHz or just below it, 817 and at least 50 kHz. For example, 100 kHz at 100 Hz, 99.6 kHz at 818 300 Hz, and 80 kHz at 20 kHz. The device's timer cannot divide 819 every rate exactly, so sample periods differ by up to one tick of 820 its 24 MHz clock (42 ns), arranged so that the frequency is exact. 821 """ 822 return self._samples_per_cycle * self._frequency 823 824 @property 825 def waveform(self): 826 """The waveform of each output channel. 827 828 Indexed by channel number (see "Channel settings" above). Each 829 cycle starts at the trigger: 830 831 - `"Sine"` and `"Triangle"`: start at the mean voltage, rising. 832 - `"Square"`: high for the first half of each cycle, then low. 833 - `"Sawtooth"`: rises from its lowest voltage to its highest, then 834 falls back at the end of the cycle. 835 - `"Fixed Voltage"`: steps to the channel's 836 `SynthPalDevice.fixed_voltage` for the play duration, and 837 returns to the resting voltage. It is not periodic, so the 838 frequency does not change it. 839 - `"White Noise"`: a new random voltage on every sample, uniform 840 over the peak to peak voltage around the mean voltage. It is 841 rendered at the sampling rate the frequency sets 842 (`SynthPalDevice.sampling_rate`), so no claim is made about its 843 spectrum. Each channel has its own random sequence, which carries 844 on from one playback to the next: every playback is new noise. 845 The sequences start from the same point when the device powers 846 up. 847 848 The periodic waveforms and white noise swing 849 `SynthPalDevice.peak_to_peak` around `SynthPalDevice.mean_voltage`. 850 Names are not case sensitive. A change applies to playback in 851 progress. 852 """ 853 return self._waveform 854 855 @waveform.setter 856 def waveform(self, values): 857 self._waveform._assign(values) 858 859 @property 860 def peak_to_peak(self): 861 """The peak to peak voltage of each output channel's waveform, in volts. 862 863 Indexed by channel number (see "Channel settings" above). 0 to 864 20 V: the waveform swings `peak_to_peak / 2` above and below the 865 channel's `SynthPalDevice.mean_voltage`, so it must stay within 866 -10 V to 10 V: `abs(mean_voltage) + peak_to_peak / 2 <= 10`. To 867 raise it beyond what the mean voltage allows, change the mean 868 voltage first, or set both with `SynthPalDevice.configure`. 869 870 A sine wave of 4 V peak to peak swings from -2 V to 2 V around a 871 mean voltage of 0 V: it is 2 sin(2 pi f t). White noise of 4 V peak 872 to peak is uniform from -2 V to 2 V. A `"Fixed Voltage"` channel 873 keeps its peak to peak voltage for when it plays a periodic waveform 874 again. 875 876 Set to the nearest microvolt. A change applies to playback in 877 progress. 878 """ 879 return self._peak_to_peak 880 881 @peak_to_peak.setter 882 def peak_to_peak(self, values): 883 self._peak_to_peak._assign(values) 884 885 @property 886 def fixed_voltage(self): 887 """The voltage each output channel steps to when its waveform is 888 `"Fixed Voltage"`, in volts. 889 890 Indexed by channel number (see "Channel settings" above). -10 to 891 10 V, with any resting voltage and mean voltage. A channel playing 892 another waveform keeps its fixed voltage for when its waveform is 893 `"Fixed Voltage"` again. 894 895 Set to the nearest microvolt. A change applies to playback in 896 progress. 897 """ 898 return self._fixed_voltage 899 900 @fixed_voltage.setter 901 def fixed_voltage(self, values): 902 self._fixed_voltage._assign(values) 903 904 @property 905 def mean_voltage(self): 906 """The mean voltage of each output channel's waveform, in volts. 907 908 Indexed by channel number (see "Channel settings" above). -10 to 909 10 V. A periodic waveform, or white noise, swings around it at full 910 amplitude, and must stay within -10 V to 10 V with the 911 `peak_to_peak` voltage: `abs(mean_voltage) + peak_to_peak / 2 <= 10`. 912 It may differ from the `resting_voltage`, which the channel 913 outputs between playbacks: the ramps move the mean between the two 914 (see "Ramps" above). A `"Fixed Voltage"` ignores it. Set to the 915 nearest microvolt. A change applies to playback in progress. 916 """ 917 return self._mean_voltage 918 919 @mean_voltage.setter 920 def mean_voltage(self, values): 921 self._mean_voltage._assign(values) 922 923 @property 924 def resting_voltage(self): 925 """The resting voltage of each output channel, in volts. 926 927 Indexed by channel number (see "Channel settings" above). -10 to 928 10 V, with any waveform. The channel outputs it while idle, and 929 its ramps start and end there (see "Ramps" above). Set to the 930 nearest microvolt. A change applies to playback in progress. 931 """ 932 return self._resting_voltage 933 934 @resting_voltage.setter 935 def resting_voltage(self, values): 936 self._resting_voltage._assign(values) 937 938 @property 939 def play_duration(self): 940 """How long each output channel plays after a trigger, in seconds. 941 942 Indexed by channel number (see "Channel settings" above). Up to 943 `DeviceInfo.max_play_duration` (3600 s). `0` plays until the 944 channel is stopped. It counts at full amplitude, after the on 945 ramp. The channel stops when the duration has elapsed, part way 946 through a cycle if need be, and returns to its resting voltage, 947 over its off ramp. Durations are counted in samples, rounded to 948 the nearest one, and a nonzero duration lasts at least one sample. 949 A change applies to playback in progress. 950 """ 951 return self._play_duration 952 953 @play_duration.setter 954 def play_duration(self, values): 955 self._play_duration._assign(values) 956 957 @property 958 def on_ramp_duration(self): 959 """How long each output channel fades in after a trigger, in seconds. 960 961 Indexed by channel number (see "Channel settings" above). Up to 962 `DeviceInfo.max_play_duration` (3600 s); `0` (the default) for no 963 ramp. Over it, the peak to peak voltage rises linearly from 0 V to 964 `peak_to_peak`, and the mean from the `resting_voltage` to the 965 `mean_voltage`, before the play duration (see "Ramps" above). 966 Counted in samples, as `play_duration` is. A change applies to 967 playback in progress: a ramp under way keeps the level it has 968 reached, and goes on at the new rate. 969 """ 970 return self._on_ramp_duration 971 972 @on_ramp_duration.setter 973 def on_ramp_duration(self, values): 974 self._on_ramp_duration._assign(values) 975 976 @property 977 def off_ramp_duration(self): 978 """How long each output channel fades out when it stops, in seconds. 979 980 Indexed by channel number (see "Channel settings" above). Up to 981 `DeviceInfo.max_play_duration` (3600 s); `0` (the default) for no 982 ramp. Over it, the peak to peak voltage falls linearly to 0 V, and 983 the mean to the `resting_voltage`, after the play duration or a 984 stop (see "Ramps" above). Counted in samples, as `play_duration` 985 is. A change applies to playback in progress. 986 """ 987 return self._off_ramp_duration 988 989 @off_ramp_duration.setter 990 def off_ramp_duration(self, values): 991 self._off_ramp_duration._assign(values) 992 993 @property 994 def trigger_mode(self): 995 """How each trigger channel acts on the output channels linked to it. 996 997 Indexed by trigger channel number, 1 or 2 (index 0 is unused). 998 These are Pulse Pal's trigger modes: 999 1000 - `"Normal"`: a rising edge starts the linked channels. Channels 1001 that are playing ignore it. 1002 - `"Toggle"`: a rising edge starts the linked channels, or stops 1003 those that are playing. 1004 - `"Gated"`: a rising edge starts the linked channels, and a 1005 falling edge stops them, unless the other trigger channel is 1006 also gated, linked to them, and still high. With a play 1007 duration of 0, a channel plays for exactly as long as the TTL is 1008 high. 1009 - `"Param Sync"`: a rising edge starts and stops nothing (the 1010 channel's links are ignored). It loads the settings most recently 1011 sent by `SynthPalDevice.sync_to_device` (or at the end of a 1012 `SynthPalDevice.batch` block), if any. This is how the next 1013 trial's settings are sent during the current trial and applied 1014 the instant it starts. 1015 1016 At a param sync edge, the frequency and both trigger modes change 1017 at once: they are shared by all channels. An output channel at its 1018 resting voltage takes its new settings at once. One that is playing 1019 (off ramp included) finishes on the settings it started with, at 1020 the new frequency, and takes the new ones the moment it reaches its 1021 resting voltage, so the next trigger plays them. A trigger mode 1022 sent by `SynthPalDevice.sync_to_device` applies from the next 1023 edge. 1024 1025 Only `SynthPalDevice.sync_to_device` is held back for the edge: 1026 with `SynthPalDevice.auto_sync` on, every assignment programs the 1027 device at once, also in param sync mode. So leaving param sync mode 1028 means assigning `trigger_mode` with `auto_sync` on. When no trigger 1029 channel is left in the mode, the device discards a stored set. 1030 1031 To start channels on the same edge, wire the TTL to the other 1032 trigger channel too: the settings load first, so the channels it 1033 starts play the new settings. Connecting, and `set_default_params`, 1034 take both trigger channels out of param sync mode. 1035 1036 Names are not case sensitive. 1037 """ 1038 return self._trigger_mode 1039 1040 @trigger_mode.setter 1041 def trigger_mode(self, values): 1042 self._trigger_mode._assign(values) 1043 1044 @property 1045 def link_trigger_channel1(self): 1046 """Whether each output channel is triggered by trigger channel 1. 1047 1048 Indexed by channel number (see "Channel settings" above). By 1049 default no output channel is linked to either trigger channel, so 1050 a TTL starts nothing until a script links it. 1051 """ 1052 return self._link_trigger_channel1 1053 1054 @link_trigger_channel1.setter 1055 def link_trigger_channel1(self, values): 1056 self._link_trigger_channel1._assign(values) 1057 1058 @property 1059 def link_trigger_channel2(self): 1060 """Whether each output channel is triggered by trigger channel 2. 1061 1062 Indexed by channel number (see "Channel settings" above). 1063 """ 1064 return self._link_trigger_channel2 1065 1066 @link_trigger_channel2.setter 1067 def link_trigger_channel2(self, values): 1068 self._link_trigger_channel2._assign(values) 1069 1070 def set_screen_saver(self, enabled, timeout=1800): 1071 """Switch the device's screen saver on or off, and set its timeout. 1072 1073 With the screen saver on, the device dims its screen once it has 1074 been left alone for `timeout` seconds: no command from a computer, 1075 no rising edge on a trigger channel, and no joystick click or 1076 push. The next of these brings the screen back. The device keeps 1077 both settings in its EEPROM, shared with Pulse Pal firmware, and 1078 saves them once no channel is playing. A new device starts with 1079 the screen saver on and 1800 s. 1080 1081 Args: 1082 enabled: `True` to switch the screen saver on, `False` to 1083 switch it off. 1084 timeout: Seconds without activity before the screen dims, 1 to 1085 65535. 1086 1087 Raises: 1088 PulsePalError: If a value is out of range. 1089 """ 1090 state = to_bool(enabled, "enabled") 1091 if isinstance(timeout, (bool, np.bool_)) \ 1092 or not isinstance(timeout, numbers.Integral) \ 1093 or not 1 <= timeout <= 65535: 1094 raise PulsePalError( 1095 "timeout must be a whole number of seconds from 1 to 65535. " 1096 f"Received {timeout!r}." 1097 ) 1098 self._write_command(self._OP_SET_SCREEN_SAVER, 1099 struct.pack("<BH", int(state), int(timeout))) 1100 self._read_ack("set_screen_saver()") 1101 1102 # ------------------------------------------------------------------ 1103 # Playback 1104 # ------------------------------------------------------------------ 1105 1106 def trigger(self, channels): 1107 """Trigger output channels in software. 1108 1109 Channels that are idle start from the beginning of their waveform 1110 cycle, together. Channels that are playing ignore it, as they 1111 ignore a trigger channel in normal mode. 1112 1113 ```python 1114 S.trigger(1) 1115 S.trigger([2, 4]) # a list, tuple or NumPy array 1116 ``` 1117 1118 Args: 1119 channels: Output channel number 1-4, or several as a list, 1120 tuple or NumPy array. 1121 """ 1122 self._write_command(self._OP_TRIGGER, bytes([_common.channel_bits(channels)])) 1123 1124 def stop(self, channels=None): 1125 """Stop playback. The stopped channels return to their resting 1126 voltage, over their off ramps. 1127 1128 Args: 1129 channels: Output channel number 1-4, or several as a list, 1130 tuple or NumPy array. `None` stops all channels. 1131 """ 1132 bits = self._ALL_CHANNELS if channels is None else _common.channel_bits(channels) 1133 self._write_command(self._OP_STOP, bytes([bits])) 1134 1135 def status(self): 1136 """Read the device's playback state. 1137 1138 Returns: 1139 A `DeviceStatus`, with the channels that are playing, the 1140 samples per cycle, each channel's output range, and timing 1141 figures. 1142 """ 1143 self._write_command(self._OP_GET_STATUS) 1144 values = struct.unpack( 1145 self._STATUS_FORMAT, 1146 self._read_raw(struct.calcsize(self._STATUS_FORMAT)), 1147 ) 1148 playing_bits = values[0] 1149 range_names = list(OUTPUT_RANGES) 1150 return DeviceStatus( 1151 playing=[ch for ch in range(1, 5) 1152 if playing_bits & (1 << (ch - 1))], 1153 samples_per_cycle=values[1], 1154 output_ranges=[None, *(range_names[i] if i < len(range_names) 1155 else None for i in values[2:6])], 1156 longest_interrupt_us=values[6] / 1000, 1157 late_updates=values[7], 1158 ) 1159 1160 def _playback_checksums(self): 1161 """For testing: what each channel played since it last started. 1162 1163 Returns two lists indexed by channel number, with index 0 unused: 1164 the number of samples played, and the sum of their DAC codes 1165 modulo 2**32. `tests/synthpal_hardware_test.py` compares them with 1166 the samples it expects, to check what the device played. 1167 """ 1168 self._write_command(self._OP_GET_PLAYBACK_CHECKSUMS) 1169 values = struct.unpack("<8I", self._read_raw(32)) 1170 return [None, *values[:4]], [None, *values[4:]] 1171 1172 # ------------------------------------------------------------------ 1173 # Connection 1174 # ------------------------------------------------------------------ 1175 1176 def close(self): 1177 """Close the connection to the device. 1178 1179 The device stops playback on all channels, each over its off ramp, 1180 and shows its own name on its screen again, in place of "PYTHON 1181 Connected". It keeps its settings, so TTL triggers still play the 1182 channels. Safe to call more than once. Called automatically when 1183 leaving a `with` block and when the object is garbage collected, 1184 so playback also stops when the last reference to the object 1185 goes. 1186 1187 """ 1188 self._close(send_disconnect=True) 1189 1190 def _close(self, send_disconnect): 1191 """Close the connection. send_disconnect: tell the device first (op 81). False when the device may not 1192 run this firmware: op 81 means something else to other devices.""" 1193 if getattr(self, "_closed", True): 1194 return 1195 self._closed = True 1196 if send_disconnect and self._unsent: 1197 _common.warn(f"SynthPalDevice closed with changes made with auto_sync off that were never sent to the " 1198 f"device: {self._unsent_names()}.") 1199 self._reply_pending = None # Nothing more is read, so a reply still to come needs no discarding 1200 try: 1201 if send_disconnect and self.port and self.port.is_open: 1202 self._write_command(self._OP_DISCONNECT) 1203 except Exception: 1204 pass # The port may already be gone, e.g. the cable was unplugged 1205 finally: 1206 if self.port and self.port.is_open: 1207 self.port.close() 1208 1209 def __enter__(self): 1210 """Enter a `with` block, returning the connected device.""" 1211 return self 1212 1213 def __exit__(self, exc_type, exc_value, traceback): 1214 """Close the port when leaving a `with` block. 1215 1216 Returns: 1217 `False`, so any exception raised in the block propagates. 1218 """ 1219 self.close() 1220 return False 1221 1222 def __del__(self): 1223 """Close the port when the object is collected.""" 1224 try: 1225 self.close() 1226 except Exception: 1227 # Destructors should not raise; the serial object may already be 1228 # gone during interpreter shutdown. 1229 pass 1230 1231 def __setattr__(self, name, value): 1232 """Refuse a public attribute that is not a setting, such as a misspelled one: it would program nothing (see 1233 _common.refuse_unknown_attribute()).""" 1234 if name[0] != "_": 1235 _common.refuse_unknown_attribute(self, name, _SETTING_NAMES) 1236 super().__setattr__(name, value) 1237 1238 def __repr__(self): 1239 """Describe the device and its settings, e.g. with `print(S)`.""" 1240 port = getattr(getattr(self, "port", None), "port", None) 1241 lines = [ 1242 f"SynthPalDevice on {port} (Synth Pal firmware v{self.info.firmware_version})", 1243 f"frequency: {self._frequency} Hz ({self._samples_per_cycle} samples per cycle)", 1244 f"auto_sync: {self._auto_sync}", 1245 ] 1246 lines += [f"{name}: {list(getattr(self, name))}" for name in _CHANNEL_SETTINGS] 1247 return "\n".join(lines) 1248 1249 # ------------------------------------------------------------------ 1250 # Internals: settings 1251 # ------------------------------------------------------------------ 1252 1253 def _handshake(self): 1254 """Check that the device runs a supported Synth Pal firmware.""" 1255 self._write_command(self._OP_HANDSHAKE) 1256 handshake = _common.read_handshake_reply(self.port) # Skips a reply to an earlier session's command 1257 if handshake is None: 1258 raise PulsePalError( 1259 f"No reply from the device on {self.port.port}. Is it a " 1260 "Pulse Pal 3 running Synth Pal firmware?" 1261 f" {_common.NO_REPLY_HINT}" 1262 ) 1263 reply, version = handshake 1264 if reply != self._HANDSHAKE_REPLY: 1265 if reply in _common.FIRMWARE_BY_HANDSHAKE_REPLY: 1266 raise _common.other_firmware_error(reply, version, self.port.port, "Synth Pal") 1267 raise PulsePalError( 1268 "Incorrect handshake returned. Expected " 1269 f"{self._HANDSHAKE_REPLY}, received {reply}." 1270 ) 1271 if version > self._CURRENT_FIRMWARE_VERSION: 1272 # New firmware only adds commands (see PROTOCOL.md), so this class still works with it 1273 warnings.warn( 1274 f"Synth Pal firmware v{version} is newer than this pulsepal package knows " 1275 f"(v{self._CURRENT_FIRMWARE_VERSION}). It works with it, but update the package to use what is " 1276 "new.", stacklevel=3) 1277 self.info.firmware_version = version 1278 1279 def _read_hardware_info(self): 1280 self._write_command(self._OP_HARDWARE_INFO) 1281 ( 1282 self.info.hardware_version, 1283 self.info.n_channels, 1284 min_centihz, 1285 max_centihz, 1286 self.info.max_sampling_rate, 1287 self.info.timer_clock_hz, 1288 max_duration_us, 1289 ) = struct.unpack( 1290 self._HARDWARE_INFO_FORMAT, 1291 self._read_raw(struct.calcsize(self._HARDWARE_INFO_FORMAT)), 1292 ) 1293 self.info.min_frequency = min_centihz / 100 1294 self.info.max_frequency = max_centihz / 100 1295 self.info.max_play_duration = max_duration_us / 1e6 1296 self._write_command(self._OP_HARDWARE_REVISION) 1297 self.info.circuit_board_revision = self._read_raw(1)[0] 1298 self._write_command(_common.OP_SERIAL_NUMBER) 1299 self.info.serial_number = _common.read_serial_number(self._read_raw) 1300 1301 def _normalize(self, name, values): 1302 """Check new values for all four channels of a setting, and return them as its list holds them.""" 1303 if name == "waveform": 1304 return [to_name(value, WAVEFORMS, "waveform") for value in values] 1305 if name == "peak_to_peak": 1306 return [self._to_volts(value, name, 0, 20, " peak to peak") for value in values] 1307 if name in ("fixed_voltage", "mean_voltage", "resting_voltage"): 1308 return [self._to_volts(value, name, -10, 10) for value in values] 1309 zero_meaning = "0 (play until stopped)" if name == "play_duration" else "0 (no ramp)" 1310 return [self._to_seconds(value, name, zero_meaning) for value in values] 1311 1312 def _check_levels(self, peak_to_peak, mean_voltage, setting): 1313 """Check that each channel's periodic waveform stays within -10 V to 10 V, as the device does. 1314 1315 Checked on every channel, also one playing a fixed voltage, so that a change of waveform always 1316 suits the levels it finds. `setting` is the one being changed, for the advice in the message. 1317 """ 1318 for channel, (pp, mean) in enumerate(zip(peak_to_peak, mean_voltage), start=1): 1319 pp_uv, mean_uv = _microvolts(pp), _microvolts(mean) 1320 if 2 * abs(mean_uv) + pp_uv > 2 * _MAX_VOLTAGE_UV: 1321 advice = { 1322 "peak_to_peak": "Change mean_voltage first, set both with configure(), or choose a " 1323 "smaller peak_to_peak.", 1324 "mean_voltage": "Change peak_to_peak first, set both with configure(), or choose a " 1325 "smaller mean_voltage.", 1326 }.get(setting, "Change peak_to_peak or mean_voltage.") 1327 raise PulsePalError( 1328 f"On channel {channel}, a mean voltage of {mean:g} V and a peak to peak voltage of " 1329 f"{pp:g} V would reach {(abs(mean_uv) + pp_uv / 2) / 1e6:g} V. The waveform must " 1330 f"stay within -10 V to 10 V. {advice}" 1331 ) 1332 1333 def _device_levels(self, waveform, peak_to_peak, fixed_voltage, mean_voltage): 1334 """What the device holds for these settings: waveforms, amplitudes in microvolts (a fixed voltage, or a 1335 periodic waveform's peak to peak voltage) and mean voltages in microvolts.""" 1336 amplitudes = [_microvolts(fixed if name == FIXED_VOLTAGE else pp) 1337 for name, pp, fixed in zip(waveform, peak_to_peak, fixed_voltage)] 1338 return list(waveform), amplitudes, [_microvolts(mean) for mean in mean_voltage] 1339 1340 def _apply_levels(self, name, values): 1341 """Apply new values of waveform, peak_to_peak, fixed_voltage or mean_voltage, for all four channels.""" 1342 values = self._normalize(name, values) 1343 levels = {level: list(getattr(self, level))[1:] 1344 for level in ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage")} 1345 levels[name] = values 1346 self._check_levels(levels["peak_to_peak"], levels["mean_voltage"], name) 1347 if self._auto_sync: 1348 self._send_levels(*self._device_levels(**levels)) 1349 else: 1350 self._unsent = self._unsent | {name} # Until sync_to_device() sends it (see auto_sync) 1351 return values 1352 1353 def _apply_waveform(self, values): 1354 return self._apply_levels("waveform", values) 1355 1356 def _apply_peak_to_peak(self, values): 1357 return self._apply_levels("peak_to_peak", values) 1358 1359 def _apply_fixed_voltage(self, values): 1360 return self._apply_levels("fixed_voltage", values) 1361 1362 def _apply_mean_voltage(self, values): 1363 return self._apply_levels("mean_voltage", values) 1364 1365 def _send_levels(self, waveforms, amplitudes_uv, means_uv): 1366 """Program the device's waveforms, amplitudes and mean voltages, in an order it accepts at every step. 1367 1368 The device checks each op against what it holds for the other two (see _is_valid_level()), and one 1369 order does not suit every change: a sine wave of 20 V peak to peak cannot become a fixed voltage of 1370 -5 V by either op first. So each channel first takes an amplitude that suits both its current and 1371 its new waveform and mean voltage: its current amplitude if it can, else its new one, else 0 V, which 1372 suits any. Then the waveforms, the mean voltages and the new amplitudes follow. If the device 1373 refuses a step, which means this object's record of what it holds is out of date (e.g. a param sync 1374 edge loaded a stored set), the change is made from 0 V, which works from any state. 1375 """ 1376 current = (self._device_waveform, self._device_amplitude_uv, self._device_mean_uv) 1377 if (waveforms, amplitudes_uv, means_uv) == tuple(map(list, current)): 1378 return 1379 first_amplitudes = [] 1380 for w0, a0, m0, w1, a1, m1 in zip(*current, waveforms, amplitudes_uv, means_uv): 1381 for candidate in (a0, a1, 0): 1382 if all(_is_valid_level(w, candidate, m) for w, m in ((w0, m0), (w1, m0), (w1, m1))): 1383 first_amplitudes.append(candidate) 1384 break 1385 steps = (("A", first_amplitudes), ("W", waveforms), ("M", means_uv), ("A", amplitudes_uv)) 1386 try: 1387 self._send_levels_in_order(steps) 1388 except _Rejected: 1389 self._send_levels_in_order((("A", [0] * 4), ("W", waveforms), ("M", means_uv), 1390 ("A", amplitudes_uv)), force=True) 1391 1392 def _send_levels_in_order(self, steps, force=False): 1393 """Send ops 'A' (amplitudes, uV), 'W' (waveform names) and 'M' (mean voltages, uV), in order, and keep 1394 the record of what the device holds. Unless `force`, an op that would not change what the record says 1395 the device holds is skipped.""" 1396 for op, values in steps: 1397 if op == "W": 1398 if values == self._device_waveform and not force: 1399 continue 1400 self._write_command(self._OP_SET_WAVEFORM, bytes(WAVEFORMS.index(name) for name in values)) 1401 self._read_ack("setting waveform") 1402 self._device_waveform = list(values) 1403 elif op == "A": 1404 if values == self._device_amplitude_uv and not force: 1405 continue 1406 self._write_command(self._OP_SET_AMPLITUDE, struct.pack("<4i", *values)) 1407 self._read_ack("setting the amplitude") 1408 self._device_amplitude_uv = list(values) 1409 else: 1410 if values == self._device_mean_uv and not force: 1411 continue 1412 self._write_command(self._OP_SET_MEAN_VOLTAGE, struct.pack("<4i", *values)) 1413 self._read_ack("setting mean_voltage") 1414 self._device_mean_uv = list(values) 1415 1416 def _apply_resting_voltage(self, values): 1417 # Any resting voltage within -10 V to 10 V goes with any waveform 1418 values = self._normalize("resting_voltage", values) 1419 if self._auto_sync: 1420 self._send_setting("resting_voltage", self._OP_SET_RESTING_VOLTAGE, values) 1421 else: 1422 self._unsent = self._unsent | {"resting_voltage"} 1423 return values 1424 1425 def _apply_play_duration(self, values): 1426 return self._apply_duration("play_duration", self._OP_SET_PLAY_DURATION, values) 1427 1428 def _apply_on_ramp_duration(self, values): 1429 return self._apply_duration("on_ramp_duration", self._OP_SET_ON_RAMP_DURATION, values) 1430 1431 def _apply_off_ramp_duration(self, values): 1432 return self._apply_duration("off_ramp_duration", self._OP_SET_OFF_RAMP_DURATION, values) 1433 1434 def _apply_duration(self, name, op, values): 1435 values = self._normalize(name, values) 1436 if self._auto_sync: 1437 self._send_setting(name, op, values) 1438 else: 1439 self._unsent = self._unsent | {name} 1440 return values 1441 1442 def _send_setting(self, name, op, values): 1443 """Send the resting voltages (int32 microvolts) or a duration (uint32 microseconds), for all four 1444 channels.""" 1445 if name == "resting_voltage": 1446 data = struct.pack("<4i", *(_microvolts(v) for v in values)) 1447 else: 1448 data = struct.pack("<4I", *(round(d * 1e6) for d in values)) 1449 self._write_command(op, data) 1450 self._read_ack(f"setting {name}") 1451 1452 def _apply_trigger_mode(self, values): 1453 names = [to_name(value, TRIGGER_MODES, "trigger mode") for value in values] 1454 if self._auto_sync: 1455 self._write_command( 1456 self._OP_SET_TRIGGER_MODE, 1457 bytes(TRIGGER_MODES.index(name) for name in names), 1458 ) 1459 self._read_ack("setting trigger_mode") 1460 else: 1461 self._unsent = self._unsent | {"trigger_mode"} 1462 return names 1463 1464 # One op programs both trigger channels' links, so each list is sent 1465 # with the other's current values 1466 def _apply_trigger_channel1_links(self, values): 1467 links1 = [to_bool(v, "link_trigger_channel1") for v in values] 1468 self._send_trigger_links(links1, self._link_trigger_channel2[1:]) 1469 if not self._auto_sync: 1470 self._unsent = self._unsent | {"link_trigger_channel1"} 1471 return links1 1472 1473 def _apply_trigger_channel2_links(self, values): 1474 links2 = [to_bool(v, "link_trigger_channel2") for v in values] 1475 self._send_trigger_links(self._link_trigger_channel1[1:], links2) 1476 if not self._auto_sync: 1477 self._unsent = self._unsent | {"link_trigger_channel2"} 1478 return links2 1479 1480 def _set_trigger_links(self, links1, links2): 1481 """Program both trigger channels' links with one command.""" 1482 self._send_trigger_links(links1, links2) 1483 self._link_trigger_channel1._store(links1) 1484 self._link_trigger_channel2._store(links2) 1485 1486 def _send_trigger_links(self, links1, links2): 1487 if self._auto_sync: 1488 self._write_command(self._OP_SET_TRIGGER_LINKS, 1489 bytes([*map(int, links1), *map(int, links2)])) 1490 self._read_ack("setting the trigger channel links") 1491 1492 def _snapshot(self): 1493 """All settings, and which are unsent, for batch() to restore if its block fails.""" 1494 settings = {name: list(getattr(self, name))[1:] for name in _CHANNEL_SETTINGS} 1495 return settings, self._frequency, self._samples_per_cycle, self._unsent 1496 1497 def _restore(self, snapshot): 1498 settings, self._frequency, self._samples_per_cycle, self._unsent = snapshot 1499 for name, values in settings.items(): 1500 getattr(self, name)._store(values) 1501 1502 def _unsent_names(self): 1503 """The settings assigned with auto_sync off and not yet sent, for messages, in the class's order.""" 1504 return ", ".join(name for name in _SETTING_NAMES if name in self._unsent) 1505 1506 def _check_sync(self): 1507 """Check the whole set before sync_to_device() sends it.""" 1508 self._check_levels(self._peak_to_peak[1:], self._mean_voltage[1:], "sync") 1509 1510 def _send_sync(self): 1511 """Send every setting with op 85 ('U'). The device checks the whole set, so no order matters.""" 1512 self._unsent = frozenset() # All are sent below 1513 waveforms, amplitudes, means = self._device_levels( 1514 self._waveform[1:], self._peak_to_peak[1:], self._fixed_voltage[1:], self._mean_voltage[1:]) 1515 links = [int(link) for link in (*self._link_trigger_channel1[1:], 1516 *self._link_trigger_channel2[1:])] 1517 payload = struct.pack( 1518 self._SETTINGS_FORMAT, 1519 round(self._frequency * 100), 1520 *(WAVEFORMS.index(name) for name in waveforms), 1521 *amplitudes, *means, *(_microvolts(v) for v in self._resting_voltage[1:]), 1522 *(round(d * 1e6) for d in self._play_duration[1:]), 1523 *(round(d * 1e6) for d in self._on_ramp_duration[1:]), 1524 *(round(d * 1e6) for d in self._off_ramp_duration[1:]), 1525 *links, 1526 *(TRIGGER_MODES.index(name) for name in self._trigger_mode[1:]), 1527 ) 1528 self._write_command(self._OP_SET_ALL_SETTINGS, payload) 1529 self._read_ack("sync_to_device()") 1530 # Applied now, or, in param sync mode, at the next edge. Either way these are the levels the next 1531 # assignments build on; if a stored set has not loaded yet, _send_levels() finds out and recovers. 1532 self._device_waveform, self._device_amplitude_uv, self._device_mean_uv = waveforms, amplitudes, means 1533 1534 @staticmethod 1535 def _to_volts(value, name, low, high, note=""): 1536 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real) \ 1537 or not math.isfinite(value) or not low <= value <= high: 1538 raise PulsePalError( 1539 f"{name} values must be numbers of volts from {low} to " 1540 f"{high}{note}. Received {value!r}." 1541 ) 1542 return float(value) 1543 1544 def _to_seconds(self, value, name, zero_meaning): 1545 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real): 1546 raise PulsePalError( 1547 f"{name} must be in seconds. Received {value!r}." 1548 ) 1549 duration = float(value) 1550 if not math.isfinite(duration) or not \ 1551 0 <= duration <= self.info.max_play_duration: 1552 raise PulsePalError( 1553 f"{name} must be {zero_meaning} or a positive number " 1554 f"of seconds up to {self.info.max_play_duration:g}. " 1555 f"Received {value!r}." 1556 ) 1557 return duration 1558 1559 # ------------------------------------------------------------------ 1560 # Internals: serial port 1561 # ------------------------------------------------------------------ 1562 1563 def _write_command(self, op_code, data=b""): 1564 """Send one command, with its framing byte, in a single write. 1565 1566 A command whose reply was not read in full, because the read timed out or was interrupted (Ctrl-C), leaves 1567 that reply, or the rest of it, to arrive later. Read as the reply to the next command, it would put every 1568 reply after it out of step. So it is discarded first (_discard_lost_reply()), and this command's reply, if it 1569 has one, is pending until _read_raw() has read it, as in PulsePalDevice. 1570 """ 1571 if self._reply_pending is not None: 1572 self._discard_lost_reply() 1573 if op_code not in _OPS_WITHOUT_TRACKED_REPLY: 1574 self._reply_pending = op_code # Before the write: a Ctrl-C can come as soon as it returns 1575 message = bytes([self._OP_MENU_BYTE, op_code]) + data 1576 bytes_written = self.port.write(message) 1577 if bytes_written != len(message): 1578 raise PulsePalError( 1579 f"Wrote {bytes_written} byte(s), expected to write " 1580 f"{len(message)} byte(s)." 1581 ) 1582 1583 def _discard_lost_reply(self): 1584 """Discard what arrives of a reply that was not read in full (see _write_command()), and bring this object 1585 back in step with the device. 1586 1587 The command may have changed settings, unknown to this object: an assignment interrupted after its write, 1588 for one, which the device took and this object did not. So after one of _SETTING_OPS, they are read back 1589 (op 'R'), outside param sync mode, where op 'R' returns the settings playing rather than a set stored for the 1590 next sync edge. 1591 """ 1592 op = self._reply_pending 1593 self._reply_pending = None 1594 n_late = len(_common.read_until_quiet(self.port)) 1595 read_back = op in self._SETTING_OPS and "Param Sync" not in list(self._trigger_mode)[1:] 1596 _common.warn_lost_reply(f"op {chr(op)!r}", n_late, 1597 ", and the settings were read back from the device" if read_back else "") 1598 if read_back: 1599 try: 1600 self.sync_from_device() 1601 except PulsePalError as exc: 1602 raise PulsePalError( 1603 f"Synth Pal did not send its settings, read back after the reply to an earlier command was lost. " 1604 f"{_common.NO_REPLY_HINT}" 1605 ) from exc 1606 1607 def _read_raw(self, n_bytes): 1608 """Read exactly n_bytes from the serial port: the whole reply to the command last sent, which is then no 1609 longer pending (see _write_command()). Only connecting reads a reply in more than one call.""" 1610 message = self.port.read(n_bytes) 1611 if len(message) < n_bytes: 1612 raise PulsePalError( 1613 f"Synth Pal sent {len(message)} of the {n_bytes} byte(s) of its reply within " 1614 f"{self.port.timeout} s. {_common.NO_REPLY_HINT}" 1615 ) 1616 self._reply_pending = None 1617 return message 1618 1619 def _read_ack(self, context): 1620 """Read a one-byte confirmation: 1 if the device executed the 1621 command, 0 if it rejected it.""" 1622 try: 1623 reply = self._read_raw(1)[0] 1624 except PulsePalError as exc: 1625 raise PulsePalError( 1626 f"Synth Pal did not confirm {context}: no reply arrived within {self.port.timeout} s. " 1627 f"{_common.NO_REPLY_HINT}" 1628 ) from exc 1629 if reply != 1: 1630 raise _Rejected( 1631 f"Synth Pal rejected {context}. A value was out of range." 1632 )
288class SynthPalDevice: 289 """A class to control a Synth Pal on a USB serial port. 290 291 Creating an instance opens the serial port, checks that the device 292 runs Synth Pal firmware, reads its properties into 293 `SynthPalDevice.info`, shows "PYTHON Connected" on the device's 294 screen, stops any playback and programs the default settings (see 295 `SynthPalDevice.set_default_params`). 296 297 ```python 298 from pulsepal import SynthPalDevice 299 300 S = SynthPalDevice("COM3") 301 S.waveform[1] = "Triangle" 302 S.trigger(1) 303 S.close() 304 ``` 305 306 Replace "COM3" with the device's USB serial port name, which 307 `SynthPalDevice.serialportlist` lists. `SynthPalDevice` is also a 308 context manager, which closes the port on exit. 309 310 Closing the connection stops playback, as it does on Pulse Pal (each 311 channel over its off ramp), and puts the device's own name back on its 312 screen. The device keeps its settings, so TTL triggers still play the 313 channels. 314 """ 315 316 port: "serial.Serial" 317 """The open `serial.Serial` port connected to the device.""" 318 319 info: DeviceInfo 320 """Properties of the connected device. See `DeviceInfo`.""" 321 322 _CURRENT_FIRMWARE_VERSION = 1 323 _BAUD_RATE = 12000000 # USB serial ignores the baud rate 324 325 _OP_MENU_BYTE = _common.OP_MENU_BYTE 326 _OP_HANDSHAKE = _common.OP_HANDSHAKE 327 _OP_DISCONNECT = 81 328 _OP_SET_CLIENT_NAME = 89 329 _OP_SET_SCREEN_SAVER = 99 330 _OP_HARDWARE_REVISION = 100 331 _OP_HARDWARE_INFO = ord("N") 332 _OP_SET_FREQUENCY = ord("F") 333 _OP_SET_WAVEFORM = ord("W") 334 _OP_SET_AMPLITUDE = ord("A") 335 _OP_SET_RESTING_VOLTAGE = ord("V") 336 _OP_SET_MEAN_VOLTAGE = ord("M") 337 _OP_SET_PLAY_DURATION = ord("D") 338 _OP_SET_ON_RAMP_DURATION = ord("B") 339 _OP_SET_OFF_RAMP_DURATION = ord("E") 340 _OP_SET_TRIGGER_LINKS = ord("I") 341 _OP_SET_TRIGGER_MODE = ord("T") 342 _OP_SET_ALL_SETTINGS = ord("U") 343 _OP_GET_ALL_SETTINGS = ord("R") 344 # Every setting, as ops 85 ('U') and 82 ('R') carry them: frequency (centiHz), waveform codes, amplitudes, 345 # mean and resting voltages (uV), play, on ramp and off ramp durations (us), links, trigger modes 346 _SETTINGS_FORMAT = "<I4B4i4i4i4I4I4I8B2B" 347 _OP_TRIGGER = ord("P") 348 _OP_STOP = ord("X") 349 _OP_GET_STATUS = ord("G") 350 _OP_GET_PLAYBACK_CHECKSUMS = ord("Z") 351 352 _HANDSHAKE_REPLY = 83 # 'S' 353 _HARDWARE_INFO_FORMAT = "<BBIIIII" 354 _STATUS_FORMAT = "<BI4BII" 355 _ALL_CHANNELS = 0x0F 356 # The ops that change settings: if the reply to one is lost, they are read back (_discard_lost_reply()) 357 _SETTING_OPS = frozenset(map(ord, "FWAVMDBEITU")) 358 359 _reply_pending = None # The op code of a command whose reply has not been read in full yet (see _write_command()) 360 _unsent = frozenset() # The settings assigned with auto_sync off since the settings were last sent or read 361 362 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 363 """Open a connection to a Synth Pal. 364 365 Args: 366 port_name: USB serial port of the device, such as `COM3` on 367 Windows or `/dev/ttyACM0` on Linux. 368 serial_number: Instead of `port_name`, the device's USB serial 369 number (`DeviceInfo.serial_number`), which finds its port 370 wherever it is plugged in. 371 timeout: Serial read timeout, in seconds. 372 373 Firmware newer than this module knows is used with a warning: new 374 firmware only adds commands. Update the package to use what is new. 375 376 Raises: 377 PulsePalError: If the device does not reply to the handshake, or 378 runs other firmware. 379 serial.SerialException: If the serial port cannot be opened, 380 e.g. because another program has it open (on Linux and 381 macOS, another session of these classes, which lock it). 382 """ 383 self._closed = True 384 self._auto_sync = True 385 self.info = DeviceInfo() 386 self._frequency = None 387 self._samples_per_cycle = None 388 # What the device holds for each output channel's waveform, amplitude (in microvolts: a periodic 389 # waveform's peak to peak voltage, or a fixed voltage) and mean voltage (in microvolts), as far as this 390 # object has programmed them. _send_levels() changes them in an order the device accepts. 391 self._device_waveform = ["Sine"] * 4 392 self._device_amplitude_uv = [5_000_000] * 4 393 self._device_mean_uv = [0] * 4 394 self._waveform = ChannelSettings( 395 "waveform", ["Sine"] * 4, self, "_apply_waveform") 396 self._peak_to_peak = ChannelSettings( 397 "peak_to_peak", [5.0] * 4, self, "_apply_peak_to_peak") 398 self._fixed_voltage = ChannelSettings( 399 "fixed_voltage", [5.0] * 4, self, "_apply_fixed_voltage") 400 self._mean_voltage = ChannelSettings( 401 "mean_voltage", [0.0] * 4, self, "_apply_mean_voltage") 402 self._resting_voltage = ChannelSettings( 403 "resting_voltage", [0.0] * 4, self, "_apply_resting_voltage") 404 self._play_duration = ChannelSettings( 405 "play_duration", [1.0] * 4, self, "_apply_play_duration") 406 self._on_ramp_duration = ChannelSettings( 407 "on_ramp_duration", [0.0] * 4, self, "_apply_on_ramp_duration") 408 self._off_ramp_duration = ChannelSettings( 409 "off_ramp_duration", [0.0] * 4, self, "_apply_off_ramp_duration") 410 self._trigger_mode = ChannelSettings( 411 "trigger_mode", ["Normal"] * 2, self, "_apply_trigger_mode") 412 self._link_trigger_channel1 = ChannelSettings( 413 "link_trigger_channel1", [False] * 4, self, 414 "_apply_trigger_channel1_links") 415 self._link_trigger_channel2 = ChannelSettings( 416 "link_trigger_channel2", [False] * 4, self, 417 "_apply_trigger_channel2_links") 418 419 port_name = _common.port_name_from(port_name, serial_number, "SynthPalDevice") 420 self.port = serial.Serial( 421 port_name, 422 self._BAUD_RATE, 423 timeout=timeout, 424 rtscts=True, 425 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 426 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 427 # it, such as another of these classes, gets an error instead 428 exclusive=True, 429 ) 430 self._closed = False 431 try: 432 # Discard anything left in the buffer by an earlier session 433 self.port.reset_input_buffer() 434 self._handshake() 435 self._read_hardware_info() 436 # Client name op + "PYTHON" in ASCII, shown as "PYTHON Connected" 437 self._write_command(self._OP_SET_CLIENT_NAME, b"PYTHON") 438 self.stop() 439 self.set_default_params() 440 except BaseException: 441 # Op 81 means something else to other devices, so it is sent 442 # only once the device has identified itself as a Synth Pal 443 self._close(send_disconnect=self.info.firmware_version is not None) 444 raise 445 446 @staticmethod 447 def serialportlist(ports_to_list="available"): 448 """Return the names of the USB serial ports on this computer. 449 450 Called on the class, without connecting to a device, to find the 451 port name to pass to `SynthPalDevice`. 452 453 Args: 454 ports_to_list: `available` to list only the ports that are 455 not already in use, or `all` to list every USB serial 456 port. Not case sensitive. 457 458 Returns: 459 Sorted list of port names, such as `["COM3", "COM7"]`. 460 461 Raises: 462 PulsePalError: If `ports_to_list` is not `available` or `all`. 463 """ 464 return _common.serialportlist(ports_to_list) 465 466 # ------------------------------------------------------------------ 467 # Settings 468 # ------------------------------------------------------------------ 469 470 def set_default_params(self): 471 """Program the default settings on the device. 472 473 The defaults are a frequency of 100 Hz, and on every output 474 channel a sine wave of 5 V peak to peak around a mean voltage of 475 0 V, resting at 0 V, played for 1 second with no ramps, and a 476 fixed voltage of 5 V for when the waveform is "Fixed Voltage". 477 Both trigger channels are in normal mode, and no output channel is 478 linked to a trigger channel, so a TTL starts nothing until a script 479 links it. They match the settings the device starts with. They are sent at once, 480 also while `SynthPalDevice.auto_sync` is off. 481 """ 482 auto_sync = self._auto_sync 483 self._auto_sync = True 484 try: 485 self.frequency = 100 486 # In this order, each is valid whatever the device holds: a mean of 0 V goes with any 487 # amplitude, 5 V is then a valid amplitude for any waveform, and a sine wave is then valid 488 self._send_levels_in_order((("M", [0] * 4), ("A", [5_000_000] * 4), ("W", ["Sine"] * 4)), 489 force=True) 490 for settings, value in ((self._waveform, "Sine"), (self._peak_to_peak, 5.0), 491 (self._fixed_voltage, 5.0), (self._mean_voltage, 0.0)): 492 settings._store([value] * 4) 493 self.resting_voltage = [0] * 4 494 self.play_duration = [1] * 4 495 self.on_ramp_duration = [0] * 4 496 self.off_ramp_duration = [0] * 4 497 self.trigger_mode = ["Normal"] * 2 498 self._set_trigger_links([False] * 4, [False] * 4) 499 self._unsent = frozenset() # Every setting has been sent 500 finally: 501 self._auto_sync = auto_sync 502 503 @property 504 def frequency(self): 505 """Frequency of all output channels, in Hz. 506 507 1 to 20000 Hz, rounded to 0.01 Hz. It can be changed during 508 playback: playing channels carry on from the same point in their 509 cycle, at the new frequency, and keep the time they have left to 510 play. The frequency played is exact: the device's sample clock is a 511 whole multiple of it (see `SynthPalDevice.sampling_rate`). For 512 `"White Noise"` it sets only the sampling rate. 513 """ 514 return self._frequency 515 516 @frequency.setter 517 def frequency(self, value): 518 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real) \ 519 or not math.isfinite(value): 520 raise PulsePalError( 521 f"frequency must be a number of Hz. Received {value!r}.") 522 centihz = round(value * 100) 523 low = round(self.info.min_frequency * 100) 524 high = round(self.info.max_frequency * 100) 525 if not low <= centihz <= high: 526 raise PulsePalError( 527 f"frequency must be {self.info.min_frequency:g} to " 528 f"{self.info.max_frequency:g} Hz. Received {value!r}." 529 ) 530 if self._auto_sync: 531 self._write_command(self._OP_SET_FREQUENCY, 532 struct.pack("<I", centihz)) 533 confirmed, samples_per_cycle = struct.unpack( 534 "<BI", self._read_raw(5)) 535 if confirmed != 1: 536 raise PulsePalError( 537 f"Synth Pal rejected the frequency {value!r} Hz.") 538 else: 539 samples_per_cycle = _samples_per_cycle(centihz) 540 self._unsent = self._unsent | {"frequency"} # Until sync_to_device() sends it (see auto_sync) 541 self._frequency = centihz / 100 542 self._samples_per_cycle = samples_per_cycle 543 544 @property 545 def auto_sync(self): 546 """Whether assigning a setting programs the device at once. 547 548 `True` (the default): each assignment, such as 549 `S.peak_to_peak[1] = 2`, programs the device at once, as the 550 device's own op for that setting does, also while a trigger 551 channel is in param sync mode. 552 553 `False`: assignments change only this object's copy of the settings, 554 checking each value; `SynthPalDevice.sync_to_device` then sends all 555 of them at once. Use it to store the next trial's settings for a 556 param sync edge (see `SynthPalDevice.trigger_mode`), or to change 557 several settings in one command. Turning it back on does not send 558 changes made meanwhile, so it raises `pulsepal.PulsePalError` while 559 there are any: call `SynthPalDevice.sync_to_device` first to send 560 them, or `SynthPalDevice.sync_from_device` to discard them. A 561 `SynthPalDevice.batch` block does it all for you. `close()` warns of 562 changes never sent. `trigger`, `stop`, `status` and 563 `set_default_params` act at once either way. 564 """ 565 return self._auto_sync 566 567 @auto_sync.setter 568 def auto_sync(self, value): 569 value = to_bool(value, "auto_sync") 570 if value and self._unsent: 571 # Turned on, auto_sync would leave them unsent, and the device playing its old settings 572 raise PulsePalError( 573 f"Changes made with auto_sync off have not been sent to the device: {self._unsent_names()}. Call " 574 "sync_to_device() to send them, or sync_from_device() to discard them, before turning auto_sync on." 575 ) 576 self._auto_sync = value 577 578 def batch(self): 579 """Change several settings, and send them in one command. 580 581 Returns a context manager. In its block, assignments change only 582 this object's copy of the settings (as with 583 `SynthPalDevice.auto_sync` off), and when the block ends, 584 `SynthPalDevice.sync_to_device` sends all of them at once. If the 585 block raises an error, nothing is sent, and the settings go back 586 to what they were. 587 588 ```python 589 with S.batch(): 590 S.frequency = 880 591 S.waveform = ["Triangle"] * 4 592 S.peak_to_peak = [2, 4, 6, 8] 593 ``` 594 595 In param sync mode, the device stores the settings for the next 596 rising edge on the param sync channel (see 597 `SynthPalDevice.trigger_mode`). 598 """ 599 return _common.batch(self) 600 601 def sync_from_device(self): 602 """Read every setting from the device into this object. 603 604 Use it after settings were changed with the device's joystick, for 605 example. The device holds one amplitude per channel: a Fixed 606 Voltage channel's goes to `SynthPalDevice.fixed_voltage`, and any 607 other channel's to `SynthPalDevice.peak_to_peak`. This object keeps 608 the other one, reduced if need be to suit the mean voltage read 609 back. In param sync mode, it reads the settings the device plays 610 now, not a set waiting for the next edge. Changes made with 611 `SynthPalDevice.auto_sync` off and not yet sent are discarded. 612 613 Raises: 614 PulsePalError: If the device does not reply. 615 """ 616 self._write_command(self._OP_GET_ALL_SETTINGS) 617 values = struct.unpack(self._SETTINGS_FORMAT, self._read_raw(struct.calcsize(self._SETTINGS_FORMAT))) 618 centihz = values[0] 619 waveforms = [WAVEFORMS[code] for code in values[1:5]] 620 amplitudes, means = list(values[5:9]), list(values[9:13]) 621 peak_to_peak, fixed_voltage = list(self._peak_to_peak)[1:], list(self._fixed_voltage)[1:] 622 for i, (name, amplitude, mean) in enumerate(zip(waveforms, amplitudes, means)): 623 if name == FIXED_VOLTAGE: 624 fixed_voltage[i] = amplitude / 1e6 625 # This object's own peak to peak voltage must still suit the mean voltage (see _check_levels()) 626 peak_to_peak[i] = min(peak_to_peak[i], (2 * _MAX_VOLTAGE_UV - 2 * abs(mean)) / 1e6) 627 else: 628 peak_to_peak[i] = amplitude / 1e6 629 self._frequency = centihz / 100 630 self._samples_per_cycle = _samples_per_cycle(centihz) 631 self._waveform._store(waveforms) 632 self._peak_to_peak._store(peak_to_peak) 633 self._fixed_voltage._store(fixed_voltage) 634 self._mean_voltage._store([mean / 1e6 for mean in means]) 635 self._resting_voltage._store([rest / 1e6 for rest in values[13:17]]) 636 self._play_duration._store([d / 1e6 for d in values[17:21]]) 637 self._on_ramp_duration._store([d / 1e6 for d in values[21:25]]) 638 self._off_ramp_duration._store([d / 1e6 for d in values[25:29]]) 639 self._link_trigger_channel1._store([bool(link) for link in values[29:33]]) 640 self._link_trigger_channel2._store([bool(link) for link in values[33:37]]) 641 self._trigger_mode._store([TRIGGER_MODES[code] for code in values[37:39]]) 642 self._device_waveform, self._device_amplitude_uv, self._device_mean_uv = waveforms, amplitudes, means 643 self._unsent = frozenset() # Every setting is read 644 645 def export_params(self): 646 """Return every setting, as a dict of plain values and lists. 647 648 Keyed by setting name: `"frequency"`, then the channel settings, 649 each with one value per channel and no unused index 0. It holds 650 only numbers, booleans and names, so it can be saved with `json` 651 and logged with your data, to record exactly what the device 652 played. 653 654 ```python 655 import json 656 with open("trial_settings.json", "w") as f: 657 json.dump(S.export_params(), f) 658 ``` 659 """ 660 params = {"frequency": self._frequency} 661 params.update({name: list(getattr(self, name))[1:] for name in _CHANNEL_SETTINGS}) 662 return params 663 664 def import_params(self, params): 665 """Program the device with settings exported by `SynthPalDevice.export_params`. 666 667 All of them are checked first, and then sent in one command, as 668 at the end of a `SynthPalDevice.batch` block: in param sync mode, 669 the device stores them for the next sync edge. The levels 670 (`waveform`, `peak_to_peak`, `fixed_voltage`, `mean_voltage`) are 671 checked together, so their order in `params` does not matter. 672 Settings missing from `params` keep their values. A name that is 673 not a setting is skipped with a warning, so that settings exported 674 by a newer version of this package, which may have more, can still 675 be imported. 676 677 ```python 678 import json 679 with open("trial_settings.json") as f: 680 S.import_params(json.load(f)) 681 ``` 682 683 Args: 684 params: A dict of setting name to its value: a number for 685 `"frequency"`, and one value per channel (a list, tuple or 686 NumPy array) for the others, as `export_params` returns. 687 688 Raises: 689 PulsePalError: If a value is invalid. Nothing is sent, and the 690 settings keep the values they had. 691 """ 692 names = {"frequency", *_CHANNEL_SETTINGS} 693 unknown = [name for name in params if name not in names] 694 if unknown: 695 warnings.warn(f"import_params(): skipped {', '.join(map(repr, unknown))}, which this version of " 696 "SynthPalDevice does not have.", stacklevel=2) 697 levels = ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage") 698 with self.batch(): 699 for name, values in params.items(): 700 if name in levels: 701 # Each is checked alone here, and the levels together when the block ends (_check_sync()): 702 # an assignment would check each against the others' old values 703 getattr(self, name)._store( 704 self._normalize(name, _common.one_value_per_channel(name, values, 4))) 705 elif name in names: 706 setattr(self, name, values) 707 708 def sync_to_device(self): 709 """Send every setting to the device in one command. 710 711 While a trigger channel is in param sync mode, the device stores 712 the set, replacing any set stored before, and that channel's next 713 rising edge loads it (see `SynthPalDevice.trigger_mode`). Otherwise 714 the device applies it at once. The settings sent are this object's: 715 `frequency`, and for each channel `waveform`, `peak_to_peak` or 716 `fixed_voltage`, `mean_voltage`, `resting_voltage`, 717 `play_duration`, `on_ramp_duration`, `off_ramp_duration`, 718 `link_trigger_channel1` and `link_trigger_channel2`, and 719 `trigger_mode`. They read as sent, including while a set waits for 720 its edge. 721 722 Raises: 723 PulsePalError: If the device rejects the set. 724 """ 725 self._check_sync() 726 self._send_sync() 727 728 def configure(self, channels, *, waveform=None, peak_to_peak=None, fixed_voltage=None, 729 mean_voltage=None, resting_voltage=None, play_duration=None, 730 on_ramp_duration=None, off_ramp_duration=None): 731 """Set several of the output channels' settings at once. 732 733 Settings left out keep their values. The new settings are checked 734 together, so their order does not matter: for example, a channel 735 whose sine wave swings 2 V around a mean voltage of 9 V can go 736 straight to 20 V peak to peak around 0 V. With 737 `SynthPalDevice.auto_sync` on, the class programs the device in an 738 order it accepts at every step. 739 740 ```python 741 S.configure(1, waveform="Sine", peak_to_peak=20, mean_voltage=0) 742 S.configure([2, 3], waveform="Fixed Voltage", fixed_voltage=-2.5, 743 play_duration=0.1) 744 ``` 745 746 Args: 747 channels: Output channel number 1-4, or several as a list, 748 tuple or NumPy array. 749 waveform, peak_to_peak, fixed_voltage, mean_voltage, 750 resting_voltage, play_duration, on_ramp_duration, 751 off_ramp_duration: New values, as for the settings of the 752 same names: one value for every channel given, or a list 753 with one value per channel given, in the same order. 754 755 Raises: 756 PulsePalError: If a value is out of range, or the settings do 757 not go together (nothing is sent), or the device rejects 758 them. 759 """ 760 channels = _common.channel_numbers(channels) 761 if len(set(channels)) != len(channels): 762 raise PulsePalError(f"configure(): channels must be distinct. Received {channels!r}.") 763 requested = {"waveform": waveform, "peak_to_peak": peak_to_peak, "fixed_voltage": fixed_voltage, 764 "mean_voltage": mean_voltage, "resting_voltage": resting_voltage, 765 "play_duration": play_duration, "on_ramp_duration": on_ramp_duration, 766 "off_ramp_duration": off_ramp_duration} 767 new_values = {} 768 for name, value in requested.items(): 769 if value is None: 770 continue 771 values = list(value) if isinstance(value, (list, tuple, np.ndarray)) else [value] 772 if len(values) == 1: 773 values = values * len(channels) 774 if len(values) != len(channels): 775 raise PulsePalError( 776 f"configure(): {name} has {len(values)} values for {len(channels)} channels. Give one " 777 "value, or one value per channel." 778 ) 779 settings = list(getattr(self, name))[1:] 780 for channel, item in zip(channels, values): 781 settings[channel - 1] = item 782 new_values[name] = self._normalize(name, settings) 783 levels = {name: new_values.get(name, list(getattr(self, name))[1:]) 784 for name in ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage")} 785 self._check_levels(levels["peak_to_peak"], levels["mean_voltage"], "configure") 786 if self._auto_sync: 787 self._send_levels(*self._device_levels(**levels)) 788 else: 789 self._unsent = self._unsent | set(new_values) # Until sync_to_device() sends them (see auto_sync) 790 for name in levels: 791 if name in new_values: 792 getattr(self, name)._store(new_values[name]) 793 ops = {"resting_voltage": self._OP_SET_RESTING_VOLTAGE, "play_duration": self._OP_SET_PLAY_DURATION, 794 "on_ramp_duration": self._OP_SET_ON_RAMP_DURATION, 795 "off_ramp_duration": self._OP_SET_OFF_RAMP_DURATION} 796 for name, op in ops.items(): 797 if name in new_values: 798 if self._auto_sync: 799 self._send_setting(name, op, new_values[name]) 800 getattr(self, name)._store(new_values[name]) 801 802 @property 803 def samples_per_cycle(self): 804 """Samples in one cycle of the waveform, at `frequency`. 805 806 The largest multiple of 4 whose sampling rate is at most 807 `DeviceInfo.max_sampling_rate` (100 kHz), so that a sample falls 808 on every edge, peak and trough of every waveform. For example, 809 332 at 300 Hz and 4 at 20 kHz. 810 """ 811 return self._samples_per_cycle 812 813 @property 814 def sampling_rate(self): 815 """The rate at which the device plays samples, in Hz. 816 817 `samples_per_cycle` times `frequency`: 100 kHz or just below it, 818 and at least 50 kHz. For example, 100 kHz at 100 Hz, 99.6 kHz at 819 300 Hz, and 80 kHz at 20 kHz. The device's timer cannot divide 820 every rate exactly, so sample periods differ by up to one tick of 821 its 24 MHz clock (42 ns), arranged so that the frequency is exact. 822 """ 823 return self._samples_per_cycle * self._frequency 824 825 @property 826 def waveform(self): 827 """The waveform of each output channel. 828 829 Indexed by channel number (see "Channel settings" above). Each 830 cycle starts at the trigger: 831 832 - `"Sine"` and `"Triangle"`: start at the mean voltage, rising. 833 - `"Square"`: high for the first half of each cycle, then low. 834 - `"Sawtooth"`: rises from its lowest voltage to its highest, then 835 falls back at the end of the cycle. 836 - `"Fixed Voltage"`: steps to the channel's 837 `SynthPalDevice.fixed_voltage` for the play duration, and 838 returns to the resting voltage. It is not periodic, so the 839 frequency does not change it. 840 - `"White Noise"`: a new random voltage on every sample, uniform 841 over the peak to peak voltage around the mean voltage. It is 842 rendered at the sampling rate the frequency sets 843 (`SynthPalDevice.sampling_rate`), so no claim is made about its 844 spectrum. Each channel has its own random sequence, which carries 845 on from one playback to the next: every playback is new noise. 846 The sequences start from the same point when the device powers 847 up. 848 849 The periodic waveforms and white noise swing 850 `SynthPalDevice.peak_to_peak` around `SynthPalDevice.mean_voltage`. 851 Names are not case sensitive. A change applies to playback in 852 progress. 853 """ 854 return self._waveform 855 856 @waveform.setter 857 def waveform(self, values): 858 self._waveform._assign(values) 859 860 @property 861 def peak_to_peak(self): 862 """The peak to peak voltage of each output channel's waveform, in volts. 863 864 Indexed by channel number (see "Channel settings" above). 0 to 865 20 V: the waveform swings `peak_to_peak / 2` above and below the 866 channel's `SynthPalDevice.mean_voltage`, so it must stay within 867 -10 V to 10 V: `abs(mean_voltage) + peak_to_peak / 2 <= 10`. To 868 raise it beyond what the mean voltage allows, change the mean 869 voltage first, or set both with `SynthPalDevice.configure`. 870 871 A sine wave of 4 V peak to peak swings from -2 V to 2 V around a 872 mean voltage of 0 V: it is 2 sin(2 pi f t). White noise of 4 V peak 873 to peak is uniform from -2 V to 2 V. A `"Fixed Voltage"` channel 874 keeps its peak to peak voltage for when it plays a periodic waveform 875 again. 876 877 Set to the nearest microvolt. A change applies to playback in 878 progress. 879 """ 880 return self._peak_to_peak 881 882 @peak_to_peak.setter 883 def peak_to_peak(self, values): 884 self._peak_to_peak._assign(values) 885 886 @property 887 def fixed_voltage(self): 888 """The voltage each output channel steps to when its waveform is 889 `"Fixed Voltage"`, in volts. 890 891 Indexed by channel number (see "Channel settings" above). -10 to 892 10 V, with any resting voltage and mean voltage. A channel playing 893 another waveform keeps its fixed voltage for when its waveform is 894 `"Fixed Voltage"` again. 895 896 Set to the nearest microvolt. A change applies to playback in 897 progress. 898 """ 899 return self._fixed_voltage 900 901 @fixed_voltage.setter 902 def fixed_voltage(self, values): 903 self._fixed_voltage._assign(values) 904 905 @property 906 def mean_voltage(self): 907 """The mean voltage of each output channel's waveform, in volts. 908 909 Indexed by channel number (see "Channel settings" above). -10 to 910 10 V. A periodic waveform, or white noise, swings around it at full 911 amplitude, and must stay within -10 V to 10 V with the 912 `peak_to_peak` voltage: `abs(mean_voltage) + peak_to_peak / 2 <= 10`. 913 It may differ from the `resting_voltage`, which the channel 914 outputs between playbacks: the ramps move the mean between the two 915 (see "Ramps" above). A `"Fixed Voltage"` ignores it. Set to the 916 nearest microvolt. A change applies to playback in progress. 917 """ 918 return self._mean_voltage 919 920 @mean_voltage.setter 921 def mean_voltage(self, values): 922 self._mean_voltage._assign(values) 923 924 @property 925 def resting_voltage(self): 926 """The resting voltage of each output channel, in volts. 927 928 Indexed by channel number (see "Channel settings" above). -10 to 929 10 V, with any waveform. The channel outputs it while idle, and 930 its ramps start and end there (see "Ramps" above). Set to the 931 nearest microvolt. A change applies to playback in progress. 932 """ 933 return self._resting_voltage 934 935 @resting_voltage.setter 936 def resting_voltage(self, values): 937 self._resting_voltage._assign(values) 938 939 @property 940 def play_duration(self): 941 """How long each output channel plays after a trigger, in seconds. 942 943 Indexed by channel number (see "Channel settings" above). Up to 944 `DeviceInfo.max_play_duration` (3600 s). `0` plays until the 945 channel is stopped. It counts at full amplitude, after the on 946 ramp. The channel stops when the duration has elapsed, part way 947 through a cycle if need be, and returns to its resting voltage, 948 over its off ramp. Durations are counted in samples, rounded to 949 the nearest one, and a nonzero duration lasts at least one sample. 950 A change applies to playback in progress. 951 """ 952 return self._play_duration 953 954 @play_duration.setter 955 def play_duration(self, values): 956 self._play_duration._assign(values) 957 958 @property 959 def on_ramp_duration(self): 960 """How long each output channel fades in after a trigger, in seconds. 961 962 Indexed by channel number (see "Channel settings" above). Up to 963 `DeviceInfo.max_play_duration` (3600 s); `0` (the default) for no 964 ramp. Over it, the peak to peak voltage rises linearly from 0 V to 965 `peak_to_peak`, and the mean from the `resting_voltage` to the 966 `mean_voltage`, before the play duration (see "Ramps" above). 967 Counted in samples, as `play_duration` is. A change applies to 968 playback in progress: a ramp under way keeps the level it has 969 reached, and goes on at the new rate. 970 """ 971 return self._on_ramp_duration 972 973 @on_ramp_duration.setter 974 def on_ramp_duration(self, values): 975 self._on_ramp_duration._assign(values) 976 977 @property 978 def off_ramp_duration(self): 979 """How long each output channel fades out when it stops, in seconds. 980 981 Indexed by channel number (see "Channel settings" above). Up to 982 `DeviceInfo.max_play_duration` (3600 s); `0` (the default) for no 983 ramp. Over it, the peak to peak voltage falls linearly to 0 V, and 984 the mean to the `resting_voltage`, after the play duration or a 985 stop (see "Ramps" above). Counted in samples, as `play_duration` 986 is. A change applies to playback in progress. 987 """ 988 return self._off_ramp_duration 989 990 @off_ramp_duration.setter 991 def off_ramp_duration(self, values): 992 self._off_ramp_duration._assign(values) 993 994 @property 995 def trigger_mode(self): 996 """How each trigger channel acts on the output channels linked to it. 997 998 Indexed by trigger channel number, 1 or 2 (index 0 is unused). 999 These are Pulse Pal's trigger modes: 1000 1001 - `"Normal"`: a rising edge starts the linked channels. Channels 1002 that are playing ignore it. 1003 - `"Toggle"`: a rising edge starts the linked channels, or stops 1004 those that are playing. 1005 - `"Gated"`: a rising edge starts the linked channels, and a 1006 falling edge stops them, unless the other trigger channel is 1007 also gated, linked to them, and still high. With a play 1008 duration of 0, a channel plays for exactly as long as the TTL is 1009 high. 1010 - `"Param Sync"`: a rising edge starts and stops nothing (the 1011 channel's links are ignored). It loads the settings most recently 1012 sent by `SynthPalDevice.sync_to_device` (or at the end of a 1013 `SynthPalDevice.batch` block), if any. This is how the next 1014 trial's settings are sent during the current trial and applied 1015 the instant it starts. 1016 1017 At a param sync edge, the frequency and both trigger modes change 1018 at once: they are shared by all channels. An output channel at its 1019 resting voltage takes its new settings at once. One that is playing 1020 (off ramp included) finishes on the settings it started with, at 1021 the new frequency, and takes the new ones the moment it reaches its 1022 resting voltage, so the next trigger plays them. A trigger mode 1023 sent by `SynthPalDevice.sync_to_device` applies from the next 1024 edge. 1025 1026 Only `SynthPalDevice.sync_to_device` is held back for the edge: 1027 with `SynthPalDevice.auto_sync` on, every assignment programs the 1028 device at once, also in param sync mode. So leaving param sync mode 1029 means assigning `trigger_mode` with `auto_sync` on. When no trigger 1030 channel is left in the mode, the device discards a stored set. 1031 1032 To start channels on the same edge, wire the TTL to the other 1033 trigger channel too: the settings load first, so the channels it 1034 starts play the new settings. Connecting, and `set_default_params`, 1035 take both trigger channels out of param sync mode. 1036 1037 Names are not case sensitive. 1038 """ 1039 return self._trigger_mode 1040 1041 @trigger_mode.setter 1042 def trigger_mode(self, values): 1043 self._trigger_mode._assign(values) 1044 1045 @property 1046 def link_trigger_channel1(self): 1047 """Whether each output channel is triggered by trigger channel 1. 1048 1049 Indexed by channel number (see "Channel settings" above). By 1050 default no output channel is linked to either trigger channel, so 1051 a TTL starts nothing until a script links it. 1052 """ 1053 return self._link_trigger_channel1 1054 1055 @link_trigger_channel1.setter 1056 def link_trigger_channel1(self, values): 1057 self._link_trigger_channel1._assign(values) 1058 1059 @property 1060 def link_trigger_channel2(self): 1061 """Whether each output channel is triggered by trigger channel 2. 1062 1063 Indexed by channel number (see "Channel settings" above). 1064 """ 1065 return self._link_trigger_channel2 1066 1067 @link_trigger_channel2.setter 1068 def link_trigger_channel2(self, values): 1069 self._link_trigger_channel2._assign(values) 1070 1071 def set_screen_saver(self, enabled, timeout=1800): 1072 """Switch the device's screen saver on or off, and set its timeout. 1073 1074 With the screen saver on, the device dims its screen once it has 1075 been left alone for `timeout` seconds: no command from a computer, 1076 no rising edge on a trigger channel, and no joystick click or 1077 push. The next of these brings the screen back. The device keeps 1078 both settings in its EEPROM, shared with Pulse Pal firmware, and 1079 saves them once no channel is playing. A new device starts with 1080 the screen saver on and 1800 s. 1081 1082 Args: 1083 enabled: `True` to switch the screen saver on, `False` to 1084 switch it off. 1085 timeout: Seconds without activity before the screen dims, 1 to 1086 65535. 1087 1088 Raises: 1089 PulsePalError: If a value is out of range. 1090 """ 1091 state = to_bool(enabled, "enabled") 1092 if isinstance(timeout, (bool, np.bool_)) \ 1093 or not isinstance(timeout, numbers.Integral) \ 1094 or not 1 <= timeout <= 65535: 1095 raise PulsePalError( 1096 "timeout must be a whole number of seconds from 1 to 65535. " 1097 f"Received {timeout!r}." 1098 ) 1099 self._write_command(self._OP_SET_SCREEN_SAVER, 1100 struct.pack("<BH", int(state), int(timeout))) 1101 self._read_ack("set_screen_saver()") 1102 1103 # ------------------------------------------------------------------ 1104 # Playback 1105 # ------------------------------------------------------------------ 1106 1107 def trigger(self, channels): 1108 """Trigger output channels in software. 1109 1110 Channels that are idle start from the beginning of their waveform 1111 cycle, together. Channels that are playing ignore it, as they 1112 ignore a trigger channel in normal mode. 1113 1114 ```python 1115 S.trigger(1) 1116 S.trigger([2, 4]) # a list, tuple or NumPy array 1117 ``` 1118 1119 Args: 1120 channels: Output channel number 1-4, or several as a list, 1121 tuple or NumPy array. 1122 """ 1123 self._write_command(self._OP_TRIGGER, bytes([_common.channel_bits(channels)])) 1124 1125 def stop(self, channels=None): 1126 """Stop playback. The stopped channels return to their resting 1127 voltage, over their off ramps. 1128 1129 Args: 1130 channels: Output channel number 1-4, or several as a list, 1131 tuple or NumPy array. `None` stops all channels. 1132 """ 1133 bits = self._ALL_CHANNELS if channels is None else _common.channel_bits(channels) 1134 self._write_command(self._OP_STOP, bytes([bits])) 1135 1136 def status(self): 1137 """Read the device's playback state. 1138 1139 Returns: 1140 A `DeviceStatus`, with the channels that are playing, the 1141 samples per cycle, each channel's output range, and timing 1142 figures. 1143 """ 1144 self._write_command(self._OP_GET_STATUS) 1145 values = struct.unpack( 1146 self._STATUS_FORMAT, 1147 self._read_raw(struct.calcsize(self._STATUS_FORMAT)), 1148 ) 1149 playing_bits = values[0] 1150 range_names = list(OUTPUT_RANGES) 1151 return DeviceStatus( 1152 playing=[ch for ch in range(1, 5) 1153 if playing_bits & (1 << (ch - 1))], 1154 samples_per_cycle=values[1], 1155 output_ranges=[None, *(range_names[i] if i < len(range_names) 1156 else None for i in values[2:6])], 1157 longest_interrupt_us=values[6] / 1000, 1158 late_updates=values[7], 1159 ) 1160 1161 def _playback_checksums(self): 1162 """For testing: what each channel played since it last started. 1163 1164 Returns two lists indexed by channel number, with index 0 unused: 1165 the number of samples played, and the sum of their DAC codes 1166 modulo 2**32. `tests/synthpal_hardware_test.py` compares them with 1167 the samples it expects, to check what the device played. 1168 """ 1169 self._write_command(self._OP_GET_PLAYBACK_CHECKSUMS) 1170 values = struct.unpack("<8I", self._read_raw(32)) 1171 return [None, *values[:4]], [None, *values[4:]] 1172 1173 # ------------------------------------------------------------------ 1174 # Connection 1175 # ------------------------------------------------------------------ 1176 1177 def close(self): 1178 """Close the connection to the device. 1179 1180 The device stops playback on all channels, each over its off ramp, 1181 and shows its own name on its screen again, in place of "PYTHON 1182 Connected". It keeps its settings, so TTL triggers still play the 1183 channels. Safe to call more than once. Called automatically when 1184 leaving a `with` block and when the object is garbage collected, 1185 so playback also stops when the last reference to the object 1186 goes. 1187 1188 """ 1189 self._close(send_disconnect=True) 1190 1191 def _close(self, send_disconnect): 1192 """Close the connection. send_disconnect: tell the device first (op 81). False when the device may not 1193 run this firmware: op 81 means something else to other devices.""" 1194 if getattr(self, "_closed", True): 1195 return 1196 self._closed = True 1197 if send_disconnect and self._unsent: 1198 _common.warn(f"SynthPalDevice closed with changes made with auto_sync off that were never sent to the " 1199 f"device: {self._unsent_names()}.") 1200 self._reply_pending = None # Nothing more is read, so a reply still to come needs no discarding 1201 try: 1202 if send_disconnect and self.port and self.port.is_open: 1203 self._write_command(self._OP_DISCONNECT) 1204 except Exception: 1205 pass # The port may already be gone, e.g. the cable was unplugged 1206 finally: 1207 if self.port and self.port.is_open: 1208 self.port.close() 1209 1210 def __enter__(self): 1211 """Enter a `with` block, returning the connected device.""" 1212 return self 1213 1214 def __exit__(self, exc_type, exc_value, traceback): 1215 """Close the port when leaving a `with` block. 1216 1217 Returns: 1218 `False`, so any exception raised in the block propagates. 1219 """ 1220 self.close() 1221 return False 1222 1223 def __del__(self): 1224 """Close the port when the object is collected.""" 1225 try: 1226 self.close() 1227 except Exception: 1228 # Destructors should not raise; the serial object may already be 1229 # gone during interpreter shutdown. 1230 pass 1231 1232 def __setattr__(self, name, value): 1233 """Refuse a public attribute that is not a setting, such as a misspelled one: it would program nothing (see 1234 _common.refuse_unknown_attribute()).""" 1235 if name[0] != "_": 1236 _common.refuse_unknown_attribute(self, name, _SETTING_NAMES) 1237 super().__setattr__(name, value) 1238 1239 def __repr__(self): 1240 """Describe the device and its settings, e.g. with `print(S)`.""" 1241 port = getattr(getattr(self, "port", None), "port", None) 1242 lines = [ 1243 f"SynthPalDevice on {port} (Synth Pal firmware v{self.info.firmware_version})", 1244 f"frequency: {self._frequency} Hz ({self._samples_per_cycle} samples per cycle)", 1245 f"auto_sync: {self._auto_sync}", 1246 ] 1247 lines += [f"{name}: {list(getattr(self, name))}" for name in _CHANNEL_SETTINGS] 1248 return "\n".join(lines) 1249 1250 # ------------------------------------------------------------------ 1251 # Internals: settings 1252 # ------------------------------------------------------------------ 1253 1254 def _handshake(self): 1255 """Check that the device runs a supported Synth Pal firmware.""" 1256 self._write_command(self._OP_HANDSHAKE) 1257 handshake = _common.read_handshake_reply(self.port) # Skips a reply to an earlier session's command 1258 if handshake is None: 1259 raise PulsePalError( 1260 f"No reply from the device on {self.port.port}. Is it a " 1261 "Pulse Pal 3 running Synth Pal firmware?" 1262 f" {_common.NO_REPLY_HINT}" 1263 ) 1264 reply, version = handshake 1265 if reply != self._HANDSHAKE_REPLY: 1266 if reply in _common.FIRMWARE_BY_HANDSHAKE_REPLY: 1267 raise _common.other_firmware_error(reply, version, self.port.port, "Synth Pal") 1268 raise PulsePalError( 1269 "Incorrect handshake returned. Expected " 1270 f"{self._HANDSHAKE_REPLY}, received {reply}." 1271 ) 1272 if version > self._CURRENT_FIRMWARE_VERSION: 1273 # New firmware only adds commands (see PROTOCOL.md), so this class still works with it 1274 warnings.warn( 1275 f"Synth Pal firmware v{version} is newer than this pulsepal package knows " 1276 f"(v{self._CURRENT_FIRMWARE_VERSION}). It works with it, but update the package to use what is " 1277 "new.", stacklevel=3) 1278 self.info.firmware_version = version 1279 1280 def _read_hardware_info(self): 1281 self._write_command(self._OP_HARDWARE_INFO) 1282 ( 1283 self.info.hardware_version, 1284 self.info.n_channels, 1285 min_centihz, 1286 max_centihz, 1287 self.info.max_sampling_rate, 1288 self.info.timer_clock_hz, 1289 max_duration_us, 1290 ) = struct.unpack( 1291 self._HARDWARE_INFO_FORMAT, 1292 self._read_raw(struct.calcsize(self._HARDWARE_INFO_FORMAT)), 1293 ) 1294 self.info.min_frequency = min_centihz / 100 1295 self.info.max_frequency = max_centihz / 100 1296 self.info.max_play_duration = max_duration_us / 1e6 1297 self._write_command(self._OP_HARDWARE_REVISION) 1298 self.info.circuit_board_revision = self._read_raw(1)[0] 1299 self._write_command(_common.OP_SERIAL_NUMBER) 1300 self.info.serial_number = _common.read_serial_number(self._read_raw) 1301 1302 def _normalize(self, name, values): 1303 """Check new values for all four channels of a setting, and return them as its list holds them.""" 1304 if name == "waveform": 1305 return [to_name(value, WAVEFORMS, "waveform") for value in values] 1306 if name == "peak_to_peak": 1307 return [self._to_volts(value, name, 0, 20, " peak to peak") for value in values] 1308 if name in ("fixed_voltage", "mean_voltage", "resting_voltage"): 1309 return [self._to_volts(value, name, -10, 10) for value in values] 1310 zero_meaning = "0 (play until stopped)" if name == "play_duration" else "0 (no ramp)" 1311 return [self._to_seconds(value, name, zero_meaning) for value in values] 1312 1313 def _check_levels(self, peak_to_peak, mean_voltage, setting): 1314 """Check that each channel's periodic waveform stays within -10 V to 10 V, as the device does. 1315 1316 Checked on every channel, also one playing a fixed voltage, so that a change of waveform always 1317 suits the levels it finds. `setting` is the one being changed, for the advice in the message. 1318 """ 1319 for channel, (pp, mean) in enumerate(zip(peak_to_peak, mean_voltage), start=1): 1320 pp_uv, mean_uv = _microvolts(pp), _microvolts(mean) 1321 if 2 * abs(mean_uv) + pp_uv > 2 * _MAX_VOLTAGE_UV: 1322 advice = { 1323 "peak_to_peak": "Change mean_voltage first, set both with configure(), or choose a " 1324 "smaller peak_to_peak.", 1325 "mean_voltage": "Change peak_to_peak first, set both with configure(), or choose a " 1326 "smaller mean_voltage.", 1327 }.get(setting, "Change peak_to_peak or mean_voltage.") 1328 raise PulsePalError( 1329 f"On channel {channel}, a mean voltage of {mean:g} V and a peak to peak voltage of " 1330 f"{pp:g} V would reach {(abs(mean_uv) + pp_uv / 2) / 1e6:g} V. The waveform must " 1331 f"stay within -10 V to 10 V. {advice}" 1332 ) 1333 1334 def _device_levels(self, waveform, peak_to_peak, fixed_voltage, mean_voltage): 1335 """What the device holds for these settings: waveforms, amplitudes in microvolts (a fixed voltage, or a 1336 periodic waveform's peak to peak voltage) and mean voltages in microvolts.""" 1337 amplitudes = [_microvolts(fixed if name == FIXED_VOLTAGE else pp) 1338 for name, pp, fixed in zip(waveform, peak_to_peak, fixed_voltage)] 1339 return list(waveform), amplitudes, [_microvolts(mean) for mean in mean_voltage] 1340 1341 def _apply_levels(self, name, values): 1342 """Apply new values of waveform, peak_to_peak, fixed_voltage or mean_voltage, for all four channels.""" 1343 values = self._normalize(name, values) 1344 levels = {level: list(getattr(self, level))[1:] 1345 for level in ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage")} 1346 levels[name] = values 1347 self._check_levels(levels["peak_to_peak"], levels["mean_voltage"], name) 1348 if self._auto_sync: 1349 self._send_levels(*self._device_levels(**levels)) 1350 else: 1351 self._unsent = self._unsent | {name} # Until sync_to_device() sends it (see auto_sync) 1352 return values 1353 1354 def _apply_waveform(self, values): 1355 return self._apply_levels("waveform", values) 1356 1357 def _apply_peak_to_peak(self, values): 1358 return self._apply_levels("peak_to_peak", values) 1359 1360 def _apply_fixed_voltage(self, values): 1361 return self._apply_levels("fixed_voltage", values) 1362 1363 def _apply_mean_voltage(self, values): 1364 return self._apply_levels("mean_voltage", values) 1365 1366 def _send_levels(self, waveforms, amplitudes_uv, means_uv): 1367 """Program the device's waveforms, amplitudes and mean voltages, in an order it accepts at every step. 1368 1369 The device checks each op against what it holds for the other two (see _is_valid_level()), and one 1370 order does not suit every change: a sine wave of 20 V peak to peak cannot become a fixed voltage of 1371 -5 V by either op first. So each channel first takes an amplitude that suits both its current and 1372 its new waveform and mean voltage: its current amplitude if it can, else its new one, else 0 V, which 1373 suits any. Then the waveforms, the mean voltages and the new amplitudes follow. If the device 1374 refuses a step, which means this object's record of what it holds is out of date (e.g. a param sync 1375 edge loaded a stored set), the change is made from 0 V, which works from any state. 1376 """ 1377 current = (self._device_waveform, self._device_amplitude_uv, self._device_mean_uv) 1378 if (waveforms, amplitudes_uv, means_uv) == tuple(map(list, current)): 1379 return 1380 first_amplitudes = [] 1381 for w0, a0, m0, w1, a1, m1 in zip(*current, waveforms, amplitudes_uv, means_uv): 1382 for candidate in (a0, a1, 0): 1383 if all(_is_valid_level(w, candidate, m) for w, m in ((w0, m0), (w1, m0), (w1, m1))): 1384 first_amplitudes.append(candidate) 1385 break 1386 steps = (("A", first_amplitudes), ("W", waveforms), ("M", means_uv), ("A", amplitudes_uv)) 1387 try: 1388 self._send_levels_in_order(steps) 1389 except _Rejected: 1390 self._send_levels_in_order((("A", [0] * 4), ("W", waveforms), ("M", means_uv), 1391 ("A", amplitudes_uv)), force=True) 1392 1393 def _send_levels_in_order(self, steps, force=False): 1394 """Send ops 'A' (amplitudes, uV), 'W' (waveform names) and 'M' (mean voltages, uV), in order, and keep 1395 the record of what the device holds. Unless `force`, an op that would not change what the record says 1396 the device holds is skipped.""" 1397 for op, values in steps: 1398 if op == "W": 1399 if values == self._device_waveform and not force: 1400 continue 1401 self._write_command(self._OP_SET_WAVEFORM, bytes(WAVEFORMS.index(name) for name in values)) 1402 self._read_ack("setting waveform") 1403 self._device_waveform = list(values) 1404 elif op == "A": 1405 if values == self._device_amplitude_uv and not force: 1406 continue 1407 self._write_command(self._OP_SET_AMPLITUDE, struct.pack("<4i", *values)) 1408 self._read_ack("setting the amplitude") 1409 self._device_amplitude_uv = list(values) 1410 else: 1411 if values == self._device_mean_uv and not force: 1412 continue 1413 self._write_command(self._OP_SET_MEAN_VOLTAGE, struct.pack("<4i", *values)) 1414 self._read_ack("setting mean_voltage") 1415 self._device_mean_uv = list(values) 1416 1417 def _apply_resting_voltage(self, values): 1418 # Any resting voltage within -10 V to 10 V goes with any waveform 1419 values = self._normalize("resting_voltage", values) 1420 if self._auto_sync: 1421 self._send_setting("resting_voltage", self._OP_SET_RESTING_VOLTAGE, values) 1422 else: 1423 self._unsent = self._unsent | {"resting_voltage"} 1424 return values 1425 1426 def _apply_play_duration(self, values): 1427 return self._apply_duration("play_duration", self._OP_SET_PLAY_DURATION, values) 1428 1429 def _apply_on_ramp_duration(self, values): 1430 return self._apply_duration("on_ramp_duration", self._OP_SET_ON_RAMP_DURATION, values) 1431 1432 def _apply_off_ramp_duration(self, values): 1433 return self._apply_duration("off_ramp_duration", self._OP_SET_OFF_RAMP_DURATION, values) 1434 1435 def _apply_duration(self, name, op, values): 1436 values = self._normalize(name, values) 1437 if self._auto_sync: 1438 self._send_setting(name, op, values) 1439 else: 1440 self._unsent = self._unsent | {name} 1441 return values 1442 1443 def _send_setting(self, name, op, values): 1444 """Send the resting voltages (int32 microvolts) or a duration (uint32 microseconds), for all four 1445 channels.""" 1446 if name == "resting_voltage": 1447 data = struct.pack("<4i", *(_microvolts(v) for v in values)) 1448 else: 1449 data = struct.pack("<4I", *(round(d * 1e6) for d in values)) 1450 self._write_command(op, data) 1451 self._read_ack(f"setting {name}") 1452 1453 def _apply_trigger_mode(self, values): 1454 names = [to_name(value, TRIGGER_MODES, "trigger mode") for value in values] 1455 if self._auto_sync: 1456 self._write_command( 1457 self._OP_SET_TRIGGER_MODE, 1458 bytes(TRIGGER_MODES.index(name) for name in names), 1459 ) 1460 self._read_ack("setting trigger_mode") 1461 else: 1462 self._unsent = self._unsent | {"trigger_mode"} 1463 return names 1464 1465 # One op programs both trigger channels' links, so each list is sent 1466 # with the other's current values 1467 def _apply_trigger_channel1_links(self, values): 1468 links1 = [to_bool(v, "link_trigger_channel1") for v in values] 1469 self._send_trigger_links(links1, self._link_trigger_channel2[1:]) 1470 if not self._auto_sync: 1471 self._unsent = self._unsent | {"link_trigger_channel1"} 1472 return links1 1473 1474 def _apply_trigger_channel2_links(self, values): 1475 links2 = [to_bool(v, "link_trigger_channel2") for v in values] 1476 self._send_trigger_links(self._link_trigger_channel1[1:], links2) 1477 if not self._auto_sync: 1478 self._unsent = self._unsent | {"link_trigger_channel2"} 1479 return links2 1480 1481 def _set_trigger_links(self, links1, links2): 1482 """Program both trigger channels' links with one command.""" 1483 self._send_trigger_links(links1, links2) 1484 self._link_trigger_channel1._store(links1) 1485 self._link_trigger_channel2._store(links2) 1486 1487 def _send_trigger_links(self, links1, links2): 1488 if self._auto_sync: 1489 self._write_command(self._OP_SET_TRIGGER_LINKS, 1490 bytes([*map(int, links1), *map(int, links2)])) 1491 self._read_ack("setting the trigger channel links") 1492 1493 def _snapshot(self): 1494 """All settings, and which are unsent, for batch() to restore if its block fails.""" 1495 settings = {name: list(getattr(self, name))[1:] for name in _CHANNEL_SETTINGS} 1496 return settings, self._frequency, self._samples_per_cycle, self._unsent 1497 1498 def _restore(self, snapshot): 1499 settings, self._frequency, self._samples_per_cycle, self._unsent = snapshot 1500 for name, values in settings.items(): 1501 getattr(self, name)._store(values) 1502 1503 def _unsent_names(self): 1504 """The settings assigned with auto_sync off and not yet sent, for messages, in the class's order.""" 1505 return ", ".join(name for name in _SETTING_NAMES if name in self._unsent) 1506 1507 def _check_sync(self): 1508 """Check the whole set before sync_to_device() sends it.""" 1509 self._check_levels(self._peak_to_peak[1:], self._mean_voltage[1:], "sync") 1510 1511 def _send_sync(self): 1512 """Send every setting with op 85 ('U'). The device checks the whole set, so no order matters.""" 1513 self._unsent = frozenset() # All are sent below 1514 waveforms, amplitudes, means = self._device_levels( 1515 self._waveform[1:], self._peak_to_peak[1:], self._fixed_voltage[1:], self._mean_voltage[1:]) 1516 links = [int(link) for link in (*self._link_trigger_channel1[1:], 1517 *self._link_trigger_channel2[1:])] 1518 payload = struct.pack( 1519 self._SETTINGS_FORMAT, 1520 round(self._frequency * 100), 1521 *(WAVEFORMS.index(name) for name in waveforms), 1522 *amplitudes, *means, *(_microvolts(v) for v in self._resting_voltage[1:]), 1523 *(round(d * 1e6) for d in self._play_duration[1:]), 1524 *(round(d * 1e6) for d in self._on_ramp_duration[1:]), 1525 *(round(d * 1e6) for d in self._off_ramp_duration[1:]), 1526 *links, 1527 *(TRIGGER_MODES.index(name) for name in self._trigger_mode[1:]), 1528 ) 1529 self._write_command(self._OP_SET_ALL_SETTINGS, payload) 1530 self._read_ack("sync_to_device()") 1531 # Applied now, or, in param sync mode, at the next edge. Either way these are the levels the next 1532 # assignments build on; if a stored set has not loaded yet, _send_levels() finds out and recovers. 1533 self._device_waveform, self._device_amplitude_uv, self._device_mean_uv = waveforms, amplitudes, means 1534 1535 @staticmethod 1536 def _to_volts(value, name, low, high, note=""): 1537 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real) \ 1538 or not math.isfinite(value) or not low <= value <= high: 1539 raise PulsePalError( 1540 f"{name} values must be numbers of volts from {low} to " 1541 f"{high}{note}. Received {value!r}." 1542 ) 1543 return float(value) 1544 1545 def _to_seconds(self, value, name, zero_meaning): 1546 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real): 1547 raise PulsePalError( 1548 f"{name} must be in seconds. Received {value!r}." 1549 ) 1550 duration = float(value) 1551 if not math.isfinite(duration) or not \ 1552 0 <= duration <= self.info.max_play_duration: 1553 raise PulsePalError( 1554 f"{name} must be {zero_meaning} or a positive number " 1555 f"of seconds up to {self.info.max_play_duration:g}. " 1556 f"Received {value!r}." 1557 ) 1558 return duration 1559 1560 # ------------------------------------------------------------------ 1561 # Internals: serial port 1562 # ------------------------------------------------------------------ 1563 1564 def _write_command(self, op_code, data=b""): 1565 """Send one command, with its framing byte, in a single write. 1566 1567 A command whose reply was not read in full, because the read timed out or was interrupted (Ctrl-C), leaves 1568 that reply, or the rest of it, to arrive later. Read as the reply to the next command, it would put every 1569 reply after it out of step. So it is discarded first (_discard_lost_reply()), and this command's reply, if it 1570 has one, is pending until _read_raw() has read it, as in PulsePalDevice. 1571 """ 1572 if self._reply_pending is not None: 1573 self._discard_lost_reply() 1574 if op_code not in _OPS_WITHOUT_TRACKED_REPLY: 1575 self._reply_pending = op_code # Before the write: a Ctrl-C can come as soon as it returns 1576 message = bytes([self._OP_MENU_BYTE, op_code]) + data 1577 bytes_written = self.port.write(message) 1578 if bytes_written != len(message): 1579 raise PulsePalError( 1580 f"Wrote {bytes_written} byte(s), expected to write " 1581 f"{len(message)} byte(s)." 1582 ) 1583 1584 def _discard_lost_reply(self): 1585 """Discard what arrives of a reply that was not read in full (see _write_command()), and bring this object 1586 back in step with the device. 1587 1588 The command may have changed settings, unknown to this object: an assignment interrupted after its write, 1589 for one, which the device took and this object did not. So after one of _SETTING_OPS, they are read back 1590 (op 'R'), outside param sync mode, where op 'R' returns the settings playing rather than a set stored for the 1591 next sync edge. 1592 """ 1593 op = self._reply_pending 1594 self._reply_pending = None 1595 n_late = len(_common.read_until_quiet(self.port)) 1596 read_back = op in self._SETTING_OPS and "Param Sync" not in list(self._trigger_mode)[1:] 1597 _common.warn_lost_reply(f"op {chr(op)!r}", n_late, 1598 ", and the settings were read back from the device" if read_back else "") 1599 if read_back: 1600 try: 1601 self.sync_from_device() 1602 except PulsePalError as exc: 1603 raise PulsePalError( 1604 f"Synth Pal did not send its settings, read back after the reply to an earlier command was lost. " 1605 f"{_common.NO_REPLY_HINT}" 1606 ) from exc 1607 1608 def _read_raw(self, n_bytes): 1609 """Read exactly n_bytes from the serial port: the whole reply to the command last sent, which is then no 1610 longer pending (see _write_command()). Only connecting reads a reply in more than one call.""" 1611 message = self.port.read(n_bytes) 1612 if len(message) < n_bytes: 1613 raise PulsePalError( 1614 f"Synth Pal sent {len(message)} of the {n_bytes} byte(s) of its reply within " 1615 f"{self.port.timeout} s. {_common.NO_REPLY_HINT}" 1616 ) 1617 self._reply_pending = None 1618 return message 1619 1620 def _read_ack(self, context): 1621 """Read a one-byte confirmation: 1 if the device executed the 1622 command, 0 if it rejected it.""" 1623 try: 1624 reply = self._read_raw(1)[0] 1625 except PulsePalError as exc: 1626 raise PulsePalError( 1627 f"Synth Pal did not confirm {context}: no reply arrived within {self.port.timeout} s. " 1628 f"{_common.NO_REPLY_HINT}" 1629 ) from exc 1630 if reply != 1: 1631 raise _Rejected( 1632 f"Synth Pal rejected {context}. A value was out of range." 1633 )
A class to control a Synth Pal on a USB serial port.
Creating an instance opens the serial port, checks that the device
runs Synth Pal firmware, reads its properties into
SynthPalDevice.info, shows "PYTHON Connected" on the device's
screen, stops any playback and programs the default settings (see
SynthPalDevice.set_default_params).
from pulsepal import SynthPalDevice
S = SynthPalDevice("COM3")
S.waveform[1] = "Triangle"
S.trigger(1)
S.close()
Replace "COM3" with the device's USB serial port name, which
SynthPalDevice.serialportlist lists. SynthPalDevice is also a
context manager, which closes the port on exit.
Closing the connection stops playback, as it does on Pulse Pal (each channel over its off ramp), and puts the device's own name back on its screen. The device keeps its settings, so TTL triggers still play the channels.
362 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 363 """Open a connection to a Synth Pal. 364 365 Args: 366 port_name: USB serial port of the device, such as `COM3` on 367 Windows or `/dev/ttyACM0` on Linux. 368 serial_number: Instead of `port_name`, the device's USB serial 369 number (`DeviceInfo.serial_number`), which finds its port 370 wherever it is plugged in. 371 timeout: Serial read timeout, in seconds. 372 373 Firmware newer than this module knows is used with a warning: new 374 firmware only adds commands. Update the package to use what is new. 375 376 Raises: 377 PulsePalError: If the device does not reply to the handshake, or 378 runs other firmware. 379 serial.SerialException: If the serial port cannot be opened, 380 e.g. because another program has it open (on Linux and 381 macOS, another session of these classes, which lock it). 382 """ 383 self._closed = True 384 self._auto_sync = True 385 self.info = DeviceInfo() 386 self._frequency = None 387 self._samples_per_cycle = None 388 # What the device holds for each output channel's waveform, amplitude (in microvolts: a periodic 389 # waveform's peak to peak voltage, or a fixed voltage) and mean voltage (in microvolts), as far as this 390 # object has programmed them. _send_levels() changes them in an order the device accepts. 391 self._device_waveform = ["Sine"] * 4 392 self._device_amplitude_uv = [5_000_000] * 4 393 self._device_mean_uv = [0] * 4 394 self._waveform = ChannelSettings( 395 "waveform", ["Sine"] * 4, self, "_apply_waveform") 396 self._peak_to_peak = ChannelSettings( 397 "peak_to_peak", [5.0] * 4, self, "_apply_peak_to_peak") 398 self._fixed_voltage = ChannelSettings( 399 "fixed_voltage", [5.0] * 4, self, "_apply_fixed_voltage") 400 self._mean_voltage = ChannelSettings( 401 "mean_voltage", [0.0] * 4, self, "_apply_mean_voltage") 402 self._resting_voltage = ChannelSettings( 403 "resting_voltage", [0.0] * 4, self, "_apply_resting_voltage") 404 self._play_duration = ChannelSettings( 405 "play_duration", [1.0] * 4, self, "_apply_play_duration") 406 self._on_ramp_duration = ChannelSettings( 407 "on_ramp_duration", [0.0] * 4, self, "_apply_on_ramp_duration") 408 self._off_ramp_duration = ChannelSettings( 409 "off_ramp_duration", [0.0] * 4, self, "_apply_off_ramp_duration") 410 self._trigger_mode = ChannelSettings( 411 "trigger_mode", ["Normal"] * 2, self, "_apply_trigger_mode") 412 self._link_trigger_channel1 = ChannelSettings( 413 "link_trigger_channel1", [False] * 4, self, 414 "_apply_trigger_channel1_links") 415 self._link_trigger_channel2 = ChannelSettings( 416 "link_trigger_channel2", [False] * 4, self, 417 "_apply_trigger_channel2_links") 418 419 port_name = _common.port_name_from(port_name, serial_number, "SynthPalDevice") 420 self.port = serial.Serial( 421 port_name, 422 self._BAUD_RATE, 423 timeout=timeout, 424 rtscts=True, 425 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 426 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 427 # it, such as another of these classes, gets an error instead 428 exclusive=True, 429 ) 430 self._closed = False 431 try: 432 # Discard anything left in the buffer by an earlier session 433 self.port.reset_input_buffer() 434 self._handshake() 435 self._read_hardware_info() 436 # Client name op + "PYTHON" in ASCII, shown as "PYTHON Connected" 437 self._write_command(self._OP_SET_CLIENT_NAME, b"PYTHON") 438 self.stop() 439 self.set_default_params() 440 except BaseException: 441 # Op 81 means something else to other devices, so it is sent 442 # only once the device has identified itself as a Synth Pal 443 self._close(send_disconnect=self.info.firmware_version is not None) 444 raise
Open a connection to a Synth Pal.
Arguments:
- port_name: USB serial port of the device, such as
COM3on Windows or/dev/ttyACM0on Linux. - serial_number: Instead of
port_name, the device's USB serial number (DeviceInfo.serial_number), which finds its port wherever it is plugged in. - timeout: Serial read timeout, in seconds.
Firmware newer than this module knows is used with a warning: new firmware only adds commands. Update the package to use what is new.
Raises:
- PulsePalError: If the device does not reply to the handshake, or runs other firmware.
- serial.SerialException: If the serial port cannot be opened, e.g. because another program has it open (on Linux and macOS, another session of these classes, which lock it).
446 @staticmethod 447 def serialportlist(ports_to_list="available"): 448 """Return the names of the USB serial ports on this computer. 449 450 Called on the class, without connecting to a device, to find the 451 port name to pass to `SynthPalDevice`. 452 453 Args: 454 ports_to_list: `available` to list only the ports that are 455 not already in use, or `all` to list every USB serial 456 port. Not case sensitive. 457 458 Returns: 459 Sorted list of port names, such as `["COM3", "COM7"]`. 460 461 Raises: 462 PulsePalError: If `ports_to_list` is not `available` or `all`. 463 """ 464 return _common.serialportlist(ports_to_list)
Return the names of the USB serial ports on this computer.
Called on the class, without connecting to a device, to find the
port name to pass to SynthPalDevice.
Arguments:
- ports_to_list:
availableto list only the ports that are not already in use, orallto list every USB serial port. Not case sensitive.
Returns:
Sorted list of port names, such as
["COM3", "COM7"].
Raises:
- PulsePalError: If
ports_to_listis notavailableorall.
470 def set_default_params(self): 471 """Program the default settings on the device. 472 473 The defaults are a frequency of 100 Hz, and on every output 474 channel a sine wave of 5 V peak to peak around a mean voltage of 475 0 V, resting at 0 V, played for 1 second with no ramps, and a 476 fixed voltage of 5 V for when the waveform is "Fixed Voltage". 477 Both trigger channels are in normal mode, and no output channel is 478 linked to a trigger channel, so a TTL starts nothing until a script 479 links it. They match the settings the device starts with. They are sent at once, 480 also while `SynthPalDevice.auto_sync` is off. 481 """ 482 auto_sync = self._auto_sync 483 self._auto_sync = True 484 try: 485 self.frequency = 100 486 # In this order, each is valid whatever the device holds: a mean of 0 V goes with any 487 # amplitude, 5 V is then a valid amplitude for any waveform, and a sine wave is then valid 488 self._send_levels_in_order((("M", [0] * 4), ("A", [5_000_000] * 4), ("W", ["Sine"] * 4)), 489 force=True) 490 for settings, value in ((self._waveform, "Sine"), (self._peak_to_peak, 5.0), 491 (self._fixed_voltage, 5.0), (self._mean_voltage, 0.0)): 492 settings._store([value] * 4) 493 self.resting_voltage = [0] * 4 494 self.play_duration = [1] * 4 495 self.on_ramp_duration = [0] * 4 496 self.off_ramp_duration = [0] * 4 497 self.trigger_mode = ["Normal"] * 2 498 self._set_trigger_links([False] * 4, [False] * 4) 499 self._unsent = frozenset() # Every setting has been sent 500 finally: 501 self._auto_sync = auto_sync
Program the default settings on the device.
The defaults are a frequency of 100 Hz, and on every output
channel a sine wave of 5 V peak to peak around a mean voltage of
0 V, resting at 0 V, played for 1 second with no ramps, and a
fixed voltage of 5 V for when the waveform is "Fixed Voltage".
Both trigger channels are in normal mode, and no output channel is
linked to a trigger channel, so a TTL starts nothing until a script
links it. They match the settings the device starts with. They are sent at once,
also while SynthPalDevice.auto_sync is off.
503 @property 504 def frequency(self): 505 """Frequency of all output channels, in Hz. 506 507 1 to 20000 Hz, rounded to 0.01 Hz. It can be changed during 508 playback: playing channels carry on from the same point in their 509 cycle, at the new frequency, and keep the time they have left to 510 play. The frequency played is exact: the device's sample clock is a 511 whole multiple of it (see `SynthPalDevice.sampling_rate`). For 512 `"White Noise"` it sets only the sampling rate. 513 """ 514 return self._frequency
Frequency of all output channels, in Hz.
1 to 20000 Hz, rounded to 0.01 Hz. It can be changed during
playback: playing channels carry on from the same point in their
cycle, at the new frequency, and keep the time they have left to
play. The frequency played is exact: the device's sample clock is a
whole multiple of it (see SynthPalDevice.sampling_rate). For
"White Noise" it sets only the sampling rate.
544 @property 545 def auto_sync(self): 546 """Whether assigning a setting programs the device at once. 547 548 `True` (the default): each assignment, such as 549 `S.peak_to_peak[1] = 2`, programs the device at once, as the 550 device's own op for that setting does, also while a trigger 551 channel is in param sync mode. 552 553 `False`: assignments change only this object's copy of the settings, 554 checking each value; `SynthPalDevice.sync_to_device` then sends all 555 of them at once. Use it to store the next trial's settings for a 556 param sync edge (see `SynthPalDevice.trigger_mode`), or to change 557 several settings in one command. Turning it back on does not send 558 changes made meanwhile, so it raises `pulsepal.PulsePalError` while 559 there are any: call `SynthPalDevice.sync_to_device` first to send 560 them, or `SynthPalDevice.sync_from_device` to discard them. A 561 `SynthPalDevice.batch` block does it all for you. `close()` warns of 562 changes never sent. `trigger`, `stop`, `status` and 563 `set_default_params` act at once either way. 564 """ 565 return self._auto_sync
Whether assigning a setting programs the device at once.
True (the default): each assignment, such as
S.peak_to_peak[1] = 2, programs the device at once, as the
device's own op for that setting does, also while a trigger
channel is in param sync mode.
False: assignments change only this object's copy of the settings,
checking each value; SynthPalDevice.sync_to_device then sends all
of them at once. Use it to store the next trial's settings for a
param sync edge (see SynthPalDevice.trigger_mode), or to change
several settings in one command. Turning it back on does not send
changes made meanwhile, so it raises pulsepal.PulsePalError while
there are any: call SynthPalDevice.sync_to_device first to send
them, or SynthPalDevice.sync_from_device to discard them. A
SynthPalDevice.batch block does it all for you. close() warns of
changes never sent. trigger, stop, status and
set_default_params act at once either way.
578 def batch(self): 579 """Change several settings, and send them in one command. 580 581 Returns a context manager. In its block, assignments change only 582 this object's copy of the settings (as with 583 `SynthPalDevice.auto_sync` off), and when the block ends, 584 `SynthPalDevice.sync_to_device` sends all of them at once. If the 585 block raises an error, nothing is sent, and the settings go back 586 to what they were. 587 588 ```python 589 with S.batch(): 590 S.frequency = 880 591 S.waveform = ["Triangle"] * 4 592 S.peak_to_peak = [2, 4, 6, 8] 593 ``` 594 595 In param sync mode, the device stores the settings for the next 596 rising edge on the param sync channel (see 597 `SynthPalDevice.trigger_mode`). 598 """ 599 return _common.batch(self)
Change several settings, and send them in one command.
Returns a context manager. In its block, assignments change only
this object's copy of the settings (as with
SynthPalDevice.auto_sync off), and when the block ends,
SynthPalDevice.sync_to_device sends all of them at once. If the
block raises an error, nothing is sent, and the settings go back
to what they were.
with S.batch():
S.frequency = 880
S.waveform = ["Triangle"] * 4
S.peak_to_peak = [2, 4, 6, 8]
In param sync mode, the device stores the settings for the next
rising edge on the param sync channel (see
SynthPalDevice.trigger_mode).
601 def sync_from_device(self): 602 """Read every setting from the device into this object. 603 604 Use it after settings were changed with the device's joystick, for 605 example. The device holds one amplitude per channel: a Fixed 606 Voltage channel's goes to `SynthPalDevice.fixed_voltage`, and any 607 other channel's to `SynthPalDevice.peak_to_peak`. This object keeps 608 the other one, reduced if need be to suit the mean voltage read 609 back. In param sync mode, it reads the settings the device plays 610 now, not a set waiting for the next edge. Changes made with 611 `SynthPalDevice.auto_sync` off and not yet sent are discarded. 612 613 Raises: 614 PulsePalError: If the device does not reply. 615 """ 616 self._write_command(self._OP_GET_ALL_SETTINGS) 617 values = struct.unpack(self._SETTINGS_FORMAT, self._read_raw(struct.calcsize(self._SETTINGS_FORMAT))) 618 centihz = values[0] 619 waveforms = [WAVEFORMS[code] for code in values[1:5]] 620 amplitudes, means = list(values[5:9]), list(values[9:13]) 621 peak_to_peak, fixed_voltage = list(self._peak_to_peak)[1:], list(self._fixed_voltage)[1:] 622 for i, (name, amplitude, mean) in enumerate(zip(waveforms, amplitudes, means)): 623 if name == FIXED_VOLTAGE: 624 fixed_voltage[i] = amplitude / 1e6 625 # This object's own peak to peak voltage must still suit the mean voltage (see _check_levels()) 626 peak_to_peak[i] = min(peak_to_peak[i], (2 * _MAX_VOLTAGE_UV - 2 * abs(mean)) / 1e6) 627 else: 628 peak_to_peak[i] = amplitude / 1e6 629 self._frequency = centihz / 100 630 self._samples_per_cycle = _samples_per_cycle(centihz) 631 self._waveform._store(waveforms) 632 self._peak_to_peak._store(peak_to_peak) 633 self._fixed_voltage._store(fixed_voltage) 634 self._mean_voltage._store([mean / 1e6 for mean in means]) 635 self._resting_voltage._store([rest / 1e6 for rest in values[13:17]]) 636 self._play_duration._store([d / 1e6 for d in values[17:21]]) 637 self._on_ramp_duration._store([d / 1e6 for d in values[21:25]]) 638 self._off_ramp_duration._store([d / 1e6 for d in values[25:29]]) 639 self._link_trigger_channel1._store([bool(link) for link in values[29:33]]) 640 self._link_trigger_channel2._store([bool(link) for link in values[33:37]]) 641 self._trigger_mode._store([TRIGGER_MODES[code] for code in values[37:39]]) 642 self._device_waveform, self._device_amplitude_uv, self._device_mean_uv = waveforms, amplitudes, means 643 self._unsent = frozenset() # Every setting is read
Read every setting from the device into this object.
Use it after settings were changed with the device's joystick, for
example. The device holds one amplitude per channel: a Fixed
Voltage channel's goes to SynthPalDevice.fixed_voltage, and any
other channel's to SynthPalDevice.peak_to_peak. This object keeps
the other one, reduced if need be to suit the mean voltage read
back. In param sync mode, it reads the settings the device plays
now, not a set waiting for the next edge. Changes made with
SynthPalDevice.auto_sync off and not yet sent are discarded.
Raises:
- PulsePalError: If the device does not reply.
645 def export_params(self): 646 """Return every setting, as a dict of plain values and lists. 647 648 Keyed by setting name: `"frequency"`, then the channel settings, 649 each with one value per channel and no unused index 0. It holds 650 only numbers, booleans and names, so it can be saved with `json` 651 and logged with your data, to record exactly what the device 652 played. 653 654 ```python 655 import json 656 with open("trial_settings.json", "w") as f: 657 json.dump(S.export_params(), f) 658 ``` 659 """ 660 params = {"frequency": self._frequency} 661 params.update({name: list(getattr(self, name))[1:] for name in _CHANNEL_SETTINGS}) 662 return params
Return every setting, as a dict of plain values and lists.
Keyed by setting name: "frequency", then the channel settings,
each with one value per channel and no unused index 0. It holds
only numbers, booleans and names, so it can be saved with json
and logged with your data, to record exactly what the device
played.
import json
with open("trial_settings.json", "w") as f:
json.dump(S.export_params(), f)
664 def import_params(self, params): 665 """Program the device with settings exported by `SynthPalDevice.export_params`. 666 667 All of them are checked first, and then sent in one command, as 668 at the end of a `SynthPalDevice.batch` block: in param sync mode, 669 the device stores them for the next sync edge. The levels 670 (`waveform`, `peak_to_peak`, `fixed_voltage`, `mean_voltage`) are 671 checked together, so their order in `params` does not matter. 672 Settings missing from `params` keep their values. A name that is 673 not a setting is skipped with a warning, so that settings exported 674 by a newer version of this package, which may have more, can still 675 be imported. 676 677 ```python 678 import json 679 with open("trial_settings.json") as f: 680 S.import_params(json.load(f)) 681 ``` 682 683 Args: 684 params: A dict of setting name to its value: a number for 685 `"frequency"`, and one value per channel (a list, tuple or 686 NumPy array) for the others, as `export_params` returns. 687 688 Raises: 689 PulsePalError: If a value is invalid. Nothing is sent, and the 690 settings keep the values they had. 691 """ 692 names = {"frequency", *_CHANNEL_SETTINGS} 693 unknown = [name for name in params if name not in names] 694 if unknown: 695 warnings.warn(f"import_params(): skipped {', '.join(map(repr, unknown))}, which this version of " 696 "SynthPalDevice does not have.", stacklevel=2) 697 levels = ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage") 698 with self.batch(): 699 for name, values in params.items(): 700 if name in levels: 701 # Each is checked alone here, and the levels together when the block ends (_check_sync()): 702 # an assignment would check each against the others' old values 703 getattr(self, name)._store( 704 self._normalize(name, _common.one_value_per_channel(name, values, 4))) 705 elif name in names: 706 setattr(self, name, values)
Program the device with settings exported by SynthPalDevice.export_params.
All of them are checked first, and then sent in one command, as
at the end of a SynthPalDevice.batch block: in param sync mode,
the device stores them for the next sync edge. The levels
(waveform, peak_to_peak, fixed_voltage, mean_voltage) are
checked together, so their order in params does not matter.
Settings missing from params keep their values. A name that is
not a setting is skipped with a warning, so that settings exported
by a newer version of this package, which may have more, can still
be imported.
import json
with open("trial_settings.json") as f:
S.import_params(json.load(f))
Arguments:
- params: A dict of setting name to its value: a number for
"frequency", and one value per channel (a list, tuple or NumPy array) for the others, asexport_paramsreturns.
Raises:
- PulsePalError: If a value is invalid. Nothing is sent, and the settings keep the values they had.
708 def sync_to_device(self): 709 """Send every setting to the device in one command. 710 711 While a trigger channel is in param sync mode, the device stores 712 the set, replacing any set stored before, and that channel's next 713 rising edge loads it (see `SynthPalDevice.trigger_mode`). Otherwise 714 the device applies it at once. The settings sent are this object's: 715 `frequency`, and for each channel `waveform`, `peak_to_peak` or 716 `fixed_voltage`, `mean_voltage`, `resting_voltage`, 717 `play_duration`, `on_ramp_duration`, `off_ramp_duration`, 718 `link_trigger_channel1` and `link_trigger_channel2`, and 719 `trigger_mode`. They read as sent, including while a set waits for 720 its edge. 721 722 Raises: 723 PulsePalError: If the device rejects the set. 724 """ 725 self._check_sync() 726 self._send_sync()
Send every setting to the device in one command.
While a trigger channel is in param sync mode, the device stores
the set, replacing any set stored before, and that channel's next
rising edge loads it (see SynthPalDevice.trigger_mode). Otherwise
the device applies it at once. The settings sent are this object's:
frequency, and for each channel waveform, peak_to_peak or
fixed_voltage, mean_voltage, resting_voltage,
play_duration, on_ramp_duration, off_ramp_duration,
link_trigger_channel1 and link_trigger_channel2, and
trigger_mode. They read as sent, including while a set waits for
its edge.
Raises:
- PulsePalError: If the device rejects the set.
728 def configure(self, channels, *, waveform=None, peak_to_peak=None, fixed_voltage=None, 729 mean_voltage=None, resting_voltage=None, play_duration=None, 730 on_ramp_duration=None, off_ramp_duration=None): 731 """Set several of the output channels' settings at once. 732 733 Settings left out keep their values. The new settings are checked 734 together, so their order does not matter: for example, a channel 735 whose sine wave swings 2 V around a mean voltage of 9 V can go 736 straight to 20 V peak to peak around 0 V. With 737 `SynthPalDevice.auto_sync` on, the class programs the device in an 738 order it accepts at every step. 739 740 ```python 741 S.configure(1, waveform="Sine", peak_to_peak=20, mean_voltage=0) 742 S.configure([2, 3], waveform="Fixed Voltage", fixed_voltage=-2.5, 743 play_duration=0.1) 744 ``` 745 746 Args: 747 channels: Output channel number 1-4, or several as a list, 748 tuple or NumPy array. 749 waveform, peak_to_peak, fixed_voltage, mean_voltage, 750 resting_voltage, play_duration, on_ramp_duration, 751 off_ramp_duration: New values, as for the settings of the 752 same names: one value for every channel given, or a list 753 with one value per channel given, in the same order. 754 755 Raises: 756 PulsePalError: If a value is out of range, or the settings do 757 not go together (nothing is sent), or the device rejects 758 them. 759 """ 760 channels = _common.channel_numbers(channels) 761 if len(set(channels)) != len(channels): 762 raise PulsePalError(f"configure(): channels must be distinct. Received {channels!r}.") 763 requested = {"waveform": waveform, "peak_to_peak": peak_to_peak, "fixed_voltage": fixed_voltage, 764 "mean_voltage": mean_voltage, "resting_voltage": resting_voltage, 765 "play_duration": play_duration, "on_ramp_duration": on_ramp_duration, 766 "off_ramp_duration": off_ramp_duration} 767 new_values = {} 768 for name, value in requested.items(): 769 if value is None: 770 continue 771 values = list(value) if isinstance(value, (list, tuple, np.ndarray)) else [value] 772 if len(values) == 1: 773 values = values * len(channels) 774 if len(values) != len(channels): 775 raise PulsePalError( 776 f"configure(): {name} has {len(values)} values for {len(channels)} channels. Give one " 777 "value, or one value per channel." 778 ) 779 settings = list(getattr(self, name))[1:] 780 for channel, item in zip(channels, values): 781 settings[channel - 1] = item 782 new_values[name] = self._normalize(name, settings) 783 levels = {name: new_values.get(name, list(getattr(self, name))[1:]) 784 for name in ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage")} 785 self._check_levels(levels["peak_to_peak"], levels["mean_voltage"], "configure") 786 if self._auto_sync: 787 self._send_levels(*self._device_levels(**levels)) 788 else: 789 self._unsent = self._unsent | set(new_values) # Until sync_to_device() sends them (see auto_sync) 790 for name in levels: 791 if name in new_values: 792 getattr(self, name)._store(new_values[name]) 793 ops = {"resting_voltage": self._OP_SET_RESTING_VOLTAGE, "play_duration": self._OP_SET_PLAY_DURATION, 794 "on_ramp_duration": self._OP_SET_ON_RAMP_DURATION, 795 "off_ramp_duration": self._OP_SET_OFF_RAMP_DURATION} 796 for name, op in ops.items(): 797 if name in new_values: 798 if self._auto_sync: 799 self._send_setting(name, op, new_values[name]) 800 getattr(self, name)._store(new_values[name])
Set several of the output channels' settings at once.
Settings left out keep their values. The new settings are checked
together, so their order does not matter: for example, a channel
whose sine wave swings 2 V around a mean voltage of 9 V can go
straight to 20 V peak to peak around 0 V. With
SynthPalDevice.auto_sync on, the class programs the device in an
order it accepts at every step.
S.configure(1, waveform="Sine", peak_to_peak=20, mean_voltage=0)
S.configure([2, 3], waveform="Fixed Voltage", fixed_voltage=-2.5,
play_duration=0.1)
Arguments:
- channels: Output channel number 1-4, or several as a list, tuple or NumPy array.
- waveform, peak_to_peak, fixed_voltage, mean_voltage, resting_voltage, play_duration, on_ramp_duration, off_ramp_duration: New values, as for the settings of the same names: one value for every channel given, or a list with one value per channel given, in the same order.
Raises:
- PulsePalError: If a value is out of range, or the settings do not go together (nothing is sent), or the device rejects them.
802 @property 803 def samples_per_cycle(self): 804 """Samples in one cycle of the waveform, at `frequency`. 805 806 The largest multiple of 4 whose sampling rate is at most 807 `DeviceInfo.max_sampling_rate` (100 kHz), so that a sample falls 808 on every edge, peak and trough of every waveform. For example, 809 332 at 300 Hz and 4 at 20 kHz. 810 """ 811 return self._samples_per_cycle
Samples in one cycle of the waveform, at frequency.
The largest multiple of 4 whose sampling rate is at most
DeviceInfo.max_sampling_rate (100 kHz), so that a sample falls
on every edge, peak and trough of every waveform. For example,
332 at 300 Hz and 4 at 20 kHz.
813 @property 814 def sampling_rate(self): 815 """The rate at which the device plays samples, in Hz. 816 817 `samples_per_cycle` times `frequency`: 100 kHz or just below it, 818 and at least 50 kHz. For example, 100 kHz at 100 Hz, 99.6 kHz at 819 300 Hz, and 80 kHz at 20 kHz. The device's timer cannot divide 820 every rate exactly, so sample periods differ by up to one tick of 821 its 24 MHz clock (42 ns), arranged so that the frequency is exact. 822 """ 823 return self._samples_per_cycle * self._frequency
The rate at which the device plays samples, in Hz.
samples_per_cycle times frequency: 100 kHz or just below it,
and at least 50 kHz. For example, 100 kHz at 100 Hz, 99.6 kHz at
300 Hz, and 80 kHz at 20 kHz. The device's timer cannot divide
every rate exactly, so sample periods differ by up to one tick of
its 24 MHz clock (42 ns), arranged so that the frequency is exact.
825 @property 826 def waveform(self): 827 """The waveform of each output channel. 828 829 Indexed by channel number (see "Channel settings" above). Each 830 cycle starts at the trigger: 831 832 - `"Sine"` and `"Triangle"`: start at the mean voltage, rising. 833 - `"Square"`: high for the first half of each cycle, then low. 834 - `"Sawtooth"`: rises from its lowest voltage to its highest, then 835 falls back at the end of the cycle. 836 - `"Fixed Voltage"`: steps to the channel's 837 `SynthPalDevice.fixed_voltage` for the play duration, and 838 returns to the resting voltage. It is not periodic, so the 839 frequency does not change it. 840 - `"White Noise"`: a new random voltage on every sample, uniform 841 over the peak to peak voltage around the mean voltage. It is 842 rendered at the sampling rate the frequency sets 843 (`SynthPalDevice.sampling_rate`), so no claim is made about its 844 spectrum. Each channel has its own random sequence, which carries 845 on from one playback to the next: every playback is new noise. 846 The sequences start from the same point when the device powers 847 up. 848 849 The periodic waveforms and white noise swing 850 `SynthPalDevice.peak_to_peak` around `SynthPalDevice.mean_voltage`. 851 Names are not case sensitive. A change applies to playback in 852 progress. 853 """ 854 return self._waveform
The waveform of each output channel.
Indexed by channel number (see "Channel settings" above). Each cycle starts at the trigger:
"Sine"and"Triangle": start at the mean voltage, rising."Square": high for the first half of each cycle, then low."Sawtooth": rises from its lowest voltage to its highest, then falls back at the end of the cycle."Fixed Voltage": steps to the channel'sSynthPalDevice.fixed_voltagefor the play duration, and returns to the resting voltage. It is not periodic, so the frequency does not change it."White Noise": a new random voltage on every sample, uniform over the peak to peak voltage around the mean voltage. It is rendered at the sampling rate the frequency sets (SynthPalDevice.sampling_rate), so no claim is made about its spectrum. Each channel has its own random sequence, which carries on from one playback to the next: every playback is new noise. The sequences start from the same point when the device powers up.
The periodic waveforms and white noise swing
SynthPalDevice.peak_to_peak around SynthPalDevice.mean_voltage.
Names are not case sensitive. A change applies to playback in
progress.
860 @property 861 def peak_to_peak(self): 862 """The peak to peak voltage of each output channel's waveform, in volts. 863 864 Indexed by channel number (see "Channel settings" above). 0 to 865 20 V: the waveform swings `peak_to_peak / 2` above and below the 866 channel's `SynthPalDevice.mean_voltage`, so it must stay within 867 -10 V to 10 V: `abs(mean_voltage) + peak_to_peak / 2 <= 10`. To 868 raise it beyond what the mean voltage allows, change the mean 869 voltage first, or set both with `SynthPalDevice.configure`. 870 871 A sine wave of 4 V peak to peak swings from -2 V to 2 V around a 872 mean voltage of 0 V: it is 2 sin(2 pi f t). White noise of 4 V peak 873 to peak is uniform from -2 V to 2 V. A `"Fixed Voltage"` channel 874 keeps its peak to peak voltage for when it plays a periodic waveform 875 again. 876 877 Set to the nearest microvolt. A change applies to playback in 878 progress. 879 """ 880 return self._peak_to_peak
The peak to peak voltage of each output channel's waveform, in volts.
Indexed by channel number (see "Channel settings" above). 0 to
20 V: the waveform swings peak_to_peak / 2 above and below the
channel's SynthPalDevice.mean_voltage, so it must stay within
-10 V to 10 V: abs(mean_voltage) + peak_to_peak / 2 <= 10. To
raise it beyond what the mean voltage allows, change the mean
voltage first, or set both with SynthPalDevice.configure.
A sine wave of 4 V peak to peak swings from -2 V to 2 V around a
mean voltage of 0 V: it is 2 sin(2 pi f t). White noise of 4 V peak
to peak is uniform from -2 V to 2 V. A "Fixed Voltage" channel
keeps its peak to peak voltage for when it plays a periodic waveform
again.
Set to the nearest microvolt. A change applies to playback in progress.
886 @property 887 def fixed_voltage(self): 888 """The voltage each output channel steps to when its waveform is 889 `"Fixed Voltage"`, in volts. 890 891 Indexed by channel number (see "Channel settings" above). -10 to 892 10 V, with any resting voltage and mean voltage. A channel playing 893 another waveform keeps its fixed voltage for when its waveform is 894 `"Fixed Voltage"` again. 895 896 Set to the nearest microvolt. A change applies to playback in 897 progress. 898 """ 899 return self._fixed_voltage
The voltage each output channel steps to when its waveform is
"Fixed Voltage", in volts.
Indexed by channel number (see "Channel settings" above). -10 to
10 V, with any resting voltage and mean voltage. A channel playing
another waveform keeps its fixed voltage for when its waveform is
"Fixed Voltage" again.
Set to the nearest microvolt. A change applies to playback in progress.
905 @property 906 def mean_voltage(self): 907 """The mean voltage of each output channel's waveform, in volts. 908 909 Indexed by channel number (see "Channel settings" above). -10 to 910 10 V. A periodic waveform, or white noise, swings around it at full 911 amplitude, and must stay within -10 V to 10 V with the 912 `peak_to_peak` voltage: `abs(mean_voltage) + peak_to_peak / 2 <= 10`. 913 It may differ from the `resting_voltage`, which the channel 914 outputs between playbacks: the ramps move the mean between the two 915 (see "Ramps" above). A `"Fixed Voltage"` ignores it. Set to the 916 nearest microvolt. A change applies to playback in progress. 917 """ 918 return self._mean_voltage
The mean voltage of each output channel's waveform, in volts.
Indexed by channel number (see "Channel settings" above). -10 to
10 V. A periodic waveform, or white noise, swings around it at full
amplitude, and must stay within -10 V to 10 V with the
peak_to_peak voltage: abs(mean_voltage) + peak_to_peak / 2 <= 10.
It may differ from the resting_voltage, which the channel
outputs between playbacks: the ramps move the mean between the two
(see "Ramps" above). A "Fixed Voltage" ignores it. Set to the
nearest microvolt. A change applies to playback in progress.
924 @property 925 def resting_voltage(self): 926 """The resting voltage of each output channel, in volts. 927 928 Indexed by channel number (see "Channel settings" above). -10 to 929 10 V, with any waveform. The channel outputs it while idle, and 930 its ramps start and end there (see "Ramps" above). Set to the 931 nearest microvolt. A change applies to playback in progress. 932 """ 933 return self._resting_voltage
The resting voltage of each output channel, in volts.
Indexed by channel number (see "Channel settings" above). -10 to 10 V, with any waveform. The channel outputs it while idle, and its ramps start and end there (see "Ramps" above). Set to the nearest microvolt. A change applies to playback in progress.
939 @property 940 def play_duration(self): 941 """How long each output channel plays after a trigger, in seconds. 942 943 Indexed by channel number (see "Channel settings" above). Up to 944 `DeviceInfo.max_play_duration` (3600 s). `0` plays until the 945 channel is stopped. It counts at full amplitude, after the on 946 ramp. The channel stops when the duration has elapsed, part way 947 through a cycle if need be, and returns to its resting voltage, 948 over its off ramp. Durations are counted in samples, rounded to 949 the nearest one, and a nonzero duration lasts at least one sample. 950 A change applies to playback in progress. 951 """ 952 return self._play_duration
How long each output channel plays after a trigger, in seconds.
Indexed by channel number (see "Channel settings" above). Up to
DeviceInfo.max_play_duration (3600 s). 0 plays until the
channel is stopped. It counts at full amplitude, after the on
ramp. The channel stops when the duration has elapsed, part way
through a cycle if need be, and returns to its resting voltage,
over its off ramp. Durations are counted in samples, rounded to
the nearest one, and a nonzero duration lasts at least one sample.
A change applies to playback in progress.
958 @property 959 def on_ramp_duration(self): 960 """How long each output channel fades in after a trigger, in seconds. 961 962 Indexed by channel number (see "Channel settings" above). Up to 963 `DeviceInfo.max_play_duration` (3600 s); `0` (the default) for no 964 ramp. Over it, the peak to peak voltage rises linearly from 0 V to 965 `peak_to_peak`, and the mean from the `resting_voltage` to the 966 `mean_voltage`, before the play duration (see "Ramps" above). 967 Counted in samples, as `play_duration` is. A change applies to 968 playback in progress: a ramp under way keeps the level it has 969 reached, and goes on at the new rate. 970 """ 971 return self._on_ramp_duration
How long each output channel fades in after a trigger, in seconds.
Indexed by channel number (see "Channel settings" above). Up to
DeviceInfo.max_play_duration (3600 s); 0 (the default) for no
ramp. Over it, the peak to peak voltage rises linearly from 0 V to
peak_to_peak, and the mean from the resting_voltage to the
mean_voltage, before the play duration (see "Ramps" above).
Counted in samples, as play_duration is. A change applies to
playback in progress: a ramp under way keeps the level it has
reached, and goes on at the new rate.
977 @property 978 def off_ramp_duration(self): 979 """How long each output channel fades out when it stops, in seconds. 980 981 Indexed by channel number (see "Channel settings" above). Up to 982 `DeviceInfo.max_play_duration` (3600 s); `0` (the default) for no 983 ramp. Over it, the peak to peak voltage falls linearly to 0 V, and 984 the mean to the `resting_voltage`, after the play duration or a 985 stop (see "Ramps" above). Counted in samples, as `play_duration` 986 is. A change applies to playback in progress. 987 """ 988 return self._off_ramp_duration
How long each output channel fades out when it stops, in seconds.
Indexed by channel number (see "Channel settings" above). Up to
DeviceInfo.max_play_duration (3600 s); 0 (the default) for no
ramp. Over it, the peak to peak voltage falls linearly to 0 V, and
the mean to the resting_voltage, after the play duration or a
stop (see "Ramps" above). Counted in samples, as play_duration
is. A change applies to playback in progress.
994 @property 995 def trigger_mode(self): 996 """How each trigger channel acts on the output channels linked to it. 997 998 Indexed by trigger channel number, 1 or 2 (index 0 is unused). 999 These are Pulse Pal's trigger modes: 1000 1001 - `"Normal"`: a rising edge starts the linked channels. Channels 1002 that are playing ignore it. 1003 - `"Toggle"`: a rising edge starts the linked channels, or stops 1004 those that are playing. 1005 - `"Gated"`: a rising edge starts the linked channels, and a 1006 falling edge stops them, unless the other trigger channel is 1007 also gated, linked to them, and still high. With a play 1008 duration of 0, a channel plays for exactly as long as the TTL is 1009 high. 1010 - `"Param Sync"`: a rising edge starts and stops nothing (the 1011 channel's links are ignored). It loads the settings most recently 1012 sent by `SynthPalDevice.sync_to_device` (or at the end of a 1013 `SynthPalDevice.batch` block), if any. This is how the next 1014 trial's settings are sent during the current trial and applied 1015 the instant it starts. 1016 1017 At a param sync edge, the frequency and both trigger modes change 1018 at once: they are shared by all channels. An output channel at its 1019 resting voltage takes its new settings at once. One that is playing 1020 (off ramp included) finishes on the settings it started with, at 1021 the new frequency, and takes the new ones the moment it reaches its 1022 resting voltage, so the next trigger plays them. A trigger mode 1023 sent by `SynthPalDevice.sync_to_device` applies from the next 1024 edge. 1025 1026 Only `SynthPalDevice.sync_to_device` is held back for the edge: 1027 with `SynthPalDevice.auto_sync` on, every assignment programs the 1028 device at once, also in param sync mode. So leaving param sync mode 1029 means assigning `trigger_mode` with `auto_sync` on. When no trigger 1030 channel is left in the mode, the device discards a stored set. 1031 1032 To start channels on the same edge, wire the TTL to the other 1033 trigger channel too: the settings load first, so the channels it 1034 starts play the new settings. Connecting, and `set_default_params`, 1035 take both trigger channels out of param sync mode. 1036 1037 Names are not case sensitive. 1038 """ 1039 return self._trigger_mode
How each trigger channel acts on the output channels linked to it.
Indexed by trigger channel number, 1 or 2 (index 0 is unused). These are Pulse Pal's trigger modes:
"Normal": a rising edge starts the linked channels. Channels that are playing ignore it."Toggle": a rising edge starts the linked channels, or stops those that are playing."Gated": a rising edge starts the linked channels, and a falling edge stops them, unless the other trigger channel is also gated, linked to them, and still high. With a play duration of 0, a channel plays for exactly as long as the TTL is high."Param Sync": a rising edge starts and stops nothing (the channel's links are ignored). It loads the settings most recently sent bySynthPalDevice.sync_to_device(or at the end of aSynthPalDevice.batchblock), if any. This is how the next trial's settings are sent during the current trial and applied the instant it starts.
At a param sync edge, the frequency and both trigger modes change
at once: they are shared by all channels. An output channel at its
resting voltage takes its new settings at once. One that is playing
(off ramp included) finishes on the settings it started with, at
the new frequency, and takes the new ones the moment it reaches its
resting voltage, so the next trigger plays them. A trigger mode
sent by SynthPalDevice.sync_to_device applies from the next
edge.
Only SynthPalDevice.sync_to_device is held back for the edge:
with SynthPalDevice.auto_sync on, every assignment programs the
device at once, also in param sync mode. So leaving param sync mode
means assigning trigger_mode with auto_sync on. When no trigger
channel is left in the mode, the device discards a stored set.
To start channels on the same edge, wire the TTL to the other
trigger channel too: the settings load first, so the channels it
starts play the new settings. Connecting, and set_default_params,
take both trigger channels out of param sync mode.
Names are not case sensitive.
1045 @property 1046 def link_trigger_channel1(self): 1047 """Whether each output channel is triggered by trigger channel 1. 1048 1049 Indexed by channel number (see "Channel settings" above). By 1050 default no output channel is linked to either trigger channel, so 1051 a TTL starts nothing until a script links it. 1052 """ 1053 return self._link_trigger_channel1
Whether each output channel is triggered by trigger channel 1.
Indexed by channel number (see "Channel settings" above). By default no output channel is linked to either trigger channel, so a TTL starts nothing until a script links it.
1059 @property 1060 def link_trigger_channel2(self): 1061 """Whether each output channel is triggered by trigger channel 2. 1062 1063 Indexed by channel number (see "Channel settings" above). 1064 """ 1065 return self._link_trigger_channel2
Whether each output channel is triggered by trigger channel 2.
Indexed by channel number (see "Channel settings" above).
1071 def set_screen_saver(self, enabled, timeout=1800): 1072 """Switch the device's screen saver on or off, and set its timeout. 1073 1074 With the screen saver on, the device dims its screen once it has 1075 been left alone for `timeout` seconds: no command from a computer, 1076 no rising edge on a trigger channel, and no joystick click or 1077 push. The next of these brings the screen back. The device keeps 1078 both settings in its EEPROM, shared with Pulse Pal firmware, and 1079 saves them once no channel is playing. A new device starts with 1080 the screen saver on and 1800 s. 1081 1082 Args: 1083 enabled: `True` to switch the screen saver on, `False` to 1084 switch it off. 1085 timeout: Seconds without activity before the screen dims, 1 to 1086 65535. 1087 1088 Raises: 1089 PulsePalError: If a value is out of range. 1090 """ 1091 state = to_bool(enabled, "enabled") 1092 if isinstance(timeout, (bool, np.bool_)) \ 1093 or not isinstance(timeout, numbers.Integral) \ 1094 or not 1 <= timeout <= 65535: 1095 raise PulsePalError( 1096 "timeout must be a whole number of seconds from 1 to 65535. " 1097 f"Received {timeout!r}." 1098 ) 1099 self._write_command(self._OP_SET_SCREEN_SAVER, 1100 struct.pack("<BH", int(state), int(timeout))) 1101 self._read_ack("set_screen_saver()")
Switch the device's screen saver on or off, and set its timeout.
With the screen saver on, the device dims its screen once it has
been left alone for timeout seconds: no command from a computer,
no rising edge on a trigger channel, and no joystick click or
push. The next of these brings the screen back. The device keeps
both settings in its EEPROM, shared with Pulse Pal firmware, and
saves them once no channel is playing. A new device starts with
the screen saver on and 1800 s.
Arguments:
- enabled:
Trueto switch the screen saver on,Falseto switch it off. - timeout: Seconds without activity before the screen dims, 1 to 65535.
Raises:
- PulsePalError: If a value is out of range.
1107 def trigger(self, channels): 1108 """Trigger output channels in software. 1109 1110 Channels that are idle start from the beginning of their waveform 1111 cycle, together. Channels that are playing ignore it, as they 1112 ignore a trigger channel in normal mode. 1113 1114 ```python 1115 S.trigger(1) 1116 S.trigger([2, 4]) # a list, tuple or NumPy array 1117 ``` 1118 1119 Args: 1120 channels: Output channel number 1-4, or several as a list, 1121 tuple or NumPy array. 1122 """ 1123 self._write_command(self._OP_TRIGGER, bytes([_common.channel_bits(channels)]))
Trigger output channels in software.
Channels that are idle start from the beginning of their waveform cycle, together. Channels that are playing ignore it, as they ignore a trigger channel in normal mode.
S.trigger(1)
S.trigger([2, 4]) # a list, tuple or NumPy array
Arguments:
- channels: Output channel number 1-4, or several as a list, tuple or NumPy array.
1125 def stop(self, channels=None): 1126 """Stop playback. The stopped channels return to their resting 1127 voltage, over their off ramps. 1128 1129 Args: 1130 channels: Output channel number 1-4, or several as a list, 1131 tuple or NumPy array. `None` stops all channels. 1132 """ 1133 bits = self._ALL_CHANNELS if channels is None else _common.channel_bits(channels) 1134 self._write_command(self._OP_STOP, bytes([bits]))
Stop playback. The stopped channels return to their resting voltage, over their off ramps.
Arguments:
- channels: Output channel number 1-4, or several as a list,
tuple or NumPy array.
Nonestops all channels.
1136 def status(self): 1137 """Read the device's playback state. 1138 1139 Returns: 1140 A `DeviceStatus`, with the channels that are playing, the 1141 samples per cycle, each channel's output range, and timing 1142 figures. 1143 """ 1144 self._write_command(self._OP_GET_STATUS) 1145 values = struct.unpack( 1146 self._STATUS_FORMAT, 1147 self._read_raw(struct.calcsize(self._STATUS_FORMAT)), 1148 ) 1149 playing_bits = values[0] 1150 range_names = list(OUTPUT_RANGES) 1151 return DeviceStatus( 1152 playing=[ch for ch in range(1, 5) 1153 if playing_bits & (1 << (ch - 1))], 1154 samples_per_cycle=values[1], 1155 output_ranges=[None, *(range_names[i] if i < len(range_names) 1156 else None for i in values[2:6])], 1157 longest_interrupt_us=values[6] / 1000, 1158 late_updates=values[7], 1159 )
Read the device's playback state.
Returns:
A
DeviceStatus, with the channels that are playing, the samples per cycle, each channel's output range, and timing figures.
1177 def close(self): 1178 """Close the connection to the device. 1179 1180 The device stops playback on all channels, each over its off ramp, 1181 and shows its own name on its screen again, in place of "PYTHON 1182 Connected". It keeps its settings, so TTL triggers still play the 1183 channels. Safe to call more than once. Called automatically when 1184 leaving a `with` block and when the object is garbage collected, 1185 so playback also stops when the last reference to the object 1186 goes. 1187 1188 """ 1189 self._close(send_disconnect=True)
Close the connection to the device.
The device stops playback on all channels, each over its off ramp,
and shows its own name on its screen again, in place of "PYTHON
Connected". It keeps its settings, so TTL triggers still play the
channels. Safe to call more than once. Called automatically when
leaving a with block and when the object is garbage collected,
so playback also stops when the last reference to the object
goes.
189@dataclass 190class DeviceInfo: 191 """Properties of the connected Synth Pal. 192 193 Populated when `SynthPalDevice` connects, and available as 194 `SynthPalDevice.info`. 195 """ 196 197 firmware_version: int = None 198 """Synth Pal firmware version running on the device.""" 199 200 hardware_version: int = None 201 """Pulse Pal hardware version, e.g. `3`.""" 202 203 circuit_board_revision: int = None 204 """Revision of the device's circuit board, 0-31. 205 206 The board encodes its revision on five pins, which the firmware reads 207 at startup. A board that encodes none reports `0`. To record exactly 208 which hardware ran an experiment, copy `hardware_version` and 209 `circuit_board_revision` to your data. 210 """ 211 212 serial_number: str = None 213 """USB serial number of the device, e.g. `"20345420"`: the Teensy's, 214 unique to each Pulse Pal 3, and the same whichever firmware it runs. 215 216 Connect by it, wherever the device is plugged in, with 217 `SynthPalDevice(serial_number="20345420")`. To record exactly which unit ran an 218 experiment, copy it to your data. 219 """ 220 221 n_channels: int = None 222 """Number of output channels.""" 223 224 min_frequency: float = None 225 """Lowest frequency, in Hz.""" 226 227 max_frequency: float = None 228 """Highest frequency, in Hz.""" 229 230 max_sampling_rate: int = None 231 """Highest sampling rate, in Hz. See `SynthPalDevice.sampling_rate`.""" 232 233 timer_clock_hz: int = None 234 """Clock that the sample clock counts, in Hz.""" 235 236 max_play_duration: float = None 237 """Longest play duration, in seconds. See 238 `SynthPalDevice.play_duration`.""" 239 240 waveforms: tuple = WAVEFORMS 241 """Names of the waveforms, accepted by `SynthPalDevice.waveform`.""" 242 243 trigger_modes: tuple = TRIGGER_MODES 244 """Names of the trigger modes, accepted by 245 `SynthPalDevice.trigger_mode`."""
Properties of the connected Synth Pal.
Populated when SynthPalDevice connects, and available as
SynthPalDevice.info.
Revision of the device's circuit board, 0-31.
The board encodes its revision on five pins, which the firmware reads
at startup. A board that encodes none reports 0. To record exactly
which hardware ran an experiment, copy hardware_version and
circuit_board_revision to your data.
USB serial number of the device, e.g. "20345420": the Teensy's,
unique to each Pulse Pal 3, and the same whichever firmware it runs.
Connect by it, wherever the device is plugged in, with
SynthPalDevice(serial_number="20345420"). To record exactly which unit ran an
experiment, copy it to your data.
Longest play duration, in seconds. See
SynthPalDevice.play_duration.
Names of the waveforms, accepted by SynthPalDevice.waveform.
Names of the trigger modes, accepted by
SynthPalDevice.trigger_mode.
248@dataclass 249class DeviceStatus: 250 """A snapshot of the device's playback state, from 251 `SynthPalDevice.status`.""" 252 253 playing: list 254 """Numbers of the output channels that are playing, e.g. `[1, 3]`. 255 256 A channel in its off ramp counts as playing until it reaches its 257 resting voltage.""" 258 259 samples_per_cycle: int 260 """Samples in one cycle of the waveform. See 261 `SynthPalDevice.samples_per_cycle`.""" 262 263 output_ranges: list 264 """The output range the device chose for each channel, indexed by 265 channel number, with index 0 unused, e.g. `"-5V:5V"`. 266 267 Each channel uses the range with the finest voltage steps that holds 268 its whole waveform and its resting voltage: `"0V:5V"` (76 uV steps), 269 then `"0V:10V"` or `"-5V:5V"` (153 uV), then `"-10V:10V"` (305 uV). 270 """ 271 272 longest_interrupt_us: float 273 """Longest run of the device's sample clock interrupt since the 274 previous call to `SynthPalDevice.status`, in microseconds. 275 276 It must stay below the sample period, `1e6 / sampling_rate`. 277 """ 278 279 late_updates: int 280 """Output updates since the previous call to `SynthPalDevice.status` 281 that may have come later than their fixed time after a sample clock 282 tick. It is normally 0; a change of frequency during playback can 283 make one update late. See "Timing" in the 284 [Synth Pal protocol](https://github.com/sanworks/PulsePal/blob/develop/Firmware/SynthPal/PROTOCOL.md#timing). 285 """
A snapshot of the device's playback state, from
SynthPalDevice.status.
Numbers of the output channels that are playing, e.g. [1, 3].
A channel in its off ramp counts as playing until it reaches its resting voltage.
The output range the device chose for each channel, indexed by
channel number, with index 0 unused, e.g. "-5V:5V".
Each channel uses the range with the finest voltage steps that holds
its whole waveform and its resting voltage: "0V:5V" (76 uV steps),
then "0V:10V" or "-5V:5V" (153 uV), then "-10V:10V" (305 uV).
Longest run of the device's sample clock interrupt since the
previous call to SynthPalDevice.status, in microseconds.
It must stay below the sample period, 1e6 / sampling_rate.
Output updates since the previous call to SynthPalDevice.status
that may have come later than their fixed time after a sample clock
tick. It is normally 0; a change of frequency during playback can
make one update late. See "Timing" in the
Synth Pal protocol.
Names of the waveforms, in order of their code on the device.
Names of the trigger modes, in order of their code on the device. These are Pulse Pal's trigger modes, with the same codes.