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            )
class WavePalDevice:
 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.

WavePalDevice(port_name=None, *, serial_number=None, timeout=10)
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 COM3 on Windows or /dev/ttyACM0 on 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).
port: serial.serialposix.Serial

The open serial.Serial port connected to the device.

info: DeviceInfo

Properties of the connected device. See DeviceInfo.

@staticmethod
def serialportlist(ports_to_list='available'):
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: available to list only the ports that are not already in use, or all to list every USB serial port. Not case sensitive.
Returns:

Sorted list of port names, such as ["COM3", "COM7"].

Raises:
  • PulsePalError: If ports_to_list is not available or all.
def set_default_params(self):
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.
sampling_rate
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.

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.

output_range
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.

current_waveform
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
loop_mode
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.

loop_duration
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.

trigger_mode
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 the WavePalDevice.current_waveform set, 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
def set_screen_saver(self, enabled, timeout=1800):
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: True to switch the screen saver on, False to switch it off.
  • timeout: Seconds without activity before the screen dims, 1 to
    1. It is sent with every call, so leaving it out sets 1800 s.
Raises:
  • PulsePalError: If a value is out of range.
def export_params(self):
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)
def import_params(self, params):
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, as export_params returns.
Raises:
  • PulsePalError: If a value is invalid (nothing is sent), or the device rejects a setting.
waveforms
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.

def load_waveform(self, waveform_number, waveform, channels=None):
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:
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.
def trigger(self, channels):
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_waveform is 0, or a waveform this object has not loaded. Nothing is sent.
def stop(self, channels=None):
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. None stops all channels.
def set_fixed_voltage(self, channels, voltage):
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.
def status(self):
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.

def close(self):
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.

@dataclass
class DeviceInfo:
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.

DeviceInfo( firmware_version: int = None, hardware_version: int = None, circuit_board_revision: int = None, serial_number: str = None, n_channels: int = None, n_waveforms: int = None, max_samples: int = None, max_sampling_rate: int = None, buffer_samples: int = None, sample_clock_hz: int = None, output_ranges: tuple = ('0V:5V', '0V:10V', '-5V:5V', '-10V:10V'), trigger_modes: tuple = ('Normal', 'Toggle', 'Gated', 'Param Sync', 'Master'))
firmware_version: int = None

Wave Pal firmware version running on the device.

hardware_version: int = None

Pulse Pal hardware version, e.g. 3.

circuit_board_revision: int = None

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.

serial_number: str = None

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.

n_channels: int = None

Number of output channels.

n_waveforms: int = None

Number of waveforms the device stores, numbered from 1.

max_samples: int = None

Maximum number of samples in one waveform.

max_sampling_rate: int = None

Highest sampling rate, in Hz.

buffer_samples: int = None

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.

sample_clock_hz: int = None

Clock that the sample rate is divided from, in Hz.

See WavePalDevice.actual_sampling_rate.

output_ranges: tuple = ('0V:5V', '0V:10V', '-5V:5V', '-10V:10V')

Names of the output ranges, accepted by WavePalDevice.output_range.

trigger_modes: tuple = ('Normal', 'Toggle', 'Gated', 'Param Sync', 'Master')

Names of the trigger modes, accepted by WavePalDevice.trigger_mode.

@dataclass
class DeviceStatus:
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.

DeviceStatus( playing: list, samples_loaded: list, underruns: list, longest_interrupt_us: float, current_waveform: list, stored_waveforms: list, stored_current_waveform: list)
playing: list

Numbers of the output channels playing a waveform, e.g. [1, 3].

samples_loaded: list

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: list

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_interrupt_us: float

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.

current_waveform: list

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.

stored_waveforms: list

Numbers of the waveforms loaded in param sync mode since the last edge, which the next edge makes current, e.g. [2].

stored_current_waveform: list

The current waveforms set in param sync mode since the last edge, which the next edge applies, indexed by channel number. None if none were set.

OUTPUT_RANGES = {'0V:5V': (0.0, 5.0), '0V:10V': (0.0, 10.0), '-5V:5V': (-5.0, 5.0), '-10V:10V': (-10.0, 10.0)}

Output ranges in order of their range index on the device, with their limits in volts. See "Output ranges" in /Firmware/WavePal/PROTOCOL.md.

TRIGGER_MODES = ('Normal', 'Toggle', 'Gated', 'Param Sync', 'Master')

Names of the trigger modes. See WavePalDevice.trigger_mode.