pulsepal.wave_pal
Python interface for Wave Pal, a waveform player for Pulse Pal 3.
Wave Pal is alternative firmware for Pulse Pal 3 hardware. It stores four sampled waveforms on the device's microSD card, and each output channel plays one of them when it is triggered: by a TTL pulse on a trigger channel, from software, or from the thumb joystick. Waveforms can be up to a million samples long, and are played at up to 100 kHz.
Everything is accessed through WavePalDevice. Import it, connect to
the device's serial port, load waveforms, and trigger, e.g.
import numpy as np
from pulsepal import WavePalDevice
with WavePalDevice("COM3") as W:
W.sampling_rate = 50000 # Hz, all channels
t = np.arange(50000) / 50000 # 1 second
W.load_waveform(1, 5 * np.sin(2 * np.pi * 10 * t), [1, 3]) # waveform 1, played by channels 1 and 3
W.trigger([1, 3])
The device needs Wave Pal firmware, which is in /Firmware/WavePal in the Pulse Pal repository. Its USB protocol is documented in PROTOCOL.md.
Channel settings
Settings that apply to one output channel, such as
WavePalDevice.loop_mode, 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.
W.loop_mode[2] = True # channel 2 only
W.loop_duration[1:5] = [1, 2, 3, 4]
W.loop_mode = [False] * 4 # all four channels
WavePalDevice.trigger_mode is indexed the same way by trigger channel
number, 1 or 2, as in Pulse Pal.
Waveforms
The device stores four waveforms, numbered 1-4. Each output channel plays
one of them, its WavePalDevice.current_waveform, or none (0, the
default). Several channels can play the same waveform, which is loaded
only once.
W.load_waveform(1, sine, [1, 3]) # waveform 1, played by channels 1 and 3
W.load_waveform(2, ramp) # stored: no channel plays it yet
W.current_waveform[1] = 2 # channel 1 plays waveform 2 from its next start
A channel that is playing finishes its waveform: a new current waveform plays from the channel's next start. Loading a waveform stops the channels playing it, outside param sync mode.
Param sync
"Param Sync", Pulse Pal's param sync trigger mode, lets the waveforms
of the next trial be loaded during the current one, and take effect the
instant it starts. While a trigger channel is in param sync mode,
WavePalDevice.load_waveform loads the waveform into a second memory
block that the device keeps for each waveform: channels go on playing
the waveform it replaces, and triggers still start it. The next rising
edge on that trigger channel makes it current. A new
WavePalDevice.current_waveform waits for the edge in the same way.
W.trigger_mode[2] = "Param Sync" # sent at once
W.load_waveform(1, next_trial_wave) # stored: waveform 1 plays on, unchanged
# The next rising edge on trigger channel 2 makes it current
Units
Voltages are in volts, within WavePalDevice.output_range. Times are in
seconds, and the sampling rate is in Hz.
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 Wave Pal, a waveform player for Pulse Pal 3. 3 4Wave Pal is alternative firmware for Pulse Pal 3 hardware. It stores four 5sampled waveforms on the device's microSD card, and each output channel 6plays one of them when it is triggered: by a TTL pulse on a trigger 7channel, from software, or from the thumb joystick. Waveforms can be up 8to a million samples long, and are played at up to 100 kHz. 9 10Everything is accessed through `WavePalDevice`. Import it, connect to 11the device's serial port, load waveforms, and trigger, e.g. 12 13```python 14import numpy as np 15from pulsepal import WavePalDevice 16 17with WavePalDevice("COM3") as W: 18 W.sampling_rate = 50000 # Hz, all channels 19 t = np.arange(50000) / 50000 # 1 second 20 W.load_waveform(1, 5 * np.sin(2 * np.pi * 10 * t), [1, 3]) # waveform 1, played by channels 1 and 3 21 W.trigger([1, 3]) 22``` 23 24The device needs Wave Pal firmware, which is in 25[/Firmware/WavePal](https://github.com/sanworks/PulsePal/tree/develop/Firmware/WavePal) in the 26Pulse Pal repository. Its USB protocol is documented in 27[PROTOCOL.md](https://github.com/sanworks/PulsePal/blob/develop/Firmware/WavePal/PROTOCOL.md). 28 29## Channel settings 30 31Settings that apply to one output channel, such as 32`WavePalDevice.loop_mode`, are lists indexed by channel number: index 0 33is unused and holds `None`, and indices 1 to 4 hold the settings of 34output channels 1-4, as in `pulsepal.PulsePalDevice`. Setting an element 35or a slice programs the device at once. Assigning a whole list sets all 36four channels, and takes one value per channel: a single value raises an 37error, because it does not say which channels it is meant for. 38 39```python 40W.loop_mode[2] = True # channel 2 only 41W.loop_duration[1:5] = [1, 2, 3, 4] 42W.loop_mode = [False] * 4 # all four channels 43``` 44 45`WavePalDevice.trigger_mode` is indexed the same way by trigger channel 46number, 1 or 2, as in Pulse Pal. 47 48## Waveforms 49 50The device stores four waveforms, numbered 1-4. Each output channel plays 51one of them, its `WavePalDevice.current_waveform`, or none (`0`, the 52default). Several channels can play the same waveform, which is loaded 53only once. 54 55```python 56W.load_waveform(1, sine, [1, 3]) # waveform 1, played by channels 1 and 3 57W.load_waveform(2, ramp) # stored: no channel plays it yet 58W.current_waveform[1] = 2 # channel 1 plays waveform 2 from its next start 59``` 60 61A channel that is playing finishes its waveform: a new current waveform 62plays from the channel's next start. Loading a waveform stops the 63channels playing it, outside param sync mode. 64 65## Param sync 66 67`"Param Sync"`, Pulse Pal's param sync trigger mode, lets the waveforms 68of the next trial be loaded during the current one, and take effect the 69instant it starts. While a trigger channel is in param sync mode, 70`WavePalDevice.load_waveform` loads the waveform into a second memory 71block that the device keeps for each waveform: channels go on playing 72the waveform it replaces, and triggers still start it. The next rising 73edge on that trigger channel makes it current. A new 74`WavePalDevice.current_waveform` waits for the edge in the same way. 75 76```python 77W.trigger_mode[2] = "Param Sync" # sent at once 78W.load_waveform(1, next_trial_wave) # stored: waveform 1 plays on, unchanged 79# The next rising edge on trigger channel 2 makes it current 80``` 81 82## Units 83 84Voltages are in volts, within `WavePalDevice.output_range`. Times are in 85seconds, and the sampling rate is in Hz. 86 87## License 88 89This file is part of the Sanworks PulsePal repository. 90Copyright (C) Sanworks LLC, Rochester, New York, USA 91 92This program is free software: you can redistribute it and/or modify 93it under the terms of the GNU General Public License as published by 94the Free Software Foundation, version 3. 95 96This program is distributed WITHOUT ANY WARRANTY and without even the 97implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 98See the GNU General Public License for more details. 99 100You should have received a copy of the GNU General Public License 101along with this program. If not, see <http://www.gnu.org/licenses/>. 102""" 103 104from dataclasses import dataclass 105import math 106import numbers 107import struct 108import warnings 109 110import numpy as np 111import serial 112 113from . import _common 114from ._common import ChannelSettings, PulsePalError, to_bool, to_name 115 116__all__ = ["WavePalDevice", "DeviceInfo", "DeviceStatus", "OUTPUT_RANGES", "TRIGGER_MODES"] 117__docformat__ = "google" 118 119OUTPUT_RANGES = { 120 "0V:5V": (0.0, 5.0), 121 "0V:10V": (0.0, 10.0), 122 "-5V:5V": (-5.0, 5.0), 123 "-10V:10V": (-10.0, 10.0), 124} 125"""Output ranges in order of their range index on the device, with their 126limits in volts. See "Output ranges" in /Firmware/WavePal/PROTOCOL.md.""" 127 128TRIGGER_MODES = ("Normal", "Toggle", "Gated", "Param Sync", "Master") 129"""Names of the trigger modes. See `WavePalDevice.trigger_mode`.""" 130 131# Each trigger mode's code on the device. The first four are Pulse Pal's codes. 132_TRIGGER_MODE_CODES = {"Normal": 0, "Toggle": 1, "Gated": 2, "Param Sync": 3, "Master": 4} 133 134 135# The channel settings export_params() returns, after the sampling rate and the output range 136_CHANNEL_SETTINGS = ("current_waveform", "loop_mode", "loop_duration", "trigger_mode", "link_trigger_channel1", 137 "link_trigger_channel2") 138_SETTING_NAMES = ("sampling_rate", "output_range", *_CHANNEL_SETTINGS) # The names a script assigns 139# The ops whose reply _write_command() does not track: those with no reply ('P', 'X', 81 and 89), and the handshake 140# (72), whose reply read_handshake_reply() reads until the device is quiet 141_OPS_WITHOUT_TRACKED_REPLY = frozenset({72, 81, 89, ord("P"), ord("X")}) 142 143 144@dataclass 145class DeviceInfo: 146 """Properties of the connected Wave Pal. 147 148 Populated when `WavePalDevice` connects, and available as 149 `WavePalDevice.info`. 150 """ 151 152 firmware_version: int = None 153 """Wave Pal firmware version running on the device.""" 154 155 hardware_version: int = None 156 """Pulse Pal hardware version, e.g. `3`.""" 157 158 circuit_board_revision: int = None 159 """Revision of the device's circuit board, 0-31. 160 161 The board encodes its revision on five pins, which the firmware reads 162 at startup. A board that encodes none reports `0`. To record exactly 163 which hardware ran an experiment, copy `hardware_version` and 164 `circuit_board_revision` to your data. 165 """ 166 167 serial_number: str = None 168 """USB serial number of the device, e.g. `"20345420"`: the Teensy's, 169 unique to each Pulse Pal 3, and the same whichever firmware it runs. 170 171 Connect by it, wherever the device is plugged in, with 172 `WavePalDevice(serial_number="20345420")`. To record exactly which unit ran an 173 experiment, copy it to your data. 174 """ 175 176 n_channels: int = None 177 """Number of output channels.""" 178 179 n_waveforms: int = None 180 """Number of waveforms the device stores, numbered from 1.""" 181 182 max_samples: int = None 183 """Maximum number of samples in one waveform.""" 184 185 max_sampling_rate: int = None 186 """Highest sampling rate, in Hz.""" 187 188 buffer_samples: int = None 189 """Samples per playback buffer. 190 191 The first `buffer_samples` samples of each waveform are kept in the 192 device's RAM, so that playback starts at once. A waveform no longer 193 than this plays without reading the microSD card. 194 """ 195 196 sample_clock_hz: int = None 197 """Clock that the sample rate is divided from, in Hz. 198 199 See `WavePalDevice.actual_sampling_rate`. 200 """ 201 202 output_ranges: tuple = tuple(OUTPUT_RANGES) 203 """Names of the output ranges, accepted by 204 `WavePalDevice.output_range`.""" 205 206 trigger_modes: tuple = TRIGGER_MODES 207 """Names of the trigger modes, accepted by 208 `WavePalDevice.trigger_mode`.""" 209 210 211@dataclass 212class DeviceStatus: 213 """A snapshot of the device's playback state, from 214 `WavePalDevice.status`.""" 215 216 playing: list 217 """Numbers of the output channels playing a waveform, e.g. `[1, 3]`.""" 218 219 samples_loaded: list 220 """Samples in each waveform, `0` if it has none. 221 222 Indexed by waveform number, with index 0 unused. This is what the 223 device holds, which can include waveforms loaded by an earlier 224 connection. A load stored for a param sync edge counts from the edge. 225 """ 226 227 underruns: list 228 """Underruns on each channel since the device started, indexed by 229 channel number. 230 231 An underrun is a block of samples that was not read from the microSD 232 card by the time it was due. The output then holds its last value 233 until the block arrives. See "Storage and buffering" in the 234 [Wave Pal protocol](https://github.com/sanworks/PulsePal/blob/develop/Firmware/WavePal/PROTOCOL.md#storage-and-buffering). 235 """ 236 237 longest_interrupt_us: float 238 """Longest run of the device's playback interrupt since the previous 239 call to `WavePalDevice.status`, in microseconds. 240 241 It must stay below the sample period, `1e6 / sampling_rate`. 242 """ 243 244 current_waveform: list 245 """The waveform each output channel plays from its next start, `0` for 246 none, as the device holds it. Indexed by channel number. 247 248 In param sync mode, `WavePalDevice.current_waveform` set since the 249 last edge is in `DeviceStatus.stored_current_waveform` until the next 250 one. 251 """ 252 253 stored_waveforms: list 254 """Numbers of the waveforms loaded in param sync mode since the last 255 edge, which the next edge makes current, e.g. `[2]`.""" 256 257 stored_current_waveform: list 258 """The current waveforms set in param sync mode since the last edge, 259 which the next edge applies, indexed by channel number. `None` if 260 none were set.""" 261 262 263class WavePalDevice: 264 """A class to control a Wave Pal on a USB serial port. 265 266 Creating an instance opens the serial port, checks that the device 267 runs Wave Pal firmware, reads its properties into 268 `WavePalDevice.info`, shows "PYTHON Connected" on the device's 269 screen, stops any playback and programs the default settings (see 270 `WavePalDevice.set_default_params`). 271 272 ```python 273 from pulsepal import WavePalDevice 274 275 W = WavePalDevice("COM3") 276 W.load_waveform(1, [0, 1, 2, 3, 4, 5, 0], 1) # waveform 1, played by channel 1 277 W.trigger(1) 278 W.close() 279 ``` 280 281 Replace "COM3" with the device's USB serial port name, which 282 `WavePalDevice.serialportlist` lists. `WavePalDevice` is also a 283 context manager, which closes the port on exit. 284 285 Closing the connection stops playback, as it does on Pulse Pal, and 286 puts the device's own name back on its screen. The device keeps its 287 settings and waveforms, so TTL triggers still play them. 288 """ 289 290 port: "serial.Serial" 291 """The open `serial.Serial` port connected to the device.""" 292 293 info: DeviceInfo 294 """Properties of the connected device. See `DeviceInfo`.""" 295 296 _CURRENT_FIRMWARE_VERSION = 1 297 _BAUD_RATE = 12000000 # USB serial ignores the baud rate 298 299 _OP_MENU_BYTE = _common.OP_MENU_BYTE 300 _OP_HANDSHAKE = _common.OP_HANDSHAKE 301 _OP_DISCONNECT = 81 302 _OP_SET_CLIENT_NAME = 89 303 _OP_SET_SCREEN_SAVER = 99 304 _OP_HARDWARE_REVISION = 100 305 _OP_HARDWARE_INFO = ord("N") 306 _OP_SET_SAMPLING_RATE = ord("S") 307 _OP_SET_OUTPUT_RANGE = ord("R") 308 _OP_LOAD_WAVEFORM = ord("L") 309 _OP_SET_CURRENT_WAVEFORMS = ord("C") 310 _OP_TRIGGER = ord("P") 311 _OP_STOP = ord("X") 312 _OP_SET_FIXED_VOLTAGE = ord("!") 313 _OP_SET_LOOP_MODE = ord("O") 314 _OP_SET_LOOP_DURATION = ord("D") 315 _OP_SET_TRIGGER_MODE = ord("T") 316 _OP_SET_TRIGGER_LINKS = ord("I") 317 _OP_GET_STATUS = ord("G") 318 _OP_GET_PLAYBACK_CHECKSUMS = ord("Z") 319 320 _HANDSHAKE_REPLY = 87 # 'W' 321 _HARDWARE_INFO_FORMAT = "<BBIIIIB" 322 _STATUS_FORMAT = "<B4I4II4BBB4B" 323 _DAC_BITMAX = 65535 324 _UINT32_MAX = 2**32 - 1 325 _ALL_CHANNELS = 0x0F 326 # The ops that carry a setting, which _discard_lost_reply() sends again if the reply to one is lost 327 _SETTING_OPS = frozenset(map(ord, "SRCODTI")) 328 329 _reply_pending = None # The op code of a command whose reply has not been read in full yet (see _write_command()) 330 331 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 332 """Open a connection to a Wave Pal. 333 334 Args: 335 port_name: USB serial port of the device, such as `COM3` on 336 Windows or `/dev/ttyACM0` on Linux. 337 serial_number: Instead of `port_name`, the device's USB serial 338 number (`DeviceInfo.serial_number`), which finds its port 339 wherever it is plugged in. 340 timeout: Serial read timeout, in seconds. Loading a long 341 waveform onto a slow microSD card can take a few seconds. 342 343 Firmware newer than this module knows is used with a warning: new 344 firmware only adds commands. Update the package to use what is new. 345 346 Raises: 347 PulsePalError: If the device does not reply to the handshake, or 348 runs other firmware. 349 serial.SerialException: If the serial port cannot be opened, 350 e.g. because another program has it open (on Linux and 351 macOS, another session of these classes, which lock it). 352 """ 353 self._closed = True 354 self.info = DeviceInfo() 355 self._sampling_rate = None 356 self._output_range = None 357 self._waveforms = [] # Indexed by waveform number, once the device has said how many it stores 358 self._current_waveform = ChannelSettings( 359 "current_waveform", [0] * 4, self, "_apply_current_waveform") 360 self._loop_mode = ChannelSettings( 361 "loop_mode", [False] * 4, self, "_apply_loop_mode") 362 self._loop_duration = ChannelSettings( 363 "loop_duration", [0.0] * 4, self, "_apply_loop_duration") 364 self._trigger_mode = ChannelSettings( 365 "trigger_mode", ["Normal"] * 2, self, "_apply_trigger_mode") 366 self._link_trigger_channel1 = ChannelSettings( 367 "link_trigger_channel1", [False] * 4, self, 368 "_apply_trigger_channel1_links") 369 self._link_trigger_channel2 = ChannelSettings( 370 "link_trigger_channel2", [False] * 4, self, 371 "_apply_trigger_channel2_links") 372 373 port_name = _common.port_name_from(port_name, serial_number, "WavePalDevice") 374 self.port = serial.Serial( 375 port_name, 376 self._BAUD_RATE, 377 timeout=timeout, 378 rtscts=True, 379 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 380 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 381 # it, such as another of these classes, gets an error instead 382 exclusive=True, 383 ) 384 self._closed = False 385 try: 386 # Discard anything left in the buffer by an earlier session 387 self.port.reset_input_buffer() 388 self._handshake() 389 self._read_hardware_info() 390 self._waveforms = [None] * (self.info.n_waveforms + 1) 391 # Client name op + "PYTHON" in ASCII, shown as "PYTHON Connected" 392 self._write_command(self._OP_SET_CLIENT_NAME, b"PYTHON") 393 self.stop() 394 self.set_default_params() 395 except BaseException: 396 # Op 81 means something else to other devices, so it is sent 397 # only once the device has identified itself as a Wave Pal 398 self._close(send_disconnect=self.info.firmware_version is not None) 399 raise 400 401 @staticmethod 402 def serialportlist(ports_to_list="available"): 403 """Return the names of the USB serial ports on this computer. 404 405 Called on the class, without connecting to a device, to find the 406 port name to pass to `WavePalDevice`. 407 408 Args: 409 ports_to_list: `available` to list only the ports that are 410 not already in use, or `all` to list every USB serial 411 port. Not case sensitive. 412 413 Returns: 414 Sorted list of port names, such as `["COM3", "COM7"]`. 415 416 Raises: 417 PulsePalError: If `ports_to_list` is not `available` or `all`. 418 """ 419 return _common.serialportlist(ports_to_list) 420 421 # ------------------------------------------------------------------ 422 # Settings 423 # ------------------------------------------------------------------ 424 425 def set_default_params(self): 426 """Program the default settings on the device. 427 428 The defaults are both trigger channels in normal mode, a 10 kHz 429 sampling rate, the -10 V to 10 V output range, loop mode off with 430 loop durations of 0, no output channel linked to a trigger channel 431 (so a TTL starts nothing until a script links it), and no current 432 waveform on any channel. They match the settings the device starts with. The 433 trigger modes are sent first: a trigger channel leaving param sync 434 mode makes what was stored for its next edge current, and the 435 settings after it then apply at once. 436 437 Loaded waveforms are kept, and loaded again if the output range 438 changes (see `WavePalDevice.output_range`). 439 440 Raises: 441 PulsePalError: If a loaded waveform does not fit the default 442 output range, -10 V to 10 V. 443 """ 444 self.trigger_mode = ["Normal"] * 2 445 self.sampling_rate = 10000 446 self.output_range = "-10V:10V" 447 self.loop_mode = [False] * 4 448 self.loop_duration = [0] * 4 449 self._set_trigger_links([False] * 4, [False] * 4) 450 self.current_waveform = [0] * 4 451 452 @property 453 def sampling_rate(self): 454 """Sampling rate of all output channels, in Hz. 455 456 A whole number from 1 to `DeviceInfo.max_sampling_rate`. It can be 457 changed during playback. The rate played can differ slightly from 458 the rate set: see `WavePalDevice.actual_sampling_rate`. 459 """ 460 return self._sampling_rate 461 462 @sampling_rate.setter 463 def sampling_rate(self, rate): 464 rate_hz = self._normalize_sampling_rate(rate) 465 # Loop durations are sent in samples, so check that they still fit 466 # before anything is changed 467 loop_samples = None 468 if self._sampling_rate is not None: 469 loop_samples = self._durations_to_samples( 470 self._loop_duration[1:], rate_hz) 471 self._write_command(self._OP_SET_SAMPLING_RATE, 472 struct.pack("<I", rate_hz)) 473 self._read_ack("setting sampling_rate") 474 self._sampling_rate = rate_hz 475 if loop_samples is not None: 476 self._send_loop_duration_samples(loop_samples) 477 478 @property 479 def actual_sampling_rate(self): 480 """The sampling rate the device plays, in Hz. 481 482 The device divides `DeviceInfo.sample_clock_hz` (24 MHz) by a 483 whole number, so the rate played is the nearest one of those to 484 `WavePalDevice.sampling_rate`. For example, 44100 Hz plays at 485 44117.6 Hz. Rates that divide 24 MHz exactly, such as 10 kHz, 486 25 kHz or 100 kHz, play exactly. 487 """ 488 return self._actual_rate(self._sampling_rate) 489 490 @property 491 def output_range(self): 492 """Voltage range of all output channels. 493 494 One of `DeviceInfo.output_ranges`: `"0V:5V"`, `"0V:10V"`, 495 `"-5V:5V"` or `"-10V:10V"`. The smallest range that fits the 496 waveforms gives the finest voltage steps. 497 498 Changing the range stops playback, sets all outputs to 0 V, and 499 loads the waveforms again, encoded for the new range. It raises 500 an error, and changes nothing, if a loaded waveform does not fit 501 the new range, or if a trigger channel is in param sync mode, 502 where the loads would wait for a TTL edge. 503 """ 504 return self._output_range 505 506 @output_range.setter 507 def output_range(self, range_name): 508 name = self._check_output_range(range_name, self._trigger_mode[1:]) 509 changed = name != self._output_range 510 range_index = list(OUTPUT_RANGES).index(name) 511 self._write_command(self._OP_SET_OUTPUT_RANGE, bytes([range_index])) 512 self._read_ack("setting output_range") 513 self._output_range = name 514 if changed: 515 # The device unloads the waveforms when the range changes, 516 # because their samples encode voltages in the old range. The 517 # channels keep their current waveforms. 518 waveforms = self._waveforms 519 self._waveforms = [None] * len(waveforms) 520 for number, waveform in enumerate(waveforms): 521 if waveform is not None: 522 self.load_waveform(number, waveform) 523 524 @property 525 def current_waveform(self): 526 """The waveform each output channel plays, by waveform number. 527 528 Indexed by channel number (see "Channel settings" above). Each 529 value is the number of a waveform loaded with 530 `WavePalDevice.load_waveform`, 1 to `DeviceInfo.n_waveforms`, or 531 `0` for none, the default: a channel with no waveform ignores 532 triggers. Several channels can play the same waveform. 533 534 A channel that is playing finishes its waveform: the new one plays 535 from the channel's next start. In param sync mode, a change waits 536 for the next rising edge on the param sync trigger channel (see 537 `WavePalDevice.trigger_mode`). 538 539 ```python 540 W.current_waveform[1] = 2 # channel 1 plays waveform 2 541 W.current_waveform = [1, 1, 1, 1] # all four play waveform 1 542 ``` 543 """ 544 return self._current_waveform 545 546 @current_waveform.setter 547 def current_waveform(self, values): 548 self._current_waveform._assign(values) 549 550 @property 551 def loop_mode(self): 552 """Whether each output channel loops its waveform. 553 554 Indexed by channel number (see "Channel settings" above). `True` 555 repeats the waveform until `WavePalDevice.loop_duration` has 556 elapsed, or until stopped if the loop duration is 0. `False` plays 557 it once. A change applies to playback in progress. 558 """ 559 return self._loop_mode 560 561 @loop_mode.setter 562 def loop_mode(self, values): 563 self._loop_mode._assign(values) 564 565 @property 566 def loop_duration(self): 567 """How long each output channel loops its waveform, in seconds. 568 569 Indexed by channel number (see "Channel settings" above). Applies 570 in loop mode only, and counts from the trigger. The channel stops 571 when it has elapsed, part way through the waveform if need be. `0` 572 loops until the channel is stopped. Durations are rounded to a 573 whole number of samples, and a nonzero duration lasts at least 574 one sample. 575 """ 576 return self._loop_duration 577 578 @loop_duration.setter 579 def loop_duration(self, values): 580 self._loop_duration._assign(values) 581 582 @property 583 def trigger_mode(self): 584 """How each trigger channel acts on the output channels linked to it. 585 586 Indexed by trigger channel number, 1 or 2 (index 0 is unused), as 587 in Pulse Pal. A TTL edge on a trigger channel acts on the output 588 channels linked to it (`WavePalDevice.link_trigger_channel1`, 589 `WavePalDevice.link_trigger_channel2`): 590 591 - `"Normal"`: a rising edge starts the linked channels' waveforms. 592 Channels that are playing ignore it. 593 - `"Toggle"`: a rising edge starts the linked channels, or stops 594 those that are playing. 595 - `"Gated"`: a rising edge starts the linked channels, and a 596 falling edge stops them, unless the other trigger channel is also 597 gated, linked to them, and still high. With loop mode on and a 598 loop duration of 0, a waveform plays for exactly as long as the 599 TTL is high. 600 - `"Param Sync"`: a rising edge starts and stops nothing (the 601 channel's links are ignored). It makes the waveforms loaded, 602 and the `WavePalDevice.current_waveform` set, since the previous 603 edge current. This is how the next trial's waveforms are loaded 604 during the current trial, and played from the instant it starts. 605 - `"Master"`: a rising edge starts the linked channels, and 606 restarts those that are playing from their first sample. 607 608 The first four are Pulse Pal's trigger modes. Names are not case 609 sensitive. 610 611 While a trigger channel is in param sync mode, 612 `WavePalDevice.load_waveform` loads into a second memory block 613 that the device keeps for each waveform. Channels go on playing 614 the waveform it replaces, and triggers still start it, until the 615 edge. A channel that is playing at the edge finishes what it plays, 616 and plays the new waveform from its next start. A later load of the 617 same waveform before the edge replaces the earlier one, and stops 618 any channel still playing the waveform from before the previous 619 edge, since the device reuses that memory block. Changes to 620 `WavePalDevice.current_waveform` wait for the edge in the same 621 way. Other settings apply at once. 622 623 When neither trigger channel is in param sync mode any more, what 624 was waiting for an edge becomes current at once, so 625 `WavePalDevice.waveforms` and `WavePalDevice.current_waveform` 626 describe the device again. (Pulse Pal and Synth Pal discard their 627 stored parameters and settings then; Pulse Pal makes its stored 628 custom trains current, as this class does its waveforms.) 629 `WavePalDevice.output_range` cannot change 630 in param sync mode. To make the waveforms current on the same edge 631 that starts the channels, wire the TTL to the other trigger channel 632 as well: the param sync edge is handled first. 633 634 ```python 635 W.trigger_mode[2] = "Param Sync" # sent at once 636 W.load_waveform(1, next_trial_wave) # waits for trigger channel 2's next rising edge 637 ``` 638 """ 639 return self._trigger_mode 640 641 @trigger_mode.setter 642 def trigger_mode(self, values): 643 self._trigger_mode._assign(values) 644 645 @property 646 def link_trigger_channel1(self): 647 """Whether each output channel is triggered by trigger channel 1. 648 649 Indexed by channel number (see "Channel settings" above). By 650 default no output channel is linked to either trigger channel, so 651 a TTL starts nothing until a script links it. 652 """ 653 return self._link_trigger_channel1 654 655 @link_trigger_channel1.setter 656 def link_trigger_channel1(self, values): 657 self._link_trigger_channel1._assign(values) 658 659 @property 660 def link_trigger_channel2(self): 661 """Whether each output channel is triggered by trigger channel 2. 662 663 Indexed by channel number (see "Channel settings" above). 664 """ 665 return self._link_trigger_channel2 666 667 @link_trigger_channel2.setter 668 def link_trigger_channel2(self, values): 669 self._link_trigger_channel2._assign(values) 670 671 def set_screen_saver(self, enabled, timeout=1800): 672 """Switch the device's screen saver on or off, and set its timeout. 673 674 With the screen saver on, the device dims its screen once it has 675 been left alone for `timeout` seconds: no command from a computer, 676 no rising edge on a trigger channel, and no joystick click or 677 push. The next of these brings the screen back. The device keeps 678 both settings in its EEPROM, shared with Pulse Pal and Synth Pal 679 firmware, and saves them once no channel is playing. A new device 680 starts with the screen saver on and 1800 s. 681 682 Args: 683 enabled: `True` to switch the screen saver on, `False` to 684 switch it off. 685 timeout: Seconds without activity before the screen dims, 1 to 686 65535. It is sent with every call, so leaving it out sets 687 1800 s. 688 689 Raises: 690 PulsePalError: If a value is out of range. 691 """ 692 state = to_bool(enabled, "enabled") 693 if isinstance(timeout, (bool, np.bool_)) \ 694 or not isinstance(timeout, numbers.Integral) \ 695 or not 1 <= timeout <= 65535: 696 raise PulsePalError( 697 "timeout must be a whole number of seconds from 1 to 65535. " 698 f"Received {timeout!r}." 699 ) 700 self._write_command(self._OP_SET_SCREEN_SAVER, 701 struct.pack("<BH", int(state), int(timeout))) 702 self._read_ack("set_screen_saver()") 703 704 def export_params(self): 705 """Return every setting, as a dict of plain values and lists. 706 707 Keyed by setting name: `"sampling_rate"` and `"output_range"`, then 708 the channel settings, each with one value per channel and no 709 unused index 0. It holds only numbers, booleans and names, so it 710 can be saved with `json` and logged with your data. The waveforms 711 are not part of it: `WavePalDevice.waveforms` holds them. 712 713 ```python 714 import json 715 with open("session_settings.json", "w") as f: 716 json.dump(W.export_params(), f) 717 ``` 718 """ 719 params = {"sampling_rate": self._sampling_rate, "output_range": self._output_range} 720 for name in _CHANNEL_SETTINGS: 721 params[name] = list(getattr(self, name))[1:] 722 return params 723 724 def import_params(self, params): 725 """Program the device with settings exported by `WavePalDevice.export_params`. 726 727 All of them are checked first, and nothing is sent if one is 728 invalid. They are then sent in an order that lets each take 729 effect: a trigger channel leaving param sync mode first, then the 730 sampling rate, the output range and the channel settings, and a 731 trigger channel entering param sync mode last. So 732 `WavePalDevice.current_waveform` applies at once, unless a trigger 733 channel was in param sync mode and stays in it: then it waits for 734 the next edge, as an assignment would. Settings missing from 735 `params` keep their values. A name that is not a setting is 736 skipped with a warning, so that settings exported by a newer 737 version of this package, which may have more, can still be 738 imported. 739 740 The waveforms are not settings: load them with 741 `WavePalDevice.load_waveform` first, since a current waveform must 742 be a loaded one. 743 744 ```python 745 import json 746 with open("session_settings.json") as f: 747 W.import_params(json.load(f)) 748 ``` 749 750 Args: 751 params: A dict of setting name to its value: a number for 752 `"sampling_rate"`, a name for `"output_range"`, and one 753 value per channel (a list, tuple or NumPy array) for the 754 others, as `export_params` returns. 755 756 Raises: 757 PulsePalError: If a value is invalid (nothing is sent), or the 758 device rejects a setting. 759 """ 760 names = {"sampling_rate", "output_range", *_CHANNEL_SETTINGS} 761 unknown = [name for name in params if name not in names] 762 if unknown: 763 warnings.warn(f"import_params(): skipped {', '.join(map(repr, unknown))}, which this version of " 764 "WavePalDevice does not have.", stacklevel=2) 765 new = {} 766 for name in _CHANNEL_SETTINGS: 767 if name in params: 768 n = 2 if name == "trigger_mode" else self.info.n_channels 769 new[name] = _common.one_value_per_channel(name, params[name], n) 770 if "current_waveform" in new: 771 new["current_waveform"] = self._normalize_current_waveform(new["current_waveform"]) 772 if "loop_mode" in new: 773 new["loop_mode"] = [to_bool(value, "loop_mode") for value in new["loop_mode"]] 774 if "loop_duration" in new: 775 new["loop_duration"] = self._normalize_loop_durations(new["loop_duration"]) 776 for name in ("link_trigger_channel1", "link_trigger_channel2"): 777 if name in new: 778 new[name] = [to_bool(value, name) for value in new[name]] 779 modes = list(self._trigger_mode)[1:] 780 if "trigger_mode" in new: 781 modes = [to_name(value, TRIGGER_MODES, "trigger mode") for value in new["trigger_mode"]] 782 # The trigger modes the other settings are sent in: those of channels leaving param sync mode are sent 783 # first, those of channels entering it last 784 first_modes = [new_mode if (old == "Param Sync" and new_mode != "Param Sync") else old 785 for old, new_mode in zip(self._trigger_mode[1:], modes)] 786 rate = self._sampling_rate 787 if "sampling_rate" in params: 788 rate = self._normalize_sampling_rate(params["sampling_rate"]) 789 range_name = self._output_range 790 if "output_range" in params: 791 range_name = self._check_output_range(params["output_range"], first_modes) 792 self._durations_to_samples(new.get("loop_duration", self._loop_duration[1:]), rate) # They fit at the rate 793 794 if first_modes != list(self._trigger_mode)[1:]: 795 self.trigger_mode = first_modes 796 if rate != self._sampling_rate: 797 self.sampling_rate = rate 798 if range_name != self._output_range: 799 self.output_range = range_name 800 for name in ("loop_mode", "loop_duration"): 801 if name in new: 802 setattr(self, name, new[name]) 803 if "link_trigger_channel1" in new or "link_trigger_channel2" in new: 804 self._set_trigger_links(new.get("link_trigger_channel1", self._link_trigger_channel1[1:]), 805 new.get("link_trigger_channel2", self._link_trigger_channel2[1:])) 806 if "current_waveform" in new: 807 self.current_waveform = new["current_waveform"] 808 if modes != first_modes: 809 self.trigger_mode = modes 810 811 @property 812 def waveforms(self): 813 """The waveforms loaded with `WavePalDevice.load_waveform`. 814 815 A list indexed by waveform number, 1 to `DeviceInfo.n_waveforms`, 816 with index 0 unused. Each element is a read-only NumPy array of 817 voltages, or `None` if this object has not loaded that waveform. In 818 param sync mode, it holds the waveforms loaded since the last edge 819 before the device plays them. `WavePalDevice.status` shows what 820 the device itself holds. 821 """ 822 return list(self._waveforms) 823 824 # ------------------------------------------------------------------ 825 # Waveforms and playback 826 # ------------------------------------------------------------------ 827 828 def load_waveform(self, waveform_number, waveform, channels=None): 829 """Load one of the device's waveforms, and choose the channels that play it. 830 831 The samples are written to the device's microSD card, and the 832 first `DeviceInfo.buffer_samples` of them are also kept in its RAM. 833 Outside param sync mode, loading stops the channels playing that 834 waveform, and the new one plays from their next start. Other 835 channels keep playing. In param sync mode, the new waveform waits 836 for the next rising edge on the param sync trigger channel, and 837 channels go on playing the old one until then (see 838 `WavePalDevice.trigger_mode`). 839 840 ```python 841 t = np.arange(10000) / W.sampling_rate 842 W.load_waveform(2, 3 * np.sin(2 * np.pi * 100 * t), [1, 3]) 843 ``` 844 845 Args: 846 waveform_number: The waveform to load, 1 to 847 `DeviceInfo.n_waveforms`. 848 waveform: Voltages of the samples, played at 849 `WavePalDevice.sampling_rate`. A list, tuple or NumPy 850 array of 1 to `DeviceInfo.max_samples` values, all within 851 `WavePalDevice.output_range`. 852 channels: Output channels that play this waveform, from their 853 next start: one channel number 1-4, or several as a list, 854 tuple or NumPy array. It sets their 855 `WavePalDevice.current_waveform`, and leaves the other 856 channels' alone, so channels already playing this waveform 857 play the new one. `None`, the default, changes no channel. 858 859 Raises: 860 PulsePalError: If an argument is invalid, or the device could 861 not store the waveform. The waveform is then left out of 862 `WavePalDevice.waveforms`: outside param sync mode the 863 device has unloaded it, and in param sync mode it plays the 864 version made current at the last edge. 865 """ 866 number = self._waveform_number(waveform_number) 867 channel_list = None if channels is None else _common.channel_numbers(channels) 868 samples = np.array(waveform, dtype=float).ravel() 869 if not 1 <= samples.size <= self.info.max_samples: 870 raise PulsePalError( 871 f"A waveform must have 1 to {self.info.max_samples} samples. " 872 f"Received {samples.size}." 873 ) 874 codes = self._volts_to_codes(samples) 875 self._waveforms[number] = None # Until the device has stored it. Outside param sync mode, it unloads it first. 876 self._write_command( 877 self._OP_LOAD_WAVEFORM, 878 struct.pack("<BI", number, samples.size) + codes.tobytes(), 879 ) 880 self._read_ack("load_waveform()") 881 samples.flags.writeable = False 882 self._waveforms[number] = samples 883 if channel_list: 884 values = list(self._current_waveform[1:]) 885 for channel in channel_list: 886 values[channel - 1] = number 887 self._current_waveform._assign(values) 888 889 def trigger(self, channels): 890 """Trigger output channels in software. 891 892 Idle channels start their current waveforms from the first 893 sample, together. Channels that are playing ignore it, as Pulse 894 Pal's channels ignore a soft trigger while they play a pulse train. 895 896 ```python 897 W.trigger(1) 898 W.trigger([2, 4]) # a list, tuple or NumPy array 899 ``` 900 901 Args: 902 channels: Output channel number 1-4, or several as a list, 903 tuple or NumPy array. 904 905 Raises: 906 PulsePalError: If a channel has no waveform to play: its 907 `WavePalDevice.current_waveform` is 0, or a waveform this 908 object has not loaded. Nothing is sent. 909 """ 910 channel_list = _common.channel_numbers(channels) 911 for channel in channel_list: 912 number = self._current_waveform[channel] 913 if number == 0: 914 raise PulsePalError( 915 f"Channel {channel} has no waveform to play. Set current_waveform[{channel}] to a loaded " 916 f"waveform's number, or load one with load_waveform(number, waveform, {channel})." 917 ) 918 if self._waveforms[number] is None: 919 raise PulsePalError( 920 f"Channel {channel} plays waveform {number}, which is not loaded. Load it with " 921 f"load_waveform({number}, waveform)." 922 ) 923 self._write_command(self._OP_TRIGGER, bytes([_common.channel_bits(channel_list)])) 924 925 def stop(self, channels=None): 926 """Stop playback. The stopped channels output 0 V. 927 928 Args: 929 channels: Output channel number 1-4, or several as a list, 930 tuple or NumPy array. `None` stops all channels. 931 """ 932 bits = self._ALL_CHANNELS if channels is None else _common.channel_bits(channels) 933 self._write_command(self._OP_STOP, bytes([bits])) 934 935 def set_fixed_voltage(self, channels, voltage): 936 """Set output channels to a fixed voltage. 937 938 Stops playback on those channels. The voltage holds until the 939 channel is triggered or stopped. 940 941 Args: 942 channels: Output channel number 1-4, or several as a list, 943 tuple or NumPy array. 944 voltage: Voltage, within `WavePalDevice.output_range`. 945 946 Raises: 947 PulsePalError: If the voltage is outside the output range, or 948 the device rejects the command. 949 """ 950 bits = _common.channel_bits(channels) 951 code = int(self._volts_to_codes([voltage])[0]) 952 self._write_command(self._OP_SET_FIXED_VOLTAGE, 953 struct.pack("<BH", bits, code)) 954 self._read_ack("set_fixed_voltage()") 955 956 def status(self): 957 """Read the device's playback state. 958 959 Returns: 960 A `DeviceStatus`, with the channels that are playing, the 961 number of samples in each waveform, underrun counts, the 962 longest playback interrupt, each channel's current waveform, 963 and what waits for a param sync edge. 964 """ 965 self._write_command(self._OP_GET_STATUS) 966 values = struct.unpack( 967 self._STATUS_FORMAT, 968 self._read_raw(struct.calcsize(self._STATUS_FORMAT)), 969 ) 970 playing_bits = values[0] 971 stored_load_bits = values[14] 972 return DeviceStatus( 973 playing=[ch for ch in range(1, 5) 974 if playing_bits & (1 << (ch - 1))], 975 samples_loaded=[None, *values[1:5]], 976 underruns=[None, *values[5:9]], 977 longest_interrupt_us=values[9] / 1000, 978 current_waveform=[None, *values[10:14]], 979 stored_waveforms=[n for n in range(1, 5) if stored_load_bits & (1 << (n - 1))], 980 stored_current_waveform=[None, *values[16:20]] if values[15] else None, 981 ) 982 983 def _playback_checksums(self): 984 """For testing: what each channel played since it last started. 985 986 Returns two lists indexed by channel number, with index 0 unused: 987 the number of samples played, and the sum of their DAC codes 988 modulo 2**32. `tests/wavepal_hardware_test.py` compares them with 989 the waveforms it loaded, to check every sample the device played. 990 """ 991 self._write_command(self._OP_GET_PLAYBACK_CHECKSUMS) 992 values = struct.unpack("<8I", self._read_raw(32)) 993 return [None, *values[:4]], [None, *values[4:]] 994 995 # ------------------------------------------------------------------ 996 # Connection 997 # ------------------------------------------------------------------ 998 999 def close(self): 1000 """Close the connection to the device. 1001 1002 The device stops playback on all channels, and shows its own name 1003 on its screen again, in place of "PYTHON Connected". It keeps its 1004 settings and waveforms, so TTL triggers still play them. Safe to 1005 call more than once. Called automatically when leaving a `with` 1006 block and when the object is garbage collected, so playback also 1007 stops when the last reference to the object goes. 1008 1009 """ 1010 self._close(send_disconnect=True) 1011 1012 def _close(self, send_disconnect): 1013 """Close the connection. send_disconnect: tell the device first (op 81). False when the device may not 1014 run this firmware: op 81 means something else to other devices.""" 1015 if getattr(self, "_closed", True): 1016 return 1017 self._closed = True 1018 self._reply_pending = None # Nothing more is read, so a reply still to come needs no discarding 1019 try: 1020 if send_disconnect and self.port and self.port.is_open: 1021 self._write_command(self._OP_DISCONNECT) 1022 except Exception: 1023 pass # The port may already be gone, e.g. the cable was unplugged 1024 finally: 1025 if self.port and self.port.is_open: 1026 self.port.close() 1027 1028 def __enter__(self): 1029 """Enter a `with` block, returning the connected device.""" 1030 return self 1031 1032 def __exit__(self, exc_type, exc_value, traceback): 1033 """Close the port when leaving a `with` block. 1034 1035 Returns: 1036 `False`, so any exception raised in the block propagates. 1037 """ 1038 self.close() 1039 return False 1040 1041 def __del__(self): 1042 """Close the port when the object is collected.""" 1043 try: 1044 self.close() 1045 except Exception: 1046 # Destructors should not raise; the serial object may already be 1047 # gone during interpreter shutdown. 1048 pass 1049 1050 def __setattr__(self, name, value): 1051 """Refuse a public attribute that is not a setting, such as a misspelled one: it would program nothing (see 1052 _common.refuse_unknown_attribute()).""" 1053 if name[0] != "_": 1054 _common.refuse_unknown_attribute(self, name, _SETTING_NAMES) 1055 super().__setattr__(name, value) 1056 1057 def __repr__(self): 1058 """Describe the device and its settings, e.g. with `print(W)`.""" 1059 port = getattr(getattr(self, "port", None), "port", None) 1060 loaded = ", ".join( 1061 f"{number}: {'none' if w is None else f'{w.size} samples'}" 1062 for number, w in enumerate(self._waveforms) if number > 0 1063 ) 1064 return ( 1065 f"WavePalDevice on {port} (Wave Pal firmware " 1066 f"v{self.info.firmware_version})\n" 1067 f"sampling_rate: {self._sampling_rate} Hz\n" 1068 f"output_range: {self._output_range}\n" 1069 f"current_waveform: {list(self._current_waveform)}\n" 1070 f"loop_mode: {list(self._loop_mode)}\n" 1071 f"loop_duration: {list(self._loop_duration)}\n" 1072 f"trigger_mode: {list(self._trigger_mode)}\n" 1073 f"link_trigger_channel1: {list(self._link_trigger_channel1)}\n" 1074 f"link_trigger_channel2: {list(self._link_trigger_channel2)}\n" 1075 f"waveforms: {loaded}" 1076 ) 1077 1078 # ------------------------------------------------------------------ 1079 # Internals 1080 # ------------------------------------------------------------------ 1081 1082 def _handshake(self): 1083 """Check that the device runs a supported Wave Pal firmware.""" 1084 self._write_command(self._OP_HANDSHAKE) 1085 handshake = _common.read_handshake_reply(self.port) # Skips a reply to an earlier session's command 1086 if handshake is None: 1087 raise PulsePalError( 1088 f"No reply from the device on {self.port.port}. Is it a " 1089 "Pulse Pal 3 running Wave Pal firmware?" 1090 f" {_common.NO_REPLY_HINT}" 1091 ) 1092 reply, version = handshake 1093 if reply != self._HANDSHAKE_REPLY: 1094 if reply in _common.FIRMWARE_BY_HANDSHAKE_REPLY: 1095 raise _common.other_firmware_error(reply, version, self.port.port, "Wave Pal") 1096 raise PulsePalError( 1097 "Incorrect handshake returned. Expected " 1098 f"{self._HANDSHAKE_REPLY}, received {reply}." 1099 ) 1100 if version > self._CURRENT_FIRMWARE_VERSION: 1101 # New firmware only adds commands (see PROTOCOL.md), so this class still works with it 1102 warnings.warn( 1103 f"Wave Pal firmware v{version} is newer than this pulsepal package knows " 1104 f"(v{self._CURRENT_FIRMWARE_VERSION}). It works with it, but update the package to use what is " 1105 "new.", stacklevel=3) 1106 self.info.firmware_version = version 1107 1108 def _read_hardware_info(self): 1109 self._write_command(self._OP_HARDWARE_INFO) 1110 ( 1111 self.info.hardware_version, 1112 self.info.n_channels, 1113 self.info.max_samples, 1114 self.info.max_sampling_rate, 1115 self.info.buffer_samples, 1116 self.info.sample_clock_hz, 1117 self.info.n_waveforms, 1118 ) = struct.unpack( 1119 self._HARDWARE_INFO_FORMAT, 1120 self._read_raw(struct.calcsize(self._HARDWARE_INFO_FORMAT)), 1121 ) 1122 self._write_command(self._OP_HARDWARE_REVISION) 1123 self.info.circuit_board_revision = self._read_raw(1)[0] 1124 self._write_command(_common.OP_SERIAL_NUMBER) 1125 self.info.serial_number = _common.read_serial_number(self._read_raw) 1126 1127 def _apply_current_waveform(self, values): 1128 waveform_numbers = self._normalize_current_waveform(values) 1129 self._write_command(self._OP_SET_CURRENT_WAVEFORMS, bytes(waveform_numbers)) 1130 self._read_ack("setting current_waveform") 1131 return waveform_numbers 1132 1133 def _normalize_current_waveform(self, values): 1134 """Check new current waveforms for all channels, and return them as the list holds them.""" 1135 waveform_numbers = [] 1136 for channel, value in enumerate(values, start=1): 1137 if (isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Integral) 1138 or not 0 <= value <= self.info.n_waveforms): 1139 raise PulsePalError( 1140 f"current_waveform values are waveform numbers, 1-{self.info.n_waveforms}, or 0 for none. " 1141 f"Received {value!r}." 1142 ) 1143 value = int(value) 1144 # A channel left on a waveform whose load failed is not refused here: trigger() refuses it 1145 if value != 0 and value != self._current_waveform[channel] and self._waveforms[value] is None: 1146 raise PulsePalError( 1147 f"Waveform {value} is not loaded. Load it with load_waveform({value}, waveform) first." 1148 ) 1149 waveform_numbers.append(value) 1150 return waveform_numbers 1151 1152 def _apply_loop_mode(self, values): 1153 modes = [to_bool(value, "loop_mode") for value in values] 1154 self._write_command(self._OP_SET_LOOP_MODE, bytes(map(int, modes))) 1155 self._read_ack("setting loop_mode") 1156 return modes 1157 1158 def _apply_loop_duration(self, values): 1159 durations = self._normalize_loop_durations(values) 1160 samples = self._durations_to_samples(durations, self._sampling_rate) 1161 self._send_loop_duration_samples(samples) 1162 return durations 1163 1164 def _normalize_loop_durations(self, values): 1165 """Check new loop durations for all channels, and return them as the list holds them, in seconds.""" 1166 durations = [] 1167 for value in values: 1168 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real): 1169 raise PulsePalError( 1170 f"loop_duration must be in seconds. Received {value!r}." 1171 ) 1172 duration = float(value) 1173 if not math.isfinite(duration) or duration < 0: 1174 raise PulsePalError( 1175 "loop_duration must be 0 (loop until stopped) or a " 1176 f"positive number of seconds. Received {value!r}." 1177 ) 1178 durations.append(duration) 1179 return durations 1180 1181 def _send_loop_duration_samples(self, samples): 1182 self._write_command(self._OP_SET_LOOP_DURATION, 1183 struct.pack("<4I", *samples)) 1184 self._read_ack("setting loop_duration") 1185 1186 def _durations_to_samples(self, durations, rate_hz): 1187 """Convert loop durations in seconds to samples at a sampling rate.""" 1188 actual_rate = self._actual_rate(rate_hz) 1189 samples = [] 1190 for duration in durations: 1191 n = round(duration * actual_rate) 1192 if duration > 0: 1193 n = max(n, 1) # 0 samples would mean "loop until stopped" 1194 if n > self._UINT32_MAX: 1195 raise PulsePalError( 1196 f"A loop_duration of {duration} s is too long at " 1197 f"{rate_hz} Hz. The longest is " 1198 f"{self._UINT32_MAX / actual_rate:.0f} s." 1199 ) 1200 samples.append(n) 1201 return samples 1202 1203 def _apply_trigger_mode(self, values): 1204 names = [to_name(value, TRIGGER_MODES, "trigger mode") for value in values] 1205 self._write_command( 1206 self._OP_SET_TRIGGER_MODE, 1207 bytes(_TRIGGER_MODE_CODES[name] for name in names), 1208 ) 1209 self._read_ack("setting trigger_mode") 1210 return names 1211 1212 # One op programs both trigger channels' links, so each list is sent 1213 # with the other's current values 1214 def _apply_trigger_channel1_links(self, values): 1215 links1 = [to_bool(v, "link_trigger_channel1") for v in values] 1216 self._send_trigger_links(links1, self._link_trigger_channel2[1:]) 1217 return links1 1218 1219 def _apply_trigger_channel2_links(self, values): 1220 links2 = [to_bool(v, "link_trigger_channel2") for v in values] 1221 self._send_trigger_links(self._link_trigger_channel1[1:], links2) 1222 return links2 1223 1224 def _set_trigger_links(self, links1, links2): 1225 """Program both trigger channels' links with one command.""" 1226 self._send_trigger_links(links1, links2) 1227 self._link_trigger_channel1._store(links1) 1228 self._link_trigger_channel2._store(links2) 1229 1230 def _send_trigger_links(self, links1, links2): 1231 self._write_command(self._OP_SET_TRIGGER_LINKS, 1232 bytes([*map(int, links1), *map(int, links2)])) 1233 self._read_ack("setting the trigger channel links") 1234 1235 def _actual_rate(self, rate_hz): 1236 # The sample period is the nearest whole number of clock cycles, computed in integers as the 1237 # firmware's setSamplingRate() does. A rate halfway between two (e.g. 3072 Hz: 7812.5 cycles) 1238 # rounds up there, where round() would round to even. 1239 clock = self.info.sample_clock_hz 1240 return clock / ((clock + rate_hz // 2) // rate_hz) 1241 1242 def _normalize_sampling_rate(self, rate): 1243 """Check a sampling rate, and return it as a whole number of Hz.""" 1244 try: 1245 rate_hz = int(rate) 1246 is_whole = rate_hz == rate and not isinstance(rate, (bool, str)) 1247 except (TypeError, ValueError, OverflowError): 1248 is_whole = False 1249 if not is_whole or not 1 <= rate_hz <= self.info.max_sampling_rate: 1250 raise PulsePalError( 1251 "sampling_rate must be a whole number of Hz from 1 to " 1252 f"{self.info.max_sampling_rate}. Received {rate!r}." 1253 ) 1254 return rate_hz 1255 1256 def _check_output_range(self, range_name, trigger_modes): 1257 """Check that the output range can be set, with these trigger modes, and return its canonical name.""" 1258 name = self._range_name(range_name) 1259 low, high = OUTPUT_RANGES[name] 1260 for number, waveform in enumerate(self._waveforms): 1261 if waveform is not None and ( 1262 waveform.min() < low or waveform.max() > high): 1263 raise PulsePalError( 1264 f"Waveform {number} spans " 1265 f"{waveform.min():g} V to {waveform.max():g} V, which " 1266 f"does not fit the {name} range. Load a new waveform " 1267 "first, or choose a wider range." 1268 ) 1269 if name != self._output_range and "Param Sync" in trigger_modes: 1270 raise PulsePalError( 1271 "output_range cannot change while a trigger channel is in param sync mode: the waveforms are " 1272 "loaded again for the new range, and in param sync mode they would wait for a TTL edge. Set " 1273 "trigger_mode to another mode first." 1274 ) 1275 return name 1276 1277 def _range_name(self, range_name): 1278 """Return the canonical name of an output range.""" 1279 if isinstance(range_name, str): 1280 for name in OUTPUT_RANGES: 1281 if name.lower() == range_name.replace(" ", "").lower(): 1282 return name 1283 raise PulsePalError( 1284 f"Unknown output range: {range_name!r}. Valid ranges are " 1285 f"{', '.join(OUTPUT_RANGES)}." 1286 ) 1287 1288 def _volts_to_codes(self, volts): 1289 """Convert voltages to DAC codes in the current output range.""" 1290 low, high = OUTPUT_RANGES[self._output_range] 1291 volts = np.asarray(volts, dtype=float) 1292 if not np.all(np.isfinite(volts)): 1293 raise PulsePalError("Voltages must be finite numbers.") 1294 if volts.min() < low or volts.max() > high: 1295 raise PulsePalError( 1296 f"Voltages must be within the output range, {low:g} V to " 1297 f"{high:g} V. Received {volts.min():g} V to " 1298 f"{volts.max():g} V. Change output_range to use a wider " 1299 "range." 1300 ) 1301 codes = np.round((volts - low) / (high - low) * self._DAC_BITMAX) 1302 return codes.astype("<u2") 1303 1304 def _waveform_number(self, number): 1305 if ( 1306 not isinstance(number, numbers.Integral) 1307 or isinstance(number, (bool, np.bool_)) 1308 or not 1 <= number <= self.info.n_waveforms 1309 ): 1310 raise PulsePalError( 1311 f"Waveforms are numbered 1-{self.info.n_waveforms}. Received {number!r}." 1312 ) 1313 return int(number) 1314 1315 def _write_command(self, op_code, data=b""): 1316 """Send one command, with its framing byte, in a single write. 1317 1318 A command whose reply was not read in full, because the read timed out or was interrupted (Ctrl-C), leaves 1319 that reply, or the rest of it, to arrive later. Read as the reply to the next command, it would put every 1320 reply after it out of step. So it is discarded first (_discard_lost_reply()), and this command's reply, if it 1321 has one, is pending until _read_raw() has read it, as in PulsePalDevice. 1322 """ 1323 if self._reply_pending is not None: 1324 self._discard_lost_reply() 1325 if op_code not in _OPS_WITHOUT_TRACKED_REPLY: 1326 self._reply_pending = op_code # Before the write: a Ctrl-C can come as soon as it returns 1327 message = bytes([self._OP_MENU_BYTE, op_code]) + data 1328 bytes_written = self.port.write(message) 1329 if bytes_written != len(message): 1330 raise PulsePalError( 1331 f"Wrote {bytes_written} byte(s), expected to write " 1332 f"{len(message)} byte(s)." 1333 ) 1334 1335 def _discard_lost_reply(self): 1336 """Discard what arrives of a reply that was not read in full (see _write_command()), and bring the device 1337 back in step with this object. 1338 1339 The command may have changed a setting, unknown to this object: an assignment interrupted after its write, 1340 for one, which the device took and this object did not. Wave Pal cannot report its settings, so the setting 1341 such a command carried is sent again as this object holds it. A change of output range unloads the device's 1342 waveforms, so after one they are loaded again too. A waveform whose load was lost is already left out of 1343 `waveforms`. 1344 """ 1345 op = self._reply_pending 1346 self._reply_pending = None 1347 n_late = len(_common.read_until_quiet(self.port)) 1348 send_again = op in self._SETTING_OPS 1349 _common.warn_lost_reply(f"op {chr(op)!r}", n_late, 1350 ", and the setting it carried was sent again as this object holds it" if send_again 1351 else "") 1352 if send_again: 1353 try: 1354 self._send_setting_again(op) 1355 except PulsePalError as exc: 1356 raise PulsePalError(f"After the reply to an earlier command was lost: {exc}") from exc 1357 1358 def _send_setting_again(self, op): 1359 """Send the setting that op carries, as this object holds it (see _discard_lost_reply()).""" 1360 if op == self._OP_SET_SAMPLING_RATE: 1361 self._write_command(op, struct.pack("<I", self._sampling_rate)) 1362 self._read_ack("setting sampling_rate") 1363 elif op == self._OP_SET_OUTPUT_RANGE: 1364 self._write_command(op, bytes([list(OUTPUT_RANGES).index(self._output_range)])) 1365 self._read_ack("setting output_range") 1366 waveforms = self._waveforms 1367 self._waveforms = [None] * len(waveforms) 1368 for number, waveform in enumerate(waveforms): 1369 if waveform is not None: 1370 self.load_waveform(number, waveform) 1371 elif op == self._OP_SET_CURRENT_WAVEFORMS: 1372 self._write_command(op, bytes(self._current_waveform[1:])) 1373 self._read_ack("setting current_waveform") 1374 elif op == self._OP_SET_LOOP_MODE: 1375 self._write_command(op, bytes(map(int, self._loop_mode[1:]))) 1376 self._read_ack("setting loop_mode") 1377 elif op == self._OP_SET_LOOP_DURATION: 1378 self._send_loop_duration_samples(self._durations_to_samples(self._loop_duration[1:], self._sampling_rate)) 1379 elif op == self._OP_SET_TRIGGER_MODE: 1380 self._write_command(op, bytes(_TRIGGER_MODE_CODES[name] for name in self._trigger_mode[1:])) 1381 self._read_ack("setting trigger_mode") 1382 else: # Trigger links 1383 self._send_trigger_links(self._link_trigger_channel1[1:], self._link_trigger_channel2[1:]) 1384 1385 def _read_raw(self, n_bytes): 1386 """Read exactly n_bytes from the serial port: the whole reply to the command last sent, which is then no 1387 longer pending (see _write_command()). Only connecting reads a reply in more than one call.""" 1388 message = self.port.read(n_bytes) 1389 if len(message) < n_bytes: 1390 raise PulsePalError( 1391 f"Wave Pal sent {len(message)} of the {n_bytes} byte(s) of its reply within " 1392 f"{self.port.timeout} s. {_common.NO_REPLY_HINT}" 1393 ) 1394 self._reply_pending = None 1395 return message 1396 1397 def _read_ack(self, context): 1398 """Read a one-byte confirmation: 1 if the device executed the 1399 command, 0 if it rejected it.""" 1400 try: 1401 reply = self._read_raw(1)[0] 1402 except PulsePalError as exc: 1403 raise PulsePalError( 1404 f"Wave Pal did not confirm {context}: no reply arrived within {self.port.timeout} s. " 1405 f"{_common.NO_REPLY_HINT}" 1406 ) from exc 1407 if reply != 1: 1408 raise PulsePalError( 1409 f"Wave Pal rejected {context}. A value was out of range, " 1410 "or a waveform could not be written to the microSD card." 1411 )
264class WavePalDevice: 265 """A class to control a Wave Pal on a USB serial port. 266 267 Creating an instance opens the serial port, checks that the device 268 runs Wave Pal firmware, reads its properties into 269 `WavePalDevice.info`, shows "PYTHON Connected" on the device's 270 screen, stops any playback and programs the default settings (see 271 `WavePalDevice.set_default_params`). 272 273 ```python 274 from pulsepal import WavePalDevice 275 276 W = WavePalDevice("COM3") 277 W.load_waveform(1, [0, 1, 2, 3, 4, 5, 0], 1) # waveform 1, played by channel 1 278 W.trigger(1) 279 W.close() 280 ``` 281 282 Replace "COM3" with the device's USB serial port name, which 283 `WavePalDevice.serialportlist` lists. `WavePalDevice` is also a 284 context manager, which closes the port on exit. 285 286 Closing the connection stops playback, as it does on Pulse Pal, and 287 puts the device's own name back on its screen. The device keeps its 288 settings and waveforms, so TTL triggers still play them. 289 """ 290 291 port: "serial.Serial" 292 """The open `serial.Serial` port connected to the device.""" 293 294 info: DeviceInfo 295 """Properties of the connected device. See `DeviceInfo`.""" 296 297 _CURRENT_FIRMWARE_VERSION = 1 298 _BAUD_RATE = 12000000 # USB serial ignores the baud rate 299 300 _OP_MENU_BYTE = _common.OP_MENU_BYTE 301 _OP_HANDSHAKE = _common.OP_HANDSHAKE 302 _OP_DISCONNECT = 81 303 _OP_SET_CLIENT_NAME = 89 304 _OP_SET_SCREEN_SAVER = 99 305 _OP_HARDWARE_REVISION = 100 306 _OP_HARDWARE_INFO = ord("N") 307 _OP_SET_SAMPLING_RATE = ord("S") 308 _OP_SET_OUTPUT_RANGE = ord("R") 309 _OP_LOAD_WAVEFORM = ord("L") 310 _OP_SET_CURRENT_WAVEFORMS = ord("C") 311 _OP_TRIGGER = ord("P") 312 _OP_STOP = ord("X") 313 _OP_SET_FIXED_VOLTAGE = ord("!") 314 _OP_SET_LOOP_MODE = ord("O") 315 _OP_SET_LOOP_DURATION = ord("D") 316 _OP_SET_TRIGGER_MODE = ord("T") 317 _OP_SET_TRIGGER_LINKS = ord("I") 318 _OP_GET_STATUS = ord("G") 319 _OP_GET_PLAYBACK_CHECKSUMS = ord("Z") 320 321 _HANDSHAKE_REPLY = 87 # 'W' 322 _HARDWARE_INFO_FORMAT = "<BBIIIIB" 323 _STATUS_FORMAT = "<B4I4II4BBB4B" 324 _DAC_BITMAX = 65535 325 _UINT32_MAX = 2**32 - 1 326 _ALL_CHANNELS = 0x0F 327 # The ops that carry a setting, which _discard_lost_reply() sends again if the reply to one is lost 328 _SETTING_OPS = frozenset(map(ord, "SRCODTI")) 329 330 _reply_pending = None # The op code of a command whose reply has not been read in full yet (see _write_command()) 331 332 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 333 """Open a connection to a Wave Pal. 334 335 Args: 336 port_name: USB serial port of the device, such as `COM3` on 337 Windows or `/dev/ttyACM0` on Linux. 338 serial_number: Instead of `port_name`, the device's USB serial 339 number (`DeviceInfo.serial_number`), which finds its port 340 wherever it is plugged in. 341 timeout: Serial read timeout, in seconds. Loading a long 342 waveform onto a slow microSD card can take a few seconds. 343 344 Firmware newer than this module knows is used with a warning: new 345 firmware only adds commands. Update the package to use what is new. 346 347 Raises: 348 PulsePalError: If the device does not reply to the handshake, or 349 runs other firmware. 350 serial.SerialException: If the serial port cannot be opened, 351 e.g. because another program has it open (on Linux and 352 macOS, another session of these classes, which lock it). 353 """ 354 self._closed = True 355 self.info = DeviceInfo() 356 self._sampling_rate = None 357 self._output_range = None 358 self._waveforms = [] # Indexed by waveform number, once the device has said how many it stores 359 self._current_waveform = ChannelSettings( 360 "current_waveform", [0] * 4, self, "_apply_current_waveform") 361 self._loop_mode = ChannelSettings( 362 "loop_mode", [False] * 4, self, "_apply_loop_mode") 363 self._loop_duration = ChannelSettings( 364 "loop_duration", [0.0] * 4, self, "_apply_loop_duration") 365 self._trigger_mode = ChannelSettings( 366 "trigger_mode", ["Normal"] * 2, self, "_apply_trigger_mode") 367 self._link_trigger_channel1 = ChannelSettings( 368 "link_trigger_channel1", [False] * 4, self, 369 "_apply_trigger_channel1_links") 370 self._link_trigger_channel2 = ChannelSettings( 371 "link_trigger_channel2", [False] * 4, self, 372 "_apply_trigger_channel2_links") 373 374 port_name = _common.port_name_from(port_name, serial_number, "WavePalDevice") 375 self.port = serial.Serial( 376 port_name, 377 self._BAUD_RATE, 378 timeout=timeout, 379 rtscts=True, 380 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 381 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 382 # it, such as another of these classes, gets an error instead 383 exclusive=True, 384 ) 385 self._closed = False 386 try: 387 # Discard anything left in the buffer by an earlier session 388 self.port.reset_input_buffer() 389 self._handshake() 390 self._read_hardware_info() 391 self._waveforms = [None] * (self.info.n_waveforms + 1) 392 # Client name op + "PYTHON" in ASCII, shown as "PYTHON Connected" 393 self._write_command(self._OP_SET_CLIENT_NAME, b"PYTHON") 394 self.stop() 395 self.set_default_params() 396 except BaseException: 397 # Op 81 means something else to other devices, so it is sent 398 # only once the device has identified itself as a Wave Pal 399 self._close(send_disconnect=self.info.firmware_version is not None) 400 raise 401 402 @staticmethod 403 def serialportlist(ports_to_list="available"): 404 """Return the names of the USB serial ports on this computer. 405 406 Called on the class, without connecting to a device, to find the 407 port name to pass to `WavePalDevice`. 408 409 Args: 410 ports_to_list: `available` to list only the ports that are 411 not already in use, or `all` to list every USB serial 412 port. Not case sensitive. 413 414 Returns: 415 Sorted list of port names, such as `["COM3", "COM7"]`. 416 417 Raises: 418 PulsePalError: If `ports_to_list` is not `available` or `all`. 419 """ 420 return _common.serialportlist(ports_to_list) 421 422 # ------------------------------------------------------------------ 423 # Settings 424 # ------------------------------------------------------------------ 425 426 def set_default_params(self): 427 """Program the default settings on the device. 428 429 The defaults are both trigger channels in normal mode, a 10 kHz 430 sampling rate, the -10 V to 10 V output range, loop mode off with 431 loop durations of 0, no output channel linked to a trigger channel 432 (so a TTL starts nothing until a script links it), and no current 433 waveform on any channel. They match the settings the device starts with. The 434 trigger modes are sent first: a trigger channel leaving param sync 435 mode makes what was stored for its next edge current, and the 436 settings after it then apply at once. 437 438 Loaded waveforms are kept, and loaded again if the output range 439 changes (see `WavePalDevice.output_range`). 440 441 Raises: 442 PulsePalError: If a loaded waveform does not fit the default 443 output range, -10 V to 10 V. 444 """ 445 self.trigger_mode = ["Normal"] * 2 446 self.sampling_rate = 10000 447 self.output_range = "-10V:10V" 448 self.loop_mode = [False] * 4 449 self.loop_duration = [0] * 4 450 self._set_trigger_links([False] * 4, [False] * 4) 451 self.current_waveform = [0] * 4 452 453 @property 454 def sampling_rate(self): 455 """Sampling rate of all output channels, in Hz. 456 457 A whole number from 1 to `DeviceInfo.max_sampling_rate`. It can be 458 changed during playback. The rate played can differ slightly from 459 the rate set: see `WavePalDevice.actual_sampling_rate`. 460 """ 461 return self._sampling_rate 462 463 @sampling_rate.setter 464 def sampling_rate(self, rate): 465 rate_hz = self._normalize_sampling_rate(rate) 466 # Loop durations are sent in samples, so check that they still fit 467 # before anything is changed 468 loop_samples = None 469 if self._sampling_rate is not None: 470 loop_samples = self._durations_to_samples( 471 self._loop_duration[1:], rate_hz) 472 self._write_command(self._OP_SET_SAMPLING_RATE, 473 struct.pack("<I", rate_hz)) 474 self._read_ack("setting sampling_rate") 475 self._sampling_rate = rate_hz 476 if loop_samples is not None: 477 self._send_loop_duration_samples(loop_samples) 478 479 @property 480 def actual_sampling_rate(self): 481 """The sampling rate the device plays, in Hz. 482 483 The device divides `DeviceInfo.sample_clock_hz` (24 MHz) by a 484 whole number, so the rate played is the nearest one of those to 485 `WavePalDevice.sampling_rate`. For example, 44100 Hz plays at 486 44117.6 Hz. Rates that divide 24 MHz exactly, such as 10 kHz, 487 25 kHz or 100 kHz, play exactly. 488 """ 489 return self._actual_rate(self._sampling_rate) 490 491 @property 492 def output_range(self): 493 """Voltage range of all output channels. 494 495 One of `DeviceInfo.output_ranges`: `"0V:5V"`, `"0V:10V"`, 496 `"-5V:5V"` or `"-10V:10V"`. The smallest range that fits the 497 waveforms gives the finest voltage steps. 498 499 Changing the range stops playback, sets all outputs to 0 V, and 500 loads the waveforms again, encoded for the new range. It raises 501 an error, and changes nothing, if a loaded waveform does not fit 502 the new range, or if a trigger channel is in param sync mode, 503 where the loads would wait for a TTL edge. 504 """ 505 return self._output_range 506 507 @output_range.setter 508 def output_range(self, range_name): 509 name = self._check_output_range(range_name, self._trigger_mode[1:]) 510 changed = name != self._output_range 511 range_index = list(OUTPUT_RANGES).index(name) 512 self._write_command(self._OP_SET_OUTPUT_RANGE, bytes([range_index])) 513 self._read_ack("setting output_range") 514 self._output_range = name 515 if changed: 516 # The device unloads the waveforms when the range changes, 517 # because their samples encode voltages in the old range. The 518 # channels keep their current waveforms. 519 waveforms = self._waveforms 520 self._waveforms = [None] * len(waveforms) 521 for number, waveform in enumerate(waveforms): 522 if waveform is not None: 523 self.load_waveform(number, waveform) 524 525 @property 526 def current_waveform(self): 527 """The waveform each output channel plays, by waveform number. 528 529 Indexed by channel number (see "Channel settings" above). Each 530 value is the number of a waveform loaded with 531 `WavePalDevice.load_waveform`, 1 to `DeviceInfo.n_waveforms`, or 532 `0` for none, the default: a channel with no waveform ignores 533 triggers. Several channels can play the same waveform. 534 535 A channel that is playing finishes its waveform: the new one plays 536 from the channel's next start. In param sync mode, a change waits 537 for the next rising edge on the param sync trigger channel (see 538 `WavePalDevice.trigger_mode`). 539 540 ```python 541 W.current_waveform[1] = 2 # channel 1 plays waveform 2 542 W.current_waveform = [1, 1, 1, 1] # all four play waveform 1 543 ``` 544 """ 545 return self._current_waveform 546 547 @current_waveform.setter 548 def current_waveform(self, values): 549 self._current_waveform._assign(values) 550 551 @property 552 def loop_mode(self): 553 """Whether each output channel loops its waveform. 554 555 Indexed by channel number (see "Channel settings" above). `True` 556 repeats the waveform until `WavePalDevice.loop_duration` has 557 elapsed, or until stopped if the loop duration is 0. `False` plays 558 it once. A change applies to playback in progress. 559 """ 560 return self._loop_mode 561 562 @loop_mode.setter 563 def loop_mode(self, values): 564 self._loop_mode._assign(values) 565 566 @property 567 def loop_duration(self): 568 """How long each output channel loops its waveform, in seconds. 569 570 Indexed by channel number (see "Channel settings" above). Applies 571 in loop mode only, and counts from the trigger. The channel stops 572 when it has elapsed, part way through the waveform if need be. `0` 573 loops until the channel is stopped. Durations are rounded to a 574 whole number of samples, and a nonzero duration lasts at least 575 one sample. 576 """ 577 return self._loop_duration 578 579 @loop_duration.setter 580 def loop_duration(self, values): 581 self._loop_duration._assign(values) 582 583 @property 584 def trigger_mode(self): 585 """How each trigger channel acts on the output channels linked to it. 586 587 Indexed by trigger channel number, 1 or 2 (index 0 is unused), as 588 in Pulse Pal. A TTL edge on a trigger channel acts on the output 589 channels linked to it (`WavePalDevice.link_trigger_channel1`, 590 `WavePalDevice.link_trigger_channel2`): 591 592 - `"Normal"`: a rising edge starts the linked channels' waveforms. 593 Channels that are playing ignore it. 594 - `"Toggle"`: a rising edge starts the linked channels, or stops 595 those that are playing. 596 - `"Gated"`: a rising edge starts the linked channels, and a 597 falling edge stops them, unless the other trigger channel is also 598 gated, linked to them, and still high. With loop mode on and a 599 loop duration of 0, a waveform plays for exactly as long as the 600 TTL is high. 601 - `"Param Sync"`: a rising edge starts and stops nothing (the 602 channel's links are ignored). It makes the waveforms loaded, 603 and the `WavePalDevice.current_waveform` set, since the previous 604 edge current. This is how the next trial's waveforms are loaded 605 during the current trial, and played from the instant it starts. 606 - `"Master"`: a rising edge starts the linked channels, and 607 restarts those that are playing from their first sample. 608 609 The first four are Pulse Pal's trigger modes. Names are not case 610 sensitive. 611 612 While a trigger channel is in param sync mode, 613 `WavePalDevice.load_waveform` loads into a second memory block 614 that the device keeps for each waveform. Channels go on playing 615 the waveform it replaces, and triggers still start it, until the 616 edge. A channel that is playing at the edge finishes what it plays, 617 and plays the new waveform from its next start. A later load of the 618 same waveform before the edge replaces the earlier one, and stops 619 any channel still playing the waveform from before the previous 620 edge, since the device reuses that memory block. Changes to 621 `WavePalDevice.current_waveform` wait for the edge in the same 622 way. Other settings apply at once. 623 624 When neither trigger channel is in param sync mode any more, what 625 was waiting for an edge becomes current at once, so 626 `WavePalDevice.waveforms` and `WavePalDevice.current_waveform` 627 describe the device again. (Pulse Pal and Synth Pal discard their 628 stored parameters and settings then; Pulse Pal makes its stored 629 custom trains current, as this class does its waveforms.) 630 `WavePalDevice.output_range` cannot change 631 in param sync mode. To make the waveforms current on the same edge 632 that starts the channels, wire the TTL to the other trigger channel 633 as well: the param sync edge is handled first. 634 635 ```python 636 W.trigger_mode[2] = "Param Sync" # sent at once 637 W.load_waveform(1, next_trial_wave) # waits for trigger channel 2's next rising edge 638 ``` 639 """ 640 return self._trigger_mode 641 642 @trigger_mode.setter 643 def trigger_mode(self, values): 644 self._trigger_mode._assign(values) 645 646 @property 647 def link_trigger_channel1(self): 648 """Whether each output channel is triggered by trigger channel 1. 649 650 Indexed by channel number (see "Channel settings" above). By 651 default no output channel is linked to either trigger channel, so 652 a TTL starts nothing until a script links it. 653 """ 654 return self._link_trigger_channel1 655 656 @link_trigger_channel1.setter 657 def link_trigger_channel1(self, values): 658 self._link_trigger_channel1._assign(values) 659 660 @property 661 def link_trigger_channel2(self): 662 """Whether each output channel is triggered by trigger channel 2. 663 664 Indexed by channel number (see "Channel settings" above). 665 """ 666 return self._link_trigger_channel2 667 668 @link_trigger_channel2.setter 669 def link_trigger_channel2(self, values): 670 self._link_trigger_channel2._assign(values) 671 672 def set_screen_saver(self, enabled, timeout=1800): 673 """Switch the device's screen saver on or off, and set its timeout. 674 675 With the screen saver on, the device dims its screen once it has 676 been left alone for `timeout` seconds: no command from a computer, 677 no rising edge on a trigger channel, and no joystick click or 678 push. The next of these brings the screen back. The device keeps 679 both settings in its EEPROM, shared with Pulse Pal and Synth Pal 680 firmware, and saves them once no channel is playing. A new device 681 starts with the screen saver on and 1800 s. 682 683 Args: 684 enabled: `True` to switch the screen saver on, `False` to 685 switch it off. 686 timeout: Seconds without activity before the screen dims, 1 to 687 65535. It is sent with every call, so leaving it out sets 688 1800 s. 689 690 Raises: 691 PulsePalError: If a value is out of range. 692 """ 693 state = to_bool(enabled, "enabled") 694 if isinstance(timeout, (bool, np.bool_)) \ 695 or not isinstance(timeout, numbers.Integral) \ 696 or not 1 <= timeout <= 65535: 697 raise PulsePalError( 698 "timeout must be a whole number of seconds from 1 to 65535. " 699 f"Received {timeout!r}." 700 ) 701 self._write_command(self._OP_SET_SCREEN_SAVER, 702 struct.pack("<BH", int(state), int(timeout))) 703 self._read_ack("set_screen_saver()") 704 705 def export_params(self): 706 """Return every setting, as a dict of plain values and lists. 707 708 Keyed by setting name: `"sampling_rate"` and `"output_range"`, then 709 the channel settings, each with one value per channel and no 710 unused index 0. It holds only numbers, booleans and names, so it 711 can be saved with `json` and logged with your data. The waveforms 712 are not part of it: `WavePalDevice.waveforms` holds them. 713 714 ```python 715 import json 716 with open("session_settings.json", "w") as f: 717 json.dump(W.export_params(), f) 718 ``` 719 """ 720 params = {"sampling_rate": self._sampling_rate, "output_range": self._output_range} 721 for name in _CHANNEL_SETTINGS: 722 params[name] = list(getattr(self, name))[1:] 723 return params 724 725 def import_params(self, params): 726 """Program the device with settings exported by `WavePalDevice.export_params`. 727 728 All of them are checked first, and nothing is sent if one is 729 invalid. They are then sent in an order that lets each take 730 effect: a trigger channel leaving param sync mode first, then the 731 sampling rate, the output range and the channel settings, and a 732 trigger channel entering param sync mode last. So 733 `WavePalDevice.current_waveform` applies at once, unless a trigger 734 channel was in param sync mode and stays in it: then it waits for 735 the next edge, as an assignment would. Settings missing from 736 `params` keep their values. A name that is not a setting is 737 skipped with a warning, so that settings exported by a newer 738 version of this package, which may have more, can still be 739 imported. 740 741 The waveforms are not settings: load them with 742 `WavePalDevice.load_waveform` first, since a current waveform must 743 be a loaded one. 744 745 ```python 746 import json 747 with open("session_settings.json") as f: 748 W.import_params(json.load(f)) 749 ``` 750 751 Args: 752 params: A dict of setting name to its value: a number for 753 `"sampling_rate"`, a name for `"output_range"`, and one 754 value per channel (a list, tuple or NumPy array) for the 755 others, as `export_params` returns. 756 757 Raises: 758 PulsePalError: If a value is invalid (nothing is sent), or the 759 device rejects a setting. 760 """ 761 names = {"sampling_rate", "output_range", *_CHANNEL_SETTINGS} 762 unknown = [name for name in params if name not in names] 763 if unknown: 764 warnings.warn(f"import_params(): skipped {', '.join(map(repr, unknown))}, which this version of " 765 "WavePalDevice does not have.", stacklevel=2) 766 new = {} 767 for name in _CHANNEL_SETTINGS: 768 if name in params: 769 n = 2 if name == "trigger_mode" else self.info.n_channels 770 new[name] = _common.one_value_per_channel(name, params[name], n) 771 if "current_waveform" in new: 772 new["current_waveform"] = self._normalize_current_waveform(new["current_waveform"]) 773 if "loop_mode" in new: 774 new["loop_mode"] = [to_bool(value, "loop_mode") for value in new["loop_mode"]] 775 if "loop_duration" in new: 776 new["loop_duration"] = self._normalize_loop_durations(new["loop_duration"]) 777 for name in ("link_trigger_channel1", "link_trigger_channel2"): 778 if name in new: 779 new[name] = [to_bool(value, name) for value in new[name]] 780 modes = list(self._trigger_mode)[1:] 781 if "trigger_mode" in new: 782 modes = [to_name(value, TRIGGER_MODES, "trigger mode") for value in new["trigger_mode"]] 783 # The trigger modes the other settings are sent in: those of channels leaving param sync mode are sent 784 # first, those of channels entering it last 785 first_modes = [new_mode if (old == "Param Sync" and new_mode != "Param Sync") else old 786 for old, new_mode in zip(self._trigger_mode[1:], modes)] 787 rate = self._sampling_rate 788 if "sampling_rate" in params: 789 rate = self._normalize_sampling_rate(params["sampling_rate"]) 790 range_name = self._output_range 791 if "output_range" in params: 792 range_name = self._check_output_range(params["output_range"], first_modes) 793 self._durations_to_samples(new.get("loop_duration", self._loop_duration[1:]), rate) # They fit at the rate 794 795 if first_modes != list(self._trigger_mode)[1:]: 796 self.trigger_mode = first_modes 797 if rate != self._sampling_rate: 798 self.sampling_rate = rate 799 if range_name != self._output_range: 800 self.output_range = range_name 801 for name in ("loop_mode", "loop_duration"): 802 if name in new: 803 setattr(self, name, new[name]) 804 if "link_trigger_channel1" in new or "link_trigger_channel2" in new: 805 self._set_trigger_links(new.get("link_trigger_channel1", self._link_trigger_channel1[1:]), 806 new.get("link_trigger_channel2", self._link_trigger_channel2[1:])) 807 if "current_waveform" in new: 808 self.current_waveform = new["current_waveform"] 809 if modes != first_modes: 810 self.trigger_mode = modes 811 812 @property 813 def waveforms(self): 814 """The waveforms loaded with `WavePalDevice.load_waveform`. 815 816 A list indexed by waveform number, 1 to `DeviceInfo.n_waveforms`, 817 with index 0 unused. Each element is a read-only NumPy array of 818 voltages, or `None` if this object has not loaded that waveform. In 819 param sync mode, it holds the waveforms loaded since the last edge 820 before the device plays them. `WavePalDevice.status` shows what 821 the device itself holds. 822 """ 823 return list(self._waveforms) 824 825 # ------------------------------------------------------------------ 826 # Waveforms and playback 827 # ------------------------------------------------------------------ 828 829 def load_waveform(self, waveform_number, waveform, channels=None): 830 """Load one of the device's waveforms, and choose the channels that play it. 831 832 The samples are written to the device's microSD card, and the 833 first `DeviceInfo.buffer_samples` of them are also kept in its RAM. 834 Outside param sync mode, loading stops the channels playing that 835 waveform, and the new one plays from their next start. Other 836 channels keep playing. In param sync mode, the new waveform waits 837 for the next rising edge on the param sync trigger channel, and 838 channels go on playing the old one until then (see 839 `WavePalDevice.trigger_mode`). 840 841 ```python 842 t = np.arange(10000) / W.sampling_rate 843 W.load_waveform(2, 3 * np.sin(2 * np.pi * 100 * t), [1, 3]) 844 ``` 845 846 Args: 847 waveform_number: The waveform to load, 1 to 848 `DeviceInfo.n_waveforms`. 849 waveform: Voltages of the samples, played at 850 `WavePalDevice.sampling_rate`. A list, tuple or NumPy 851 array of 1 to `DeviceInfo.max_samples` values, all within 852 `WavePalDevice.output_range`. 853 channels: Output channels that play this waveform, from their 854 next start: one channel number 1-4, or several as a list, 855 tuple or NumPy array. It sets their 856 `WavePalDevice.current_waveform`, and leaves the other 857 channels' alone, so channels already playing this waveform 858 play the new one. `None`, the default, changes no channel. 859 860 Raises: 861 PulsePalError: If an argument is invalid, or the device could 862 not store the waveform. The waveform is then left out of 863 `WavePalDevice.waveforms`: outside param sync mode the 864 device has unloaded it, and in param sync mode it plays the 865 version made current at the last edge. 866 """ 867 number = self._waveform_number(waveform_number) 868 channel_list = None if channels is None else _common.channel_numbers(channels) 869 samples = np.array(waveform, dtype=float).ravel() 870 if not 1 <= samples.size <= self.info.max_samples: 871 raise PulsePalError( 872 f"A waveform must have 1 to {self.info.max_samples} samples. " 873 f"Received {samples.size}." 874 ) 875 codes = self._volts_to_codes(samples) 876 self._waveforms[number] = None # Until the device has stored it. Outside param sync mode, it unloads it first. 877 self._write_command( 878 self._OP_LOAD_WAVEFORM, 879 struct.pack("<BI", number, samples.size) + codes.tobytes(), 880 ) 881 self._read_ack("load_waveform()") 882 samples.flags.writeable = False 883 self._waveforms[number] = samples 884 if channel_list: 885 values = list(self._current_waveform[1:]) 886 for channel in channel_list: 887 values[channel - 1] = number 888 self._current_waveform._assign(values) 889 890 def trigger(self, channels): 891 """Trigger output channels in software. 892 893 Idle channels start their current waveforms from the first 894 sample, together. Channels that are playing ignore it, as Pulse 895 Pal's channels ignore a soft trigger while they play a pulse train. 896 897 ```python 898 W.trigger(1) 899 W.trigger([2, 4]) # a list, tuple or NumPy array 900 ``` 901 902 Args: 903 channels: Output channel number 1-4, or several as a list, 904 tuple or NumPy array. 905 906 Raises: 907 PulsePalError: If a channel has no waveform to play: its 908 `WavePalDevice.current_waveform` is 0, or a waveform this 909 object has not loaded. Nothing is sent. 910 """ 911 channel_list = _common.channel_numbers(channels) 912 for channel in channel_list: 913 number = self._current_waveform[channel] 914 if number == 0: 915 raise PulsePalError( 916 f"Channel {channel} has no waveform to play. Set current_waveform[{channel}] to a loaded " 917 f"waveform's number, or load one with load_waveform(number, waveform, {channel})." 918 ) 919 if self._waveforms[number] is None: 920 raise PulsePalError( 921 f"Channel {channel} plays waveform {number}, which is not loaded. Load it with " 922 f"load_waveform({number}, waveform)." 923 ) 924 self._write_command(self._OP_TRIGGER, bytes([_common.channel_bits(channel_list)])) 925 926 def stop(self, channels=None): 927 """Stop playback. The stopped channels output 0 V. 928 929 Args: 930 channels: Output channel number 1-4, or several as a list, 931 tuple or NumPy array. `None` stops all channels. 932 """ 933 bits = self._ALL_CHANNELS if channels is None else _common.channel_bits(channels) 934 self._write_command(self._OP_STOP, bytes([bits])) 935 936 def set_fixed_voltage(self, channels, voltage): 937 """Set output channels to a fixed voltage. 938 939 Stops playback on those channels. The voltage holds until the 940 channel is triggered or stopped. 941 942 Args: 943 channels: Output channel number 1-4, or several as a list, 944 tuple or NumPy array. 945 voltage: Voltage, within `WavePalDevice.output_range`. 946 947 Raises: 948 PulsePalError: If the voltage is outside the output range, or 949 the device rejects the command. 950 """ 951 bits = _common.channel_bits(channels) 952 code = int(self._volts_to_codes([voltage])[0]) 953 self._write_command(self._OP_SET_FIXED_VOLTAGE, 954 struct.pack("<BH", bits, code)) 955 self._read_ack("set_fixed_voltage()") 956 957 def status(self): 958 """Read the device's playback state. 959 960 Returns: 961 A `DeviceStatus`, with the channels that are playing, the 962 number of samples in each waveform, underrun counts, the 963 longest playback interrupt, each channel's current waveform, 964 and what waits for a param sync edge. 965 """ 966 self._write_command(self._OP_GET_STATUS) 967 values = struct.unpack( 968 self._STATUS_FORMAT, 969 self._read_raw(struct.calcsize(self._STATUS_FORMAT)), 970 ) 971 playing_bits = values[0] 972 stored_load_bits = values[14] 973 return DeviceStatus( 974 playing=[ch for ch in range(1, 5) 975 if playing_bits & (1 << (ch - 1))], 976 samples_loaded=[None, *values[1:5]], 977 underruns=[None, *values[5:9]], 978 longest_interrupt_us=values[9] / 1000, 979 current_waveform=[None, *values[10:14]], 980 stored_waveforms=[n for n in range(1, 5) if stored_load_bits & (1 << (n - 1))], 981 stored_current_waveform=[None, *values[16:20]] if values[15] else None, 982 ) 983 984 def _playback_checksums(self): 985 """For testing: what each channel played since it last started. 986 987 Returns two lists indexed by channel number, with index 0 unused: 988 the number of samples played, and the sum of their DAC codes 989 modulo 2**32. `tests/wavepal_hardware_test.py` compares them with 990 the waveforms it loaded, to check every sample the device played. 991 """ 992 self._write_command(self._OP_GET_PLAYBACK_CHECKSUMS) 993 values = struct.unpack("<8I", self._read_raw(32)) 994 return [None, *values[:4]], [None, *values[4:]] 995 996 # ------------------------------------------------------------------ 997 # Connection 998 # ------------------------------------------------------------------ 999 1000 def close(self): 1001 """Close the connection to the device. 1002 1003 The device stops playback on all channels, and shows its own name 1004 on its screen again, in place of "PYTHON Connected". It keeps its 1005 settings and waveforms, so TTL triggers still play them. Safe to 1006 call more than once. Called automatically when leaving a `with` 1007 block and when the object is garbage collected, so playback also 1008 stops when the last reference to the object goes. 1009 1010 """ 1011 self._close(send_disconnect=True) 1012 1013 def _close(self, send_disconnect): 1014 """Close the connection. send_disconnect: tell the device first (op 81). False when the device may not 1015 run this firmware: op 81 means something else to other devices.""" 1016 if getattr(self, "_closed", True): 1017 return 1018 self._closed = True 1019 self._reply_pending = None # Nothing more is read, so a reply still to come needs no discarding 1020 try: 1021 if send_disconnect and self.port and self.port.is_open: 1022 self._write_command(self._OP_DISCONNECT) 1023 except Exception: 1024 pass # The port may already be gone, e.g. the cable was unplugged 1025 finally: 1026 if self.port and self.port.is_open: 1027 self.port.close() 1028 1029 def __enter__(self): 1030 """Enter a `with` block, returning the connected device.""" 1031 return self 1032 1033 def __exit__(self, exc_type, exc_value, traceback): 1034 """Close the port when leaving a `with` block. 1035 1036 Returns: 1037 `False`, so any exception raised in the block propagates. 1038 """ 1039 self.close() 1040 return False 1041 1042 def __del__(self): 1043 """Close the port when the object is collected.""" 1044 try: 1045 self.close() 1046 except Exception: 1047 # Destructors should not raise; the serial object may already be 1048 # gone during interpreter shutdown. 1049 pass 1050 1051 def __setattr__(self, name, value): 1052 """Refuse a public attribute that is not a setting, such as a misspelled one: it would program nothing (see 1053 _common.refuse_unknown_attribute()).""" 1054 if name[0] != "_": 1055 _common.refuse_unknown_attribute(self, name, _SETTING_NAMES) 1056 super().__setattr__(name, value) 1057 1058 def __repr__(self): 1059 """Describe the device and its settings, e.g. with `print(W)`.""" 1060 port = getattr(getattr(self, "port", None), "port", None) 1061 loaded = ", ".join( 1062 f"{number}: {'none' if w is None else f'{w.size} samples'}" 1063 for number, w in enumerate(self._waveforms) if number > 0 1064 ) 1065 return ( 1066 f"WavePalDevice on {port} (Wave Pal firmware " 1067 f"v{self.info.firmware_version})\n" 1068 f"sampling_rate: {self._sampling_rate} Hz\n" 1069 f"output_range: {self._output_range}\n" 1070 f"current_waveform: {list(self._current_waveform)}\n" 1071 f"loop_mode: {list(self._loop_mode)}\n" 1072 f"loop_duration: {list(self._loop_duration)}\n" 1073 f"trigger_mode: {list(self._trigger_mode)}\n" 1074 f"link_trigger_channel1: {list(self._link_trigger_channel1)}\n" 1075 f"link_trigger_channel2: {list(self._link_trigger_channel2)}\n" 1076 f"waveforms: {loaded}" 1077 ) 1078 1079 # ------------------------------------------------------------------ 1080 # Internals 1081 # ------------------------------------------------------------------ 1082 1083 def _handshake(self): 1084 """Check that the device runs a supported Wave Pal firmware.""" 1085 self._write_command(self._OP_HANDSHAKE) 1086 handshake = _common.read_handshake_reply(self.port) # Skips a reply to an earlier session's command 1087 if handshake is None: 1088 raise PulsePalError( 1089 f"No reply from the device on {self.port.port}. Is it a " 1090 "Pulse Pal 3 running Wave Pal firmware?" 1091 f" {_common.NO_REPLY_HINT}" 1092 ) 1093 reply, version = handshake 1094 if reply != self._HANDSHAKE_REPLY: 1095 if reply in _common.FIRMWARE_BY_HANDSHAKE_REPLY: 1096 raise _common.other_firmware_error(reply, version, self.port.port, "Wave Pal") 1097 raise PulsePalError( 1098 "Incorrect handshake returned. Expected " 1099 f"{self._HANDSHAKE_REPLY}, received {reply}." 1100 ) 1101 if version > self._CURRENT_FIRMWARE_VERSION: 1102 # New firmware only adds commands (see PROTOCOL.md), so this class still works with it 1103 warnings.warn( 1104 f"Wave Pal firmware v{version} is newer than this pulsepal package knows " 1105 f"(v{self._CURRENT_FIRMWARE_VERSION}). It works with it, but update the package to use what is " 1106 "new.", stacklevel=3) 1107 self.info.firmware_version = version 1108 1109 def _read_hardware_info(self): 1110 self._write_command(self._OP_HARDWARE_INFO) 1111 ( 1112 self.info.hardware_version, 1113 self.info.n_channels, 1114 self.info.max_samples, 1115 self.info.max_sampling_rate, 1116 self.info.buffer_samples, 1117 self.info.sample_clock_hz, 1118 self.info.n_waveforms, 1119 ) = struct.unpack( 1120 self._HARDWARE_INFO_FORMAT, 1121 self._read_raw(struct.calcsize(self._HARDWARE_INFO_FORMAT)), 1122 ) 1123 self._write_command(self._OP_HARDWARE_REVISION) 1124 self.info.circuit_board_revision = self._read_raw(1)[0] 1125 self._write_command(_common.OP_SERIAL_NUMBER) 1126 self.info.serial_number = _common.read_serial_number(self._read_raw) 1127 1128 def _apply_current_waveform(self, values): 1129 waveform_numbers = self._normalize_current_waveform(values) 1130 self._write_command(self._OP_SET_CURRENT_WAVEFORMS, bytes(waveform_numbers)) 1131 self._read_ack("setting current_waveform") 1132 return waveform_numbers 1133 1134 def _normalize_current_waveform(self, values): 1135 """Check new current waveforms for all channels, and return them as the list holds them.""" 1136 waveform_numbers = [] 1137 for channel, value in enumerate(values, start=1): 1138 if (isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Integral) 1139 or not 0 <= value <= self.info.n_waveforms): 1140 raise PulsePalError( 1141 f"current_waveform values are waveform numbers, 1-{self.info.n_waveforms}, or 0 for none. " 1142 f"Received {value!r}." 1143 ) 1144 value = int(value) 1145 # A channel left on a waveform whose load failed is not refused here: trigger() refuses it 1146 if value != 0 and value != self._current_waveform[channel] and self._waveforms[value] is None: 1147 raise PulsePalError( 1148 f"Waveform {value} is not loaded. Load it with load_waveform({value}, waveform) first." 1149 ) 1150 waveform_numbers.append(value) 1151 return waveform_numbers 1152 1153 def _apply_loop_mode(self, values): 1154 modes = [to_bool(value, "loop_mode") for value in values] 1155 self._write_command(self._OP_SET_LOOP_MODE, bytes(map(int, modes))) 1156 self._read_ack("setting loop_mode") 1157 return modes 1158 1159 def _apply_loop_duration(self, values): 1160 durations = self._normalize_loop_durations(values) 1161 samples = self._durations_to_samples(durations, self._sampling_rate) 1162 self._send_loop_duration_samples(samples) 1163 return durations 1164 1165 def _normalize_loop_durations(self, values): 1166 """Check new loop durations for all channels, and return them as the list holds them, in seconds.""" 1167 durations = [] 1168 for value in values: 1169 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real): 1170 raise PulsePalError( 1171 f"loop_duration must be in seconds. Received {value!r}." 1172 ) 1173 duration = float(value) 1174 if not math.isfinite(duration) or duration < 0: 1175 raise PulsePalError( 1176 "loop_duration must be 0 (loop until stopped) or a " 1177 f"positive number of seconds. Received {value!r}." 1178 ) 1179 durations.append(duration) 1180 return durations 1181 1182 def _send_loop_duration_samples(self, samples): 1183 self._write_command(self._OP_SET_LOOP_DURATION, 1184 struct.pack("<4I", *samples)) 1185 self._read_ack("setting loop_duration") 1186 1187 def _durations_to_samples(self, durations, rate_hz): 1188 """Convert loop durations in seconds to samples at a sampling rate.""" 1189 actual_rate = self._actual_rate(rate_hz) 1190 samples = [] 1191 for duration in durations: 1192 n = round(duration * actual_rate) 1193 if duration > 0: 1194 n = max(n, 1) # 0 samples would mean "loop until stopped" 1195 if n > self._UINT32_MAX: 1196 raise PulsePalError( 1197 f"A loop_duration of {duration} s is too long at " 1198 f"{rate_hz} Hz. The longest is " 1199 f"{self._UINT32_MAX / actual_rate:.0f} s." 1200 ) 1201 samples.append(n) 1202 return samples 1203 1204 def _apply_trigger_mode(self, values): 1205 names = [to_name(value, TRIGGER_MODES, "trigger mode") for value in values] 1206 self._write_command( 1207 self._OP_SET_TRIGGER_MODE, 1208 bytes(_TRIGGER_MODE_CODES[name] for name in names), 1209 ) 1210 self._read_ack("setting trigger_mode") 1211 return names 1212 1213 # One op programs both trigger channels' links, so each list is sent 1214 # with the other's current values 1215 def _apply_trigger_channel1_links(self, values): 1216 links1 = [to_bool(v, "link_trigger_channel1") for v in values] 1217 self._send_trigger_links(links1, self._link_trigger_channel2[1:]) 1218 return links1 1219 1220 def _apply_trigger_channel2_links(self, values): 1221 links2 = [to_bool(v, "link_trigger_channel2") for v in values] 1222 self._send_trigger_links(self._link_trigger_channel1[1:], links2) 1223 return links2 1224 1225 def _set_trigger_links(self, links1, links2): 1226 """Program both trigger channels' links with one command.""" 1227 self._send_trigger_links(links1, links2) 1228 self._link_trigger_channel1._store(links1) 1229 self._link_trigger_channel2._store(links2) 1230 1231 def _send_trigger_links(self, links1, links2): 1232 self._write_command(self._OP_SET_TRIGGER_LINKS, 1233 bytes([*map(int, links1), *map(int, links2)])) 1234 self._read_ack("setting the trigger channel links") 1235 1236 def _actual_rate(self, rate_hz): 1237 # The sample period is the nearest whole number of clock cycles, computed in integers as the 1238 # firmware's setSamplingRate() does. A rate halfway between two (e.g. 3072 Hz: 7812.5 cycles) 1239 # rounds up there, where round() would round to even. 1240 clock = self.info.sample_clock_hz 1241 return clock / ((clock + rate_hz // 2) // rate_hz) 1242 1243 def _normalize_sampling_rate(self, rate): 1244 """Check a sampling rate, and return it as a whole number of Hz.""" 1245 try: 1246 rate_hz = int(rate) 1247 is_whole = rate_hz == rate and not isinstance(rate, (bool, str)) 1248 except (TypeError, ValueError, OverflowError): 1249 is_whole = False 1250 if not is_whole or not 1 <= rate_hz <= self.info.max_sampling_rate: 1251 raise PulsePalError( 1252 "sampling_rate must be a whole number of Hz from 1 to " 1253 f"{self.info.max_sampling_rate}. Received {rate!r}." 1254 ) 1255 return rate_hz 1256 1257 def _check_output_range(self, range_name, trigger_modes): 1258 """Check that the output range can be set, with these trigger modes, and return its canonical name.""" 1259 name = self._range_name(range_name) 1260 low, high = OUTPUT_RANGES[name] 1261 for number, waveform in enumerate(self._waveforms): 1262 if waveform is not None and ( 1263 waveform.min() < low or waveform.max() > high): 1264 raise PulsePalError( 1265 f"Waveform {number} spans " 1266 f"{waveform.min():g} V to {waveform.max():g} V, which " 1267 f"does not fit the {name} range. Load a new waveform " 1268 "first, or choose a wider range." 1269 ) 1270 if name != self._output_range and "Param Sync" in trigger_modes: 1271 raise PulsePalError( 1272 "output_range cannot change while a trigger channel is in param sync mode: the waveforms are " 1273 "loaded again for the new range, and in param sync mode they would wait for a TTL edge. Set " 1274 "trigger_mode to another mode first." 1275 ) 1276 return name 1277 1278 def _range_name(self, range_name): 1279 """Return the canonical name of an output range.""" 1280 if isinstance(range_name, str): 1281 for name in OUTPUT_RANGES: 1282 if name.lower() == range_name.replace(" ", "").lower(): 1283 return name 1284 raise PulsePalError( 1285 f"Unknown output range: {range_name!r}. Valid ranges are " 1286 f"{', '.join(OUTPUT_RANGES)}." 1287 ) 1288 1289 def _volts_to_codes(self, volts): 1290 """Convert voltages to DAC codes in the current output range.""" 1291 low, high = OUTPUT_RANGES[self._output_range] 1292 volts = np.asarray(volts, dtype=float) 1293 if not np.all(np.isfinite(volts)): 1294 raise PulsePalError("Voltages must be finite numbers.") 1295 if volts.min() < low or volts.max() > high: 1296 raise PulsePalError( 1297 f"Voltages must be within the output range, {low:g} V to " 1298 f"{high:g} V. Received {volts.min():g} V to " 1299 f"{volts.max():g} V. Change output_range to use a wider " 1300 "range." 1301 ) 1302 codes = np.round((volts - low) / (high - low) * self._DAC_BITMAX) 1303 return codes.astype("<u2") 1304 1305 def _waveform_number(self, number): 1306 if ( 1307 not isinstance(number, numbers.Integral) 1308 or isinstance(number, (bool, np.bool_)) 1309 or not 1 <= number <= self.info.n_waveforms 1310 ): 1311 raise PulsePalError( 1312 f"Waveforms are numbered 1-{self.info.n_waveforms}. Received {number!r}." 1313 ) 1314 return int(number) 1315 1316 def _write_command(self, op_code, data=b""): 1317 """Send one command, with its framing byte, in a single write. 1318 1319 A command whose reply was not read in full, because the read timed out or was interrupted (Ctrl-C), leaves 1320 that reply, or the rest of it, to arrive later. Read as the reply to the next command, it would put every 1321 reply after it out of step. So it is discarded first (_discard_lost_reply()), and this command's reply, if it 1322 has one, is pending until _read_raw() has read it, as in PulsePalDevice. 1323 """ 1324 if self._reply_pending is not None: 1325 self._discard_lost_reply() 1326 if op_code not in _OPS_WITHOUT_TRACKED_REPLY: 1327 self._reply_pending = op_code # Before the write: a Ctrl-C can come as soon as it returns 1328 message = bytes([self._OP_MENU_BYTE, op_code]) + data 1329 bytes_written = self.port.write(message) 1330 if bytes_written != len(message): 1331 raise PulsePalError( 1332 f"Wrote {bytes_written} byte(s), expected to write " 1333 f"{len(message)} byte(s)." 1334 ) 1335 1336 def _discard_lost_reply(self): 1337 """Discard what arrives of a reply that was not read in full (see _write_command()), and bring the device 1338 back in step with this object. 1339 1340 The command may have changed a setting, unknown to this object: an assignment interrupted after its write, 1341 for one, which the device took and this object did not. Wave Pal cannot report its settings, so the setting 1342 such a command carried is sent again as this object holds it. A change of output range unloads the device's 1343 waveforms, so after one they are loaded again too. A waveform whose load was lost is already left out of 1344 `waveforms`. 1345 """ 1346 op = self._reply_pending 1347 self._reply_pending = None 1348 n_late = len(_common.read_until_quiet(self.port)) 1349 send_again = op in self._SETTING_OPS 1350 _common.warn_lost_reply(f"op {chr(op)!r}", n_late, 1351 ", and the setting it carried was sent again as this object holds it" if send_again 1352 else "") 1353 if send_again: 1354 try: 1355 self._send_setting_again(op) 1356 except PulsePalError as exc: 1357 raise PulsePalError(f"After the reply to an earlier command was lost: {exc}") from exc 1358 1359 def _send_setting_again(self, op): 1360 """Send the setting that op carries, as this object holds it (see _discard_lost_reply()).""" 1361 if op == self._OP_SET_SAMPLING_RATE: 1362 self._write_command(op, struct.pack("<I", self._sampling_rate)) 1363 self._read_ack("setting sampling_rate") 1364 elif op == self._OP_SET_OUTPUT_RANGE: 1365 self._write_command(op, bytes([list(OUTPUT_RANGES).index(self._output_range)])) 1366 self._read_ack("setting output_range") 1367 waveforms = self._waveforms 1368 self._waveforms = [None] * len(waveforms) 1369 for number, waveform in enumerate(waveforms): 1370 if waveform is not None: 1371 self.load_waveform(number, waveform) 1372 elif op == self._OP_SET_CURRENT_WAVEFORMS: 1373 self._write_command(op, bytes(self._current_waveform[1:])) 1374 self._read_ack("setting current_waveform") 1375 elif op == self._OP_SET_LOOP_MODE: 1376 self._write_command(op, bytes(map(int, self._loop_mode[1:]))) 1377 self._read_ack("setting loop_mode") 1378 elif op == self._OP_SET_LOOP_DURATION: 1379 self._send_loop_duration_samples(self._durations_to_samples(self._loop_duration[1:], self._sampling_rate)) 1380 elif op == self._OP_SET_TRIGGER_MODE: 1381 self._write_command(op, bytes(_TRIGGER_MODE_CODES[name] for name in self._trigger_mode[1:])) 1382 self._read_ack("setting trigger_mode") 1383 else: # Trigger links 1384 self._send_trigger_links(self._link_trigger_channel1[1:], self._link_trigger_channel2[1:]) 1385 1386 def _read_raw(self, n_bytes): 1387 """Read exactly n_bytes from the serial port: the whole reply to the command last sent, which is then no 1388 longer pending (see _write_command()). Only connecting reads a reply in more than one call.""" 1389 message = self.port.read(n_bytes) 1390 if len(message) < n_bytes: 1391 raise PulsePalError( 1392 f"Wave Pal sent {len(message)} of the {n_bytes} byte(s) of its reply within " 1393 f"{self.port.timeout} s. {_common.NO_REPLY_HINT}" 1394 ) 1395 self._reply_pending = None 1396 return message 1397 1398 def _read_ack(self, context): 1399 """Read a one-byte confirmation: 1 if the device executed the 1400 command, 0 if it rejected it.""" 1401 try: 1402 reply = self._read_raw(1)[0] 1403 except PulsePalError as exc: 1404 raise PulsePalError( 1405 f"Wave Pal did not confirm {context}: no reply arrived within {self.port.timeout} s. " 1406 f"{_common.NO_REPLY_HINT}" 1407 ) from exc 1408 if reply != 1: 1409 raise PulsePalError( 1410 f"Wave Pal rejected {context}. A value was out of range, " 1411 "or a waveform could not be written to the microSD card." 1412 )
A class to control a Wave Pal on a USB serial port.
Creating an instance opens the serial port, checks that the device
runs Wave Pal firmware, reads its properties into
WavePalDevice.info, shows "PYTHON Connected" on the device's
screen, stops any playback and programs the default settings (see
WavePalDevice.set_default_params).
from pulsepal import WavePalDevice
W = WavePalDevice("COM3")
W.load_waveform(1, [0, 1, 2, 3, 4, 5, 0], 1) # waveform 1, played by channel 1
W.trigger(1)
W.close()
Replace "COM3" with the device's USB serial port name, which
WavePalDevice.serialportlist lists. WavePalDevice is also a
context manager, which closes the port on exit.
Closing the connection stops playback, as it does on Pulse Pal, and puts the device's own name back on its screen. The device keeps its settings and waveforms, so TTL triggers still play them.
332 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 333 """Open a connection to a Wave Pal. 334 335 Args: 336 port_name: USB serial port of the device, such as `COM3` on 337 Windows or `/dev/ttyACM0` on Linux. 338 serial_number: Instead of `port_name`, the device's USB serial 339 number (`DeviceInfo.serial_number`), which finds its port 340 wherever it is plugged in. 341 timeout: Serial read timeout, in seconds. Loading a long 342 waveform onto a slow microSD card can take a few seconds. 343 344 Firmware newer than this module knows is used with a warning: new 345 firmware only adds commands. Update the package to use what is new. 346 347 Raises: 348 PulsePalError: If the device does not reply to the handshake, or 349 runs other firmware. 350 serial.SerialException: If the serial port cannot be opened, 351 e.g. because another program has it open (on Linux and 352 macOS, another session of these classes, which lock it). 353 """ 354 self._closed = True 355 self.info = DeviceInfo() 356 self._sampling_rate = None 357 self._output_range = None 358 self._waveforms = [] # Indexed by waveform number, once the device has said how many it stores 359 self._current_waveform = ChannelSettings( 360 "current_waveform", [0] * 4, self, "_apply_current_waveform") 361 self._loop_mode = ChannelSettings( 362 "loop_mode", [False] * 4, self, "_apply_loop_mode") 363 self._loop_duration = ChannelSettings( 364 "loop_duration", [0.0] * 4, self, "_apply_loop_duration") 365 self._trigger_mode = ChannelSettings( 366 "trigger_mode", ["Normal"] * 2, self, "_apply_trigger_mode") 367 self._link_trigger_channel1 = ChannelSettings( 368 "link_trigger_channel1", [False] * 4, self, 369 "_apply_trigger_channel1_links") 370 self._link_trigger_channel2 = ChannelSettings( 371 "link_trigger_channel2", [False] * 4, self, 372 "_apply_trigger_channel2_links") 373 374 port_name = _common.port_name_from(port_name, serial_number, "WavePalDevice") 375 self.port = serial.Serial( 376 port_name, 377 self._BAUD_RATE, 378 timeout=timeout, 379 rtscts=True, 380 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 381 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 382 # it, such as another of these classes, gets an error instead 383 exclusive=True, 384 ) 385 self._closed = False 386 try: 387 # Discard anything left in the buffer by an earlier session 388 self.port.reset_input_buffer() 389 self._handshake() 390 self._read_hardware_info() 391 self._waveforms = [None] * (self.info.n_waveforms + 1) 392 # Client name op + "PYTHON" in ASCII, shown as "PYTHON Connected" 393 self._write_command(self._OP_SET_CLIENT_NAME, b"PYTHON") 394 self.stop() 395 self.set_default_params() 396 except BaseException: 397 # Op 81 means something else to other devices, so it is sent 398 # only once the device has identified itself as a Wave Pal 399 self._close(send_disconnect=self.info.firmware_version is not None) 400 raise
Open a connection to a Wave 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. Loading a long waveform onto a slow microSD card can take a few 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).
402 @staticmethod 403 def serialportlist(ports_to_list="available"): 404 """Return the names of the USB serial ports on this computer. 405 406 Called on the class, without connecting to a device, to find the 407 port name to pass to `WavePalDevice`. 408 409 Args: 410 ports_to_list: `available` to list only the ports that are 411 not already in use, or `all` to list every USB serial 412 port. Not case sensitive. 413 414 Returns: 415 Sorted list of port names, such as `["COM3", "COM7"]`. 416 417 Raises: 418 PulsePalError: If `ports_to_list` is not `available` or `all`. 419 """ 420 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 WavePalDevice.
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.
426 def set_default_params(self): 427 """Program the default settings on the device. 428 429 The defaults are both trigger channels in normal mode, a 10 kHz 430 sampling rate, the -10 V to 10 V output range, loop mode off with 431 loop durations of 0, no output channel linked to a trigger channel 432 (so a TTL starts nothing until a script links it), and no current 433 waveform on any channel. They match the settings the device starts with. The 434 trigger modes are sent first: a trigger channel leaving param sync 435 mode makes what was stored for its next edge current, and the 436 settings after it then apply at once. 437 438 Loaded waveforms are kept, and loaded again if the output range 439 changes (see `WavePalDevice.output_range`). 440 441 Raises: 442 PulsePalError: If a loaded waveform does not fit the default 443 output range, -10 V to 10 V. 444 """ 445 self.trigger_mode = ["Normal"] * 2 446 self.sampling_rate = 10000 447 self.output_range = "-10V:10V" 448 self.loop_mode = [False] * 4 449 self.loop_duration = [0] * 4 450 self._set_trigger_links([False] * 4, [False] * 4) 451 self.current_waveform = [0] * 4
Program the default settings on the device.
The defaults are both trigger channels in normal mode, a 10 kHz sampling rate, the -10 V to 10 V output range, loop mode off with loop durations of 0, no output channel linked to a trigger channel (so a TTL starts nothing until a script links it), and no current waveform on any channel. They match the settings the device starts with. The trigger modes are sent first: a trigger channel leaving param sync mode makes what was stored for its next edge current, and the settings after it then apply at once.
Loaded waveforms are kept, and loaded again if the output range
changes (see WavePalDevice.output_range).
Raises:
- PulsePalError: If a loaded waveform does not fit the default output range, -10 V to 10 V.
453 @property 454 def sampling_rate(self): 455 """Sampling rate of all output channels, in Hz. 456 457 A whole number from 1 to `DeviceInfo.max_sampling_rate`. It can be 458 changed during playback. The rate played can differ slightly from 459 the rate set: see `WavePalDevice.actual_sampling_rate`. 460 """ 461 return self._sampling_rate
Sampling rate of all output channels, in Hz.
A whole number from 1 to DeviceInfo.max_sampling_rate. It can be
changed during playback. The rate played can differ slightly from
the rate set: see WavePalDevice.actual_sampling_rate.
479 @property 480 def actual_sampling_rate(self): 481 """The sampling rate the device plays, in Hz. 482 483 The device divides `DeviceInfo.sample_clock_hz` (24 MHz) by a 484 whole number, so the rate played is the nearest one of those to 485 `WavePalDevice.sampling_rate`. For example, 44100 Hz plays at 486 44117.6 Hz. Rates that divide 24 MHz exactly, such as 10 kHz, 487 25 kHz or 100 kHz, play exactly. 488 """ 489 return self._actual_rate(self._sampling_rate)
The sampling rate the device plays, in Hz.
The device divides DeviceInfo.sample_clock_hz (24 MHz) by a
whole number, so the rate played is the nearest one of those to
WavePalDevice.sampling_rate. For example, 44100 Hz plays at
44117.6 Hz. Rates that divide 24 MHz exactly, such as 10 kHz,
25 kHz or 100 kHz, play exactly.
491 @property 492 def output_range(self): 493 """Voltage range of all output channels. 494 495 One of `DeviceInfo.output_ranges`: `"0V:5V"`, `"0V:10V"`, 496 `"-5V:5V"` or `"-10V:10V"`. The smallest range that fits the 497 waveforms gives the finest voltage steps. 498 499 Changing the range stops playback, sets all outputs to 0 V, and 500 loads the waveforms again, encoded for the new range. It raises 501 an error, and changes nothing, if a loaded waveform does not fit 502 the new range, or if a trigger channel is in param sync mode, 503 where the loads would wait for a TTL edge. 504 """ 505 return self._output_range
Voltage range of all output channels.
One of DeviceInfo.output_ranges: "0V:5V", "0V:10V",
"-5V:5V" or "-10V:10V". The smallest range that fits the
waveforms gives the finest voltage steps.
Changing the range stops playback, sets all outputs to 0 V, and loads the waveforms again, encoded for the new range. It raises an error, and changes nothing, if a loaded waveform does not fit the new range, or if a trigger channel is in param sync mode, where the loads would wait for a TTL edge.
525 @property 526 def current_waveform(self): 527 """The waveform each output channel plays, by waveform number. 528 529 Indexed by channel number (see "Channel settings" above). Each 530 value is the number of a waveform loaded with 531 `WavePalDevice.load_waveform`, 1 to `DeviceInfo.n_waveforms`, or 532 `0` for none, the default: a channel with no waveform ignores 533 triggers. Several channels can play the same waveform. 534 535 A channel that is playing finishes its waveform: the new one plays 536 from the channel's next start. In param sync mode, a change waits 537 for the next rising edge on the param sync trigger channel (see 538 `WavePalDevice.trigger_mode`). 539 540 ```python 541 W.current_waveform[1] = 2 # channel 1 plays waveform 2 542 W.current_waveform = [1, 1, 1, 1] # all four play waveform 1 543 ``` 544 """ 545 return self._current_waveform
The waveform each output channel plays, by waveform number.
Indexed by channel number (see "Channel settings" above). Each
value is the number of a waveform loaded with
WavePalDevice.load_waveform, 1 to DeviceInfo.n_waveforms, or
0 for none, the default: a channel with no waveform ignores
triggers. Several channels can play the same waveform.
A channel that is playing finishes its waveform: the new one plays
from the channel's next start. In param sync mode, a change waits
for the next rising edge on the param sync trigger channel (see
WavePalDevice.trigger_mode).
W.current_waveform[1] = 2 # channel 1 plays waveform 2
W.current_waveform = [1, 1, 1, 1] # all four play waveform 1
551 @property 552 def loop_mode(self): 553 """Whether each output channel loops its waveform. 554 555 Indexed by channel number (see "Channel settings" above). `True` 556 repeats the waveform until `WavePalDevice.loop_duration` has 557 elapsed, or until stopped if the loop duration is 0. `False` plays 558 it once. A change applies to playback in progress. 559 """ 560 return self._loop_mode
Whether each output channel loops its waveform.
Indexed by channel number (see "Channel settings" above). True
repeats the waveform until WavePalDevice.loop_duration has
elapsed, or until stopped if the loop duration is 0. False plays
it once. A change applies to playback in progress.
566 @property 567 def loop_duration(self): 568 """How long each output channel loops its waveform, in seconds. 569 570 Indexed by channel number (see "Channel settings" above). Applies 571 in loop mode only, and counts from the trigger. The channel stops 572 when it has elapsed, part way through the waveform if need be. `0` 573 loops until the channel is stopped. Durations are rounded to a 574 whole number of samples, and a nonzero duration lasts at least 575 one sample. 576 """ 577 return self._loop_duration
How long each output channel loops its waveform, in seconds.
Indexed by channel number (see "Channel settings" above). Applies
in loop mode only, and counts from the trigger. The channel stops
when it has elapsed, part way through the waveform if need be. 0
loops until the channel is stopped. Durations are rounded to a
whole number of samples, and a nonzero duration lasts at least
one sample.
583 @property 584 def trigger_mode(self): 585 """How each trigger channel acts on the output channels linked to it. 586 587 Indexed by trigger channel number, 1 or 2 (index 0 is unused), as 588 in Pulse Pal. A TTL edge on a trigger channel acts on the output 589 channels linked to it (`WavePalDevice.link_trigger_channel1`, 590 `WavePalDevice.link_trigger_channel2`): 591 592 - `"Normal"`: a rising edge starts the linked channels' waveforms. 593 Channels that are playing ignore it. 594 - `"Toggle"`: a rising edge starts the linked channels, or stops 595 those that are playing. 596 - `"Gated"`: a rising edge starts the linked channels, and a 597 falling edge stops them, unless the other trigger channel is also 598 gated, linked to them, and still high. With loop mode on and a 599 loop duration of 0, a waveform plays for exactly as long as the 600 TTL is high. 601 - `"Param Sync"`: a rising edge starts and stops nothing (the 602 channel's links are ignored). It makes the waveforms loaded, 603 and the `WavePalDevice.current_waveform` set, since the previous 604 edge current. This is how the next trial's waveforms are loaded 605 during the current trial, and played from the instant it starts. 606 - `"Master"`: a rising edge starts the linked channels, and 607 restarts those that are playing from their first sample. 608 609 The first four are Pulse Pal's trigger modes. Names are not case 610 sensitive. 611 612 While a trigger channel is in param sync mode, 613 `WavePalDevice.load_waveform` loads into a second memory block 614 that the device keeps for each waveform. Channels go on playing 615 the waveform it replaces, and triggers still start it, until the 616 edge. A channel that is playing at the edge finishes what it plays, 617 and plays the new waveform from its next start. A later load of the 618 same waveform before the edge replaces the earlier one, and stops 619 any channel still playing the waveform from before the previous 620 edge, since the device reuses that memory block. Changes to 621 `WavePalDevice.current_waveform` wait for the edge in the same 622 way. Other settings apply at once. 623 624 When neither trigger channel is in param sync mode any more, what 625 was waiting for an edge becomes current at once, so 626 `WavePalDevice.waveforms` and `WavePalDevice.current_waveform` 627 describe the device again. (Pulse Pal and Synth Pal discard their 628 stored parameters and settings then; Pulse Pal makes its stored 629 custom trains current, as this class does its waveforms.) 630 `WavePalDevice.output_range` cannot change 631 in param sync mode. To make the waveforms current on the same edge 632 that starts the channels, wire the TTL to the other trigger channel 633 as well: the param sync edge is handled first. 634 635 ```python 636 W.trigger_mode[2] = "Param Sync" # sent at once 637 W.load_waveform(1, next_trial_wave) # waits for trigger channel 2's next rising edge 638 ``` 639 """ 640 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), as
in Pulse Pal. A TTL edge on a trigger channel acts on the output
channels linked to it (WavePalDevice.link_trigger_channel1,
WavePalDevice.link_trigger_channel2):
"Normal": a rising edge starts the linked channels' waveforms. 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 loop mode on and a loop duration of 0, a waveform 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 makes the waveforms loaded, and theWavePalDevice.current_waveformset, since the previous edge current. This is how the next trial's waveforms are loaded during the current trial, and played from the instant it starts."Master": a rising edge starts the linked channels, and restarts those that are playing from their first sample.
The first four are Pulse Pal's trigger modes. Names are not case sensitive.
While a trigger channel is in param sync mode,
WavePalDevice.load_waveform loads into a second memory block
that the device keeps for each waveform. Channels go on playing
the waveform it replaces, and triggers still start it, until the
edge. A channel that is playing at the edge finishes what it plays,
and plays the new waveform from its next start. A later load of the
same waveform before the edge replaces the earlier one, and stops
any channel still playing the waveform from before the previous
edge, since the device reuses that memory block. Changes to
WavePalDevice.current_waveform wait for the edge in the same
way. Other settings apply at once.
When neither trigger channel is in param sync mode any more, what
was waiting for an edge becomes current at once, so
WavePalDevice.waveforms and WavePalDevice.current_waveform
describe the device again. (Pulse Pal and Synth Pal discard their
stored parameters and settings then; Pulse Pal makes its stored
custom trains current, as this class does its waveforms.)
WavePalDevice.output_range cannot change
in param sync mode. To make the waveforms current on the same edge
that starts the channels, wire the TTL to the other trigger channel
as well: the param sync edge is handled first.
W.trigger_mode[2] = "Param Sync" # sent at once
W.load_waveform(1, next_trial_wave) # waits for trigger channel 2's next rising edge
646 @property 647 def link_trigger_channel1(self): 648 """Whether each output channel is triggered by trigger channel 1. 649 650 Indexed by channel number (see "Channel settings" above). By 651 default no output channel is linked to either trigger channel, so 652 a TTL starts nothing until a script links it. 653 """ 654 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.
660 @property 661 def link_trigger_channel2(self): 662 """Whether each output channel is triggered by trigger channel 2. 663 664 Indexed by channel number (see "Channel settings" above). 665 """ 666 return self._link_trigger_channel2
Whether each output channel is triggered by trigger channel 2.
Indexed by channel number (see "Channel settings" above).
672 def set_screen_saver(self, enabled, timeout=1800): 673 """Switch the device's screen saver on or off, and set its timeout. 674 675 With the screen saver on, the device dims its screen once it has 676 been left alone for `timeout` seconds: no command from a computer, 677 no rising edge on a trigger channel, and no joystick click or 678 push. The next of these brings the screen back. The device keeps 679 both settings in its EEPROM, shared with Pulse Pal and Synth Pal 680 firmware, and saves them once no channel is playing. A new device 681 starts with the screen saver on and 1800 s. 682 683 Args: 684 enabled: `True` to switch the screen saver on, `False` to 685 switch it off. 686 timeout: Seconds without activity before the screen dims, 1 to 687 65535. It is sent with every call, so leaving it out sets 688 1800 s. 689 690 Raises: 691 PulsePalError: If a value is out of range. 692 """ 693 state = to_bool(enabled, "enabled") 694 if isinstance(timeout, (bool, np.bool_)) \ 695 or not isinstance(timeout, numbers.Integral) \ 696 or not 1 <= timeout <= 65535: 697 raise PulsePalError( 698 "timeout must be a whole number of seconds from 1 to 65535. " 699 f"Received {timeout!r}." 700 ) 701 self._write_command(self._OP_SET_SCREEN_SAVER, 702 struct.pack("<BH", int(state), int(timeout))) 703 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 and Synth 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
- It is sent with every call, so leaving it out sets 1800 s.
Raises:
- PulsePalError: If a value is out of range.
705 def export_params(self): 706 """Return every setting, as a dict of plain values and lists. 707 708 Keyed by setting name: `"sampling_rate"` and `"output_range"`, then 709 the channel settings, each with one value per channel and no 710 unused index 0. It holds only numbers, booleans and names, so it 711 can be saved with `json` and logged with your data. The waveforms 712 are not part of it: `WavePalDevice.waveforms` holds them. 713 714 ```python 715 import json 716 with open("session_settings.json", "w") as f: 717 json.dump(W.export_params(), f) 718 ``` 719 """ 720 params = {"sampling_rate": self._sampling_rate, "output_range": self._output_range} 721 for name in _CHANNEL_SETTINGS: 722 params[name] = list(getattr(self, name))[1:] 723 return params
Return every setting, as a dict of plain values and lists.
Keyed by setting name: "sampling_rate" and "output_range", 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. The waveforms
are not part of it: WavePalDevice.waveforms holds them.
import json
with open("session_settings.json", "w") as f:
json.dump(W.export_params(), f)
725 def import_params(self, params): 726 """Program the device with settings exported by `WavePalDevice.export_params`. 727 728 All of them are checked first, and nothing is sent if one is 729 invalid. They are then sent in an order that lets each take 730 effect: a trigger channel leaving param sync mode first, then the 731 sampling rate, the output range and the channel settings, and a 732 trigger channel entering param sync mode last. So 733 `WavePalDevice.current_waveform` applies at once, unless a trigger 734 channel was in param sync mode and stays in it: then it waits for 735 the next edge, as an assignment would. Settings missing from 736 `params` keep their values. A name that is not a setting is 737 skipped with a warning, so that settings exported by a newer 738 version of this package, which may have more, can still be 739 imported. 740 741 The waveforms are not settings: load them with 742 `WavePalDevice.load_waveform` first, since a current waveform must 743 be a loaded one. 744 745 ```python 746 import json 747 with open("session_settings.json") as f: 748 W.import_params(json.load(f)) 749 ``` 750 751 Args: 752 params: A dict of setting name to its value: a number for 753 `"sampling_rate"`, a name for `"output_range"`, and one 754 value per channel (a list, tuple or NumPy array) for the 755 others, as `export_params` returns. 756 757 Raises: 758 PulsePalError: If a value is invalid (nothing is sent), or the 759 device rejects a setting. 760 """ 761 names = {"sampling_rate", "output_range", *_CHANNEL_SETTINGS} 762 unknown = [name for name in params if name not in names] 763 if unknown: 764 warnings.warn(f"import_params(): skipped {', '.join(map(repr, unknown))}, which this version of " 765 "WavePalDevice does not have.", stacklevel=2) 766 new = {} 767 for name in _CHANNEL_SETTINGS: 768 if name in params: 769 n = 2 if name == "trigger_mode" else self.info.n_channels 770 new[name] = _common.one_value_per_channel(name, params[name], n) 771 if "current_waveform" in new: 772 new["current_waveform"] = self._normalize_current_waveform(new["current_waveform"]) 773 if "loop_mode" in new: 774 new["loop_mode"] = [to_bool(value, "loop_mode") for value in new["loop_mode"]] 775 if "loop_duration" in new: 776 new["loop_duration"] = self._normalize_loop_durations(new["loop_duration"]) 777 for name in ("link_trigger_channel1", "link_trigger_channel2"): 778 if name in new: 779 new[name] = [to_bool(value, name) for value in new[name]] 780 modes = list(self._trigger_mode)[1:] 781 if "trigger_mode" in new: 782 modes = [to_name(value, TRIGGER_MODES, "trigger mode") for value in new["trigger_mode"]] 783 # The trigger modes the other settings are sent in: those of channels leaving param sync mode are sent 784 # first, those of channels entering it last 785 first_modes = [new_mode if (old == "Param Sync" and new_mode != "Param Sync") else old 786 for old, new_mode in zip(self._trigger_mode[1:], modes)] 787 rate = self._sampling_rate 788 if "sampling_rate" in params: 789 rate = self._normalize_sampling_rate(params["sampling_rate"]) 790 range_name = self._output_range 791 if "output_range" in params: 792 range_name = self._check_output_range(params["output_range"], first_modes) 793 self._durations_to_samples(new.get("loop_duration", self._loop_duration[1:]), rate) # They fit at the rate 794 795 if first_modes != list(self._trigger_mode)[1:]: 796 self.trigger_mode = first_modes 797 if rate != self._sampling_rate: 798 self.sampling_rate = rate 799 if range_name != self._output_range: 800 self.output_range = range_name 801 for name in ("loop_mode", "loop_duration"): 802 if name in new: 803 setattr(self, name, new[name]) 804 if "link_trigger_channel1" in new or "link_trigger_channel2" in new: 805 self._set_trigger_links(new.get("link_trigger_channel1", self._link_trigger_channel1[1:]), 806 new.get("link_trigger_channel2", self._link_trigger_channel2[1:])) 807 if "current_waveform" in new: 808 self.current_waveform = new["current_waveform"] 809 if modes != first_modes: 810 self.trigger_mode = modes
Program the device with settings exported by WavePalDevice.export_params.
All of them are checked first, and nothing is sent if one is
invalid. They are then sent in an order that lets each take
effect: a trigger channel leaving param sync mode first, then the
sampling rate, the output range and the channel settings, and a
trigger channel entering param sync mode last. So
WavePalDevice.current_waveform applies at once, unless a trigger
channel was in param sync mode and stays in it: then it waits for
the next edge, as an assignment would. 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.
The waveforms are not settings: load them with
WavePalDevice.load_waveform first, since a current waveform must
be a loaded one.
import json
with open("session_settings.json") as f:
W.import_params(json.load(f))
Arguments:
- params: A dict of setting name to its value: a number for
"sampling_rate", a name for"output_range", 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), or the device rejects a setting.
812 @property 813 def waveforms(self): 814 """The waveforms loaded with `WavePalDevice.load_waveform`. 815 816 A list indexed by waveform number, 1 to `DeviceInfo.n_waveforms`, 817 with index 0 unused. Each element is a read-only NumPy array of 818 voltages, or `None` if this object has not loaded that waveform. In 819 param sync mode, it holds the waveforms loaded since the last edge 820 before the device plays them. `WavePalDevice.status` shows what 821 the device itself holds. 822 """ 823 return list(self._waveforms)
The waveforms loaded with WavePalDevice.load_waveform.
A list indexed by waveform number, 1 to DeviceInfo.n_waveforms,
with index 0 unused. Each element is a read-only NumPy array of
voltages, or None if this object has not loaded that waveform. In
param sync mode, it holds the waveforms loaded since the last edge
before the device plays them. WavePalDevice.status shows what
the device itself holds.
829 def load_waveform(self, waveform_number, waveform, channels=None): 830 """Load one of the device's waveforms, and choose the channels that play it. 831 832 The samples are written to the device's microSD card, and the 833 first `DeviceInfo.buffer_samples` of them are also kept in its RAM. 834 Outside param sync mode, loading stops the channels playing that 835 waveform, and the new one plays from their next start. Other 836 channels keep playing. In param sync mode, the new waveform waits 837 for the next rising edge on the param sync trigger channel, and 838 channels go on playing the old one until then (see 839 `WavePalDevice.trigger_mode`). 840 841 ```python 842 t = np.arange(10000) / W.sampling_rate 843 W.load_waveform(2, 3 * np.sin(2 * np.pi * 100 * t), [1, 3]) 844 ``` 845 846 Args: 847 waveform_number: The waveform to load, 1 to 848 `DeviceInfo.n_waveforms`. 849 waveform: Voltages of the samples, played at 850 `WavePalDevice.sampling_rate`. A list, tuple or NumPy 851 array of 1 to `DeviceInfo.max_samples` values, all within 852 `WavePalDevice.output_range`. 853 channels: Output channels that play this waveform, from their 854 next start: one channel number 1-4, or several as a list, 855 tuple or NumPy array. It sets their 856 `WavePalDevice.current_waveform`, and leaves the other 857 channels' alone, so channels already playing this waveform 858 play the new one. `None`, the default, changes no channel. 859 860 Raises: 861 PulsePalError: If an argument is invalid, or the device could 862 not store the waveform. The waveform is then left out of 863 `WavePalDevice.waveforms`: outside param sync mode the 864 device has unloaded it, and in param sync mode it plays the 865 version made current at the last edge. 866 """ 867 number = self._waveform_number(waveform_number) 868 channel_list = None if channels is None else _common.channel_numbers(channels) 869 samples = np.array(waveform, dtype=float).ravel() 870 if not 1 <= samples.size <= self.info.max_samples: 871 raise PulsePalError( 872 f"A waveform must have 1 to {self.info.max_samples} samples. " 873 f"Received {samples.size}." 874 ) 875 codes = self._volts_to_codes(samples) 876 self._waveforms[number] = None # Until the device has stored it. Outside param sync mode, it unloads it first. 877 self._write_command( 878 self._OP_LOAD_WAVEFORM, 879 struct.pack("<BI", number, samples.size) + codes.tobytes(), 880 ) 881 self._read_ack("load_waveform()") 882 samples.flags.writeable = False 883 self._waveforms[number] = samples 884 if channel_list: 885 values = list(self._current_waveform[1:]) 886 for channel in channel_list: 887 values[channel - 1] = number 888 self._current_waveform._assign(values)
Load one of the device's waveforms, and choose the channels that play it.
The samples are written to the device's microSD card, and the
first DeviceInfo.buffer_samples of them are also kept in its RAM.
Outside param sync mode, loading stops the channels playing that
waveform, and the new one plays from their next start. Other
channels keep playing. In param sync mode, the new waveform waits
for the next rising edge on the param sync trigger channel, and
channels go on playing the old one until then (see
WavePalDevice.trigger_mode).
t = np.arange(10000) / W.sampling_rate
W.load_waveform(2, 3 * np.sin(2 * np.pi * 100 * t), [1, 3])
Arguments:
- waveform_number: The waveform to load, 1 to
DeviceInfo.n_waveforms. - waveform: Voltages of the samples, played at
WavePalDevice.sampling_rate. A list, tuple or NumPy array of 1 toDeviceInfo.max_samplesvalues, all withinWavePalDevice.output_range. - channels: Output channels that play this waveform, from their
next start: one channel number 1-4, or several as a list,
tuple or NumPy array. It sets their
WavePalDevice.current_waveform, and leaves the other channels' alone, so channels already playing this waveform play the new one.None, the default, changes no channel.
Raises:
- PulsePalError: If an argument is invalid, or the device could
not store the waveform. The waveform is then left out of
WavePalDevice.waveforms: outside param sync mode the device has unloaded it, and in param sync mode it plays the version made current at the last edge.
890 def trigger(self, channels): 891 """Trigger output channels in software. 892 893 Idle channels start their current waveforms from the first 894 sample, together. Channels that are playing ignore it, as Pulse 895 Pal's channels ignore a soft trigger while they play a pulse train. 896 897 ```python 898 W.trigger(1) 899 W.trigger([2, 4]) # a list, tuple or NumPy array 900 ``` 901 902 Args: 903 channels: Output channel number 1-4, or several as a list, 904 tuple or NumPy array. 905 906 Raises: 907 PulsePalError: If a channel has no waveform to play: its 908 `WavePalDevice.current_waveform` is 0, or a waveform this 909 object has not loaded. Nothing is sent. 910 """ 911 channel_list = _common.channel_numbers(channels) 912 for channel in channel_list: 913 number = self._current_waveform[channel] 914 if number == 0: 915 raise PulsePalError( 916 f"Channel {channel} has no waveform to play. Set current_waveform[{channel}] to a loaded " 917 f"waveform's number, or load one with load_waveform(number, waveform, {channel})." 918 ) 919 if self._waveforms[number] is None: 920 raise PulsePalError( 921 f"Channel {channel} plays waveform {number}, which is not loaded. Load it with " 922 f"load_waveform({number}, waveform)." 923 ) 924 self._write_command(self._OP_TRIGGER, bytes([_common.channel_bits(channel_list)]))
Trigger output channels in software.
Idle channels start their current waveforms from the first sample, together. Channels that are playing ignore it, as Pulse Pal's channels ignore a soft trigger while they play a pulse train.
W.trigger(1)
W.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.
Raises:
- PulsePalError: If a channel has no waveform to play: its
WavePalDevice.current_waveformis 0, or a waveform this object has not loaded. Nothing is sent.
926 def stop(self, channels=None): 927 """Stop playback. The stopped channels output 0 V. 928 929 Args: 930 channels: Output channel number 1-4, or several as a list, 931 tuple or NumPy array. `None` stops all channels. 932 """ 933 bits = self._ALL_CHANNELS if channels is None else _common.channel_bits(channels) 934 self._write_command(self._OP_STOP, bytes([bits]))
Stop playback. The stopped channels output 0 V.
Arguments:
- channels: Output channel number 1-4, or several as a list,
tuple or NumPy array.
Nonestops all channels.
936 def set_fixed_voltage(self, channels, voltage): 937 """Set output channels to a fixed voltage. 938 939 Stops playback on those channels. The voltage holds until the 940 channel is triggered or stopped. 941 942 Args: 943 channels: Output channel number 1-4, or several as a list, 944 tuple or NumPy array. 945 voltage: Voltage, within `WavePalDevice.output_range`. 946 947 Raises: 948 PulsePalError: If the voltage is outside the output range, or 949 the device rejects the command. 950 """ 951 bits = _common.channel_bits(channels) 952 code = int(self._volts_to_codes([voltage])[0]) 953 self._write_command(self._OP_SET_FIXED_VOLTAGE, 954 struct.pack("<BH", bits, code)) 955 self._read_ack("set_fixed_voltage()")
Set output channels to a fixed voltage.
Stops playback on those channels. The voltage holds until the channel is triggered or stopped.
Arguments:
- channels: Output channel number 1-4, or several as a list, tuple or NumPy array.
- voltage: Voltage, within
WavePalDevice.output_range.
Raises:
- PulsePalError: If the voltage is outside the output range, or the device rejects the command.
957 def status(self): 958 """Read the device's playback state. 959 960 Returns: 961 A `DeviceStatus`, with the channels that are playing, the 962 number of samples in each waveform, underrun counts, the 963 longest playback interrupt, each channel's current waveform, 964 and what waits for a param sync edge. 965 """ 966 self._write_command(self._OP_GET_STATUS) 967 values = struct.unpack( 968 self._STATUS_FORMAT, 969 self._read_raw(struct.calcsize(self._STATUS_FORMAT)), 970 ) 971 playing_bits = values[0] 972 stored_load_bits = values[14] 973 return DeviceStatus( 974 playing=[ch for ch in range(1, 5) 975 if playing_bits & (1 << (ch - 1))], 976 samples_loaded=[None, *values[1:5]], 977 underruns=[None, *values[5:9]], 978 longest_interrupt_us=values[9] / 1000, 979 current_waveform=[None, *values[10:14]], 980 stored_waveforms=[n for n in range(1, 5) if stored_load_bits & (1 << (n - 1))], 981 stored_current_waveform=[None, *values[16:20]] if values[15] else None, 982 )
Read the device's playback state.
Returns:
A
DeviceStatus, with the channels that are playing, the number of samples in each waveform, underrun counts, the longest playback interrupt, each channel's current waveform, and what waits for a param sync edge.
1000 def close(self): 1001 """Close the connection to the device. 1002 1003 The device stops playback on all channels, and shows its own name 1004 on its screen again, in place of "PYTHON Connected". It keeps its 1005 settings and waveforms, so TTL triggers still play them. Safe to 1006 call more than once. Called automatically when leaving a `with` 1007 block and when the object is garbage collected, so playback also 1008 stops when the last reference to the object goes. 1009 1010 """ 1011 self._close(send_disconnect=True)
Close the connection to the device.
The device stops playback on all channels, and shows its own name
on its screen again, in place of "PYTHON Connected". It keeps its
settings and waveforms, so TTL triggers still play them. 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.
145@dataclass 146class DeviceInfo: 147 """Properties of the connected Wave Pal. 148 149 Populated when `WavePalDevice` connects, and available as 150 `WavePalDevice.info`. 151 """ 152 153 firmware_version: int = None 154 """Wave Pal firmware version running on the device.""" 155 156 hardware_version: int = None 157 """Pulse Pal hardware version, e.g. `3`.""" 158 159 circuit_board_revision: int = None 160 """Revision of the device's circuit board, 0-31. 161 162 The board encodes its revision on five pins, which the firmware reads 163 at startup. A board that encodes none reports `0`. To record exactly 164 which hardware ran an experiment, copy `hardware_version` and 165 `circuit_board_revision` to your data. 166 """ 167 168 serial_number: str = None 169 """USB serial number of the device, e.g. `"20345420"`: the Teensy's, 170 unique to each Pulse Pal 3, and the same whichever firmware it runs. 171 172 Connect by it, wherever the device is plugged in, with 173 `WavePalDevice(serial_number="20345420")`. To record exactly which unit ran an 174 experiment, copy it to your data. 175 """ 176 177 n_channels: int = None 178 """Number of output channels.""" 179 180 n_waveforms: int = None 181 """Number of waveforms the device stores, numbered from 1.""" 182 183 max_samples: int = None 184 """Maximum number of samples in one waveform.""" 185 186 max_sampling_rate: int = None 187 """Highest sampling rate, in Hz.""" 188 189 buffer_samples: int = None 190 """Samples per playback buffer. 191 192 The first `buffer_samples` samples of each waveform are kept in the 193 device's RAM, so that playback starts at once. A waveform no longer 194 than this plays without reading the microSD card. 195 """ 196 197 sample_clock_hz: int = None 198 """Clock that the sample rate is divided from, in Hz. 199 200 See `WavePalDevice.actual_sampling_rate`. 201 """ 202 203 output_ranges: tuple = tuple(OUTPUT_RANGES) 204 """Names of the output ranges, accepted by 205 `WavePalDevice.output_range`.""" 206 207 trigger_modes: tuple = TRIGGER_MODES 208 """Names of the trigger modes, accepted by 209 `WavePalDevice.trigger_mode`."""
Properties of the connected Wave Pal.
Populated when WavePalDevice connects, and available as
WavePalDevice.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
WavePalDevice(serial_number="20345420"). To record exactly which unit ran an
experiment, copy it to your data.
Samples per playback buffer.
The first buffer_samples samples of each waveform are kept in the
device's RAM, so that playback starts at once. A waveform no longer
than this plays without reading the microSD card.
Names of the output ranges, accepted by
WavePalDevice.output_range.
Names of the trigger modes, accepted by
WavePalDevice.trigger_mode.
212@dataclass 213class DeviceStatus: 214 """A snapshot of the device's playback state, from 215 `WavePalDevice.status`.""" 216 217 playing: list 218 """Numbers of the output channels playing a waveform, e.g. `[1, 3]`.""" 219 220 samples_loaded: list 221 """Samples in each waveform, `0` if it has none. 222 223 Indexed by waveform number, with index 0 unused. This is what the 224 device holds, which can include waveforms loaded by an earlier 225 connection. A load stored for a param sync edge counts from the edge. 226 """ 227 228 underruns: list 229 """Underruns on each channel since the device started, indexed by 230 channel number. 231 232 An underrun is a block of samples that was not read from the microSD 233 card by the time it was due. The output then holds its last value 234 until the block arrives. See "Storage and buffering" in the 235 [Wave Pal protocol](https://github.com/sanworks/PulsePal/blob/develop/Firmware/WavePal/PROTOCOL.md#storage-and-buffering). 236 """ 237 238 longest_interrupt_us: float 239 """Longest run of the device's playback interrupt since the previous 240 call to `WavePalDevice.status`, in microseconds. 241 242 It must stay below the sample period, `1e6 / sampling_rate`. 243 """ 244 245 current_waveform: list 246 """The waveform each output channel plays from its next start, `0` for 247 none, as the device holds it. Indexed by channel number. 248 249 In param sync mode, `WavePalDevice.current_waveform` set since the 250 last edge is in `DeviceStatus.stored_current_waveform` until the next 251 one. 252 """ 253 254 stored_waveforms: list 255 """Numbers of the waveforms loaded in param sync mode since the last 256 edge, which the next edge makes current, e.g. `[2]`.""" 257 258 stored_current_waveform: list 259 """The current waveforms set in param sync mode since the last edge, 260 which the next edge applies, indexed by channel number. `None` if 261 none were set."""
A snapshot of the device's playback state, from
WavePalDevice.status.
Samples in each waveform, 0 if it has none.
Indexed by waveform number, with index 0 unused. This is what the device holds, which can include waveforms loaded by an earlier connection. A load stored for a param sync edge counts from the edge.
Underruns on each channel since the device started, indexed by channel number.
An underrun is a block of samples that was not read from the microSD card by the time it was due. The output then holds its last value until the block arrives. See "Storage and buffering" in the Wave Pal protocol.
Longest run of the device's playback interrupt since the previous
call to WavePalDevice.status, in microseconds.
It must stay below the sample period, 1e6 / sampling_rate.
The waveform each output channel plays from its next start, 0 for
none, as the device holds it. Indexed by channel number.
In param sync mode, WavePalDevice.current_waveform set since the
last edge is in DeviceStatus.stored_current_waveform until the next
one.
Output ranges in order of their range index on the device, with their limits in volts. See "Output ranges" in /Firmware/WavePal/PROTOCOL.md.
Names of the trigger modes. See WavePalDevice.trigger_mode.