pulsepal
Python interface for Pulse Pal, an open source pulse train generator, and for Wave Pal and Synth Pal, two alternative firmwares for Pulse Pal 3.
from pulsepal import PulsePalDevice
with PulsePalDevice("COM3") as P: # Replace COM3 with Pulse Pal's port
P.phase1_voltage[1] = 5
P.trigger(1)
| Device firmware | Class | Module |
|---|---|---|
| Pulse Pal: pulse trains | PulsePalDevice |
pulsepal.pulse_pal |
| Wave Pal: sampled waveforms, streamed from the microSD card | WavePalDevice |
pulsepal.wave_pal |
| Synth Pal: sine, triangle, square and sawtooth waves, white noise, and fixed voltages | SynthPalDevice |
pulsepal.synth_pal |
The three classes work the same way. Settings are lists indexed by
channel number (index 0 is unused), and assigning to one programs the
device at once; trigger() starts channels and stop() stops them;
set_default_params() programs the defaults; export_params() and
import_params() record and restore every setting; print(device) shows
every setting; and they raise PulsePalError when something fails. Each
class names the firmware a device runs when it is not its own.
PulsePalDevice.serialportlist() lists the USB serial ports, to find the
device's.
This file is part of the Sanworks PulsePal repository, released under the GNU General Public License version 3.
1""" 2Python interface for [Pulse Pal](https://sites.google.com/site/pulsepalwiki/), 3an open source pulse train generator, and for Wave Pal and Synth Pal, two 4alternative firmwares for Pulse Pal 3. 5 6```python 7from pulsepal import PulsePalDevice 8 9with PulsePalDevice("COM3") as P: # Replace COM3 with Pulse Pal's port 10 P.phase1_voltage[1] = 5 11 P.trigger(1) 12``` 13 14| Device firmware | Class | Module | 15|---|---|---| 16| Pulse Pal: pulse trains | `PulsePalDevice` | `pulsepal.pulse_pal` | 17| Wave Pal: sampled waveforms, streamed from the microSD card | `WavePalDevice` | `pulsepal.wave_pal` | 18| Synth Pal: sine, triangle, square and sawtooth waves, white noise, and fixed voltages | `SynthPalDevice` | `pulsepal.synth_pal` | 19 20The three classes work the same way. Settings are lists indexed by 21channel number (index 0 is unused), and assigning to one programs the 22device at once; `trigger()` starts channels and `stop()` stops them; 23`set_default_params()` programs the defaults; `export_params()` and 24`import_params()` record and restore every setting; `print(device)` shows 25every setting; and they raise `PulsePalError` when something fails. Each 26class names the firmware a device runs when it is not its own. 27 28`PulsePalDevice.serialportlist()` lists the USB serial ports, to find the 29device's. 30 31This file is part of the Sanworks PulsePal repository, released under the 32GNU General Public License version 3. 33""" 34 35from ._common import PulsePalError 36from .pulse_pal import PulsePalDevice 37from .synth_pal import SynthPalDevice 38from .wave_pal import WavePalDevice 39 40__version__ = "3.0.0" 41"""Version of this package.""" 42 43__all__ = ["PulsePalDevice", "WavePalDevice", "SynthPalDevice", "PulsePalError", "__version__"] 44__docformat__ = "google"
399class PulsePalDevice: 400 """A class to control a Pulse Pal device on a USB serial port. 401 402 Creating an instance opens the serial port, exchanges a handshake 403 with the device, verifies that its firmware is supported, reads the 404 device properties into `PulsePalDevice.info`, and programs the 405 device with the default parameters (see 406 `PulsePalDevice.set_default_params`). 407 408 ```python 409 from pulsepal import PulsePalDevice 410 411 P = PulsePalDevice("COM3") 412 P.phase1_voltage[1] = 5 413 P.trigger(1) 414 P.close() 415 ``` 416 417 Here, replace "COM3" with Pulse Pal's USB serial port name. 418 To view a list of available ports, use 419 `PulsePalDevice.serialportlist()`. 420 Pulse Pal's port may not be visible if it is connected to another 421 instance of PulsePalDevice or an external application. Use 422 `PulsePalDevice.serialportlist("all")` to view all ports. 423 If you see multiple available ports, disconnect Pulse Pal's USB plug 424 and run serialportlist() again. Notice which port disappears from the 425 list. 426 427 PulsePalDevice is also a context manager, which closes the connection on 428 exit even if an error is raised: 429 430 ```python 431 with PulsePalDevice("COM3") as P: 432 P.trigger(1) 433 ``` 434 435 The attributes below named after Pulse Pal parameters are lists 436 indexed by channel number, with index 0 unused (see "Parameters" 437 above). `print(P)` shows them all, and `PulsePalDevice.export_params` 438 returns them as a dict. 439 """ 440 441 port: "serial.Serial" 442 """The open `serial.Serial` port connected to the device.""" 443 444 info: DeviceInfo 445 """Properties of the connected device. See `DeviceInfo`.""" 446 447 is_biphasic = _channel_setting("is_biphasic", """Pulse shape per channel: `False` for monophasic, `True` for biphasic. 448 449 Monophasic pulses use only the phase 1 parameters. Biphasic pulses 450 follow phase 1 with `PulsePalDevice.inter_phase_interval` and then 451 phase 2. 452 """) 453 454 phase1_voltage = _channel_setting("phase1_voltage", """Voltage of the first phase of each pulse, in volts [-10, 10].""") 455 456 phase2_voltage = _channel_setting("phase2_voltage", """Voltage of the second phase of each pulse, in volts [-10, 10]. 457 458 Used only when `PulsePalDevice.is_biphasic` is `True` for the channel. 459 """) 460 461 resting_voltage = _channel_setting("resting_voltage", """Voltage held between pulses, in volts [-10, 10]. 462 463 A new resting voltage reaches an idle channel's output at once. A 464 channel playing a pulse train keeps playing it, and moves to the new 465 resting voltage at its next transition to rest. 466 """) 467 468 phase1_duration = _channel_setting("phase1_duration", """Duration of the first phase of each pulse, in seconds. 469 470 At least `DeviceInfo.min_pulse_width_us`. 471 """) 472 473 inter_phase_interval = _channel_setting("inter_phase_interval", """Interval between the two phases of a biphasic pulse, in seconds. 474 475 The channel rests at `PulsePalDevice.resting_voltage` during the 476 interval. Used only when `PulsePalDevice.is_biphasic` is `True`. 477 """) 478 479 phase2_duration = _channel_setting("phase2_duration", """Duration of the second phase of each pulse, in seconds. 480 481 Used only when `PulsePalDevice.is_biphasic` is `True` for the channel. 482 At least `DeviceInfo.min_pulse_width_us`. 483 """) 484 485 inter_pulse_interval = _channel_setting("inter_pulse_interval", """Interval from the end of one pulse to the start of the next, in 486 seconds. At least `DeviceInfo.min_pulse_width_us`. 487 488 It is not the pulse period: 1 ms pulses 49 ms apart play 20 pulses per 489 second. 490 """) 491 492 burst_duration = _channel_setting("burst_duration", """Duration of each burst of pulses, in seconds. 493 494 Set to `0` to disable bursts, so that pulses continue for the whole 495 pulse train. A pulse starts only if it ends before the burst does: 496 its first phase, or for a biphasic pulse the whole pulse, so that 497 the end of a burst never cuts off a second phase. 498 """) 499 500 inter_burst_interval = _channel_setting("inter_burst_interval", """Interval between bursts of pulses, in seconds. 501 502 The channel rests at `PulsePalDevice.resting_voltage` between 503 bursts. Ignored when `PulsePalDevice.burst_duration` is `0`. 504 """) 505 506 pulse_train_duration = _channel_setting("pulse_train_duration", """Total duration of the pulse train, in seconds. 507 508 At least `DeviceInfo.min_pulse_width_us`. The end of the train cuts 509 short a monophasic pulse still playing. A biphasic pulse starts only 510 if it can end by the end of the train, so that it keeps its second 511 phase. A train's end is set when it starts: a new duration applies 512 from the next train. 513 """) 514 515 pulse_train_delay = _channel_setting("pulse_train_delay", """Delay from the trigger to the onset of the pulse train, in 516 seconds.""") 517 518 link_trigger_channel1 = _channel_setting("link_trigger_channel1", """Whether each output channel is triggered by trigger channel 1. 519 520 `True` links the output channel to trigger channel 1, `False` unlinks 521 it. By default no output channel is linked to either trigger channel, 522 so a TTL starts nothing until a script links it. 523 """) 524 525 link_trigger_channel2 = _channel_setting("link_trigger_channel2", """Whether each output channel is triggered by trigger channel 2. 526 527 `True` links the output channel to trigger channel 2, `False` unlinks 528 it. 529 """) 530 531 custom_train_id = _channel_setting("custom_train_id", """Custom pulse train played by each output channel. 532 533 `0` plays the parametrically defined train. `1` or higher plays the 534 matching custom train, previously loaded with 535 `PulsePalDevice.send_custom_pulse_train` or 536 `PulsePalDevice.send_custom_waveform`: 1-4 on Pulse Pal 3, 1-2 on 537 Pulse Pal 2 (`DeviceInfo.n_custom_pulse_trains`). 538 """) 539 540 custom_train_target = _channel_setting("custom_train_target", """What the times of a custom train mark. 541 542 `"Pulses"` if each time is the onset of a pulse, `"Bursts"` if each 543 time is the onset of a burst of pulses, which lasts 544 `PulsePalDevice.burst_duration`. A burst whose time comes while the 545 burst before plays is dropped, so that every burst played is on 546 time. `"Bursts"` needs a burst duration above 0: with 547 `PulsePalDevice.auto_sync` on, set the burst duration first, or set 548 both in a `PulsePalDevice.batch` block. 549 """) 550 551 custom_train_loop = _channel_setting("custom_train_loop", """Whether a custom train repeats. 552 553 `True` loops the custom train until 554 `PulsePalDevice.pulse_train_duration` has elapsed, `False` plays it 555 once. Each pass starts as the last pulse (or burst, for a train 556 targeting bursts) of the pass before ends. 557 `PulsePalDevice.continuous_loop` does not apply to custom trains. 558 """) 559 560 continuous_loop = _channel_setting("continuous_loop", """Whether each output channel plays its pulse train until stopped. 561 562 `False` plays the pulse train once per trigger, for 563 `PulsePalDevice.pulse_train_duration`. `True` plays it until the 564 channel is stopped, ignoring `PulsePalDevice.pulse_train_duration`. 565 For parametric trains only: a custom train ignores it, and plays as 566 `PulsePalDevice.custom_train_loop` says. Requires firmware v22 or 567 newer. The device does not report it, so 568 `PulsePalDevice.sync_from_device` leaves it as it is. 569 """) 570 571 trigger_mode = _channel_setting("trigger_mode", """Response of each trigger channel to an incoming TTL pulse. 572 573 Indexed by trigger channel number, 1 or 2 (index 0 is unused). The 574 modes, also listed in `DeviceInfo.trigger_modes`, are: 575 576 - `"Normal"`: a rising edge starts the pulse trains of the linked 577 output channels, and edges during a train are ignored. 578 - `"Toggle"`: as `"Normal"`, but a rising edge during a train stops 579 it. 580 - `"Gated"`: the trains play only while the trigger TTL is high. 581 - `"Param Sync"` (Pulse Pal 3 only): a TTL rising edge starts and 582 stops nothing. It loads the parameters most recently sent by 583 `PulsePalDevice.sync_to_device` (or at the end of a 584 `PulsePalDevice.batch` block), and the custom trains loaded since 585 the previous edge. This is how the next trial's parameters and 586 custom trains are sent during the current trial and applied the 587 instant it starts. 588 589 The integer codes 0-3 are accepted too. 590 591 An output channel that is idle at a param sync edge takes its new 592 parameters in the timer cycle the edge is detected. One that is 593 playing a pulse train finishes that train on the parameters it 594 started with, and takes the new ones the moment it ends, so a train 595 that runs past the end of a trial keeps one shape throughout and the 596 next trigger plays a whole train with the new parameters. A channel 597 in continuous loop mode has no train end, so it keeps its parameters 598 until something stops it. 599 600 While either trigger channel is in param sync mode, **only 601 `PulsePalDevice.sync_to_device` (and the end of a 602 `PulsePalDevice.batch` block) and the custom train loads are held 603 back**: with `PulsePalDevice.auto_sync` on, every assignment still 604 programs the device immediately. So leaving param sync mode means 605 assigning `trigger_mode` with `auto_sync` on; a trigger mode sent by 606 `PulsePalDevice.sync_to_device` does not take effect until a sync 607 edge arrives. A custom train loaded in param sync mode 608 (`PulsePalDevice.send_custom_pulse_train`) replaces the old one at 609 the edge: channels playing the old one at the edge finish their 610 trains on it. Leaving param sync mode discards parameters still 611 waiting for an edge, but makes a custom train still waiting current 612 at once. 613 614 A param sync channel's links to output channels are ignored. To 615 start a pulse train on the same edge, wire the TTL to the other 616 trigger channel as well: both edges arrive in the same timer cycle, 617 and the parameters are loaded first. 618 619 Connecting a new `PulsePalDevice`, and 620 `PulsePalDevice.set_default_params`, take both trigger channels out 621 of param sync mode, so that the default parameters reach the device 622 instead of waiting for a TTL. 623 624 ```python 625 P.trigger_mode[2] = "Param Sync" # sent at once 626 with P.batch(): 627 P.phase1_voltage[1] = 2.5 628 P.phase1_duration[1] = 0.002 629 # Stored by the device: the next rising edge on trigger channel 2 applies it 630 ``` 631 """) 632 633 _CURRENT_FIRMWARE_VERSION = 22 634 _OLDEST_FIRMWARE_SUPPORTED = 21 635 _BAUD_RATE = 12000000 # USB serial ignores the baud rate 636 637 _OP_MENU_BYTE = _common.OP_MENU_BYTE 638 _HANDSHAKE_OPCODE = _common.OP_HANDSHAKE 639 _HANDSHAKE_DEVICE_CYCLES_OPCODE = _common.OP_HANDSHAKE_DEVICE_CYCLES 640 _HANDSHAKE_DEVICE_CYCLES_TIMEOUT = 0.3 # Seconds to wait for a reply to op 103 before trying op 72 641 _HANDSHAKE_RESPONSE = 75 # 'K' 642 _DAC_BITMAX = 65535 643 _PARAM_MESSAGE_BYTES = 178 # Length of the parameter set sent by op 93 644 _STATUS_MESSAGE_BYTES = 29 # Length of the reply to op 101: playing channels, then 7 uint32 counters 645 # Phase 1 and 2 durations, inter-pulse interval and train duration last at least this many 646 # timer cycles, by hardware version, as in the MATLAB and C++ classes and the joystick menu. 647 # Pulse Pal 2's trigger channels read their inputs once per cycle, so a one cycle pulse from 648 # another Pulse Pal could fall between two reads. Pulse Pal 3's also latch every edge. 649 _MIN_PULSE_CYCLES = {2: 2, 3: 1} 650 651 _ENDIANNESS = "<" 652 _STRUCT_FORMATS = { 653 "uint8": "B", 654 "int8": "b", 655 "uint16": "H", 656 "int16": "h", 657 "uint32": "I", 658 "int32": "i", 659 } 660 _TYPE_RANGES = { 661 "uint8": (0, 2**8 - 1), 662 "int8": (-(2**7), 2**7 - 1), 663 "uint16": (0, 2**16 - 1), 664 "int16": (-(2**15), 2**15 - 1), 665 "uint32": (0, 2**32 - 1), 666 "int32": (-(2**31), 2**31 - 1), 667 } 668 669 _reply_pending = None # The op code of a command whose reply has not been read in full yet (see _write_raw()) 670 _unsent = frozenset() # The settings assigned with auto_sync off since the parameters were last sent or read 671 672 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 673 """Open a connection to a Pulse Pal device. 674 675 Opens the serial port, exchanges the handshake, verifies the 676 firmware version, reads the device properties into 677 `PulsePalDevice.info`, and programs the device with the default 678 parameters. 679 680 ```python 681 P = PulsePalDevice("COM3") 682 P = PulsePalDevice(serial_number="20345420") # Wherever it is plugged in 683 ``` 684 685 Args: 686 port_name: USB serial port for the Pulse Pal device, such as 687 `COM3` on Windows or `/dev/ttyACM0` on Linux. 688 serial_number: Instead of `port_name`, the device's USB serial 689 number (`DeviceInfo.serial_number`), which finds its port. 690 Pulse Pal 3 only: Pulse Pal 2 has none. 691 timeout: Serial read timeout, in seconds. 692 693 Firmware newer than this module knows (`v22`) is used with a 694 warning: new firmware only adds commands, so everything here still 695 works with it. Update the package to use what is new. 696 697 Raises: 698 PulsePalError: If the device does not return the expected 699 handshake (for example, it runs Wave Pal or Synth Pal 700 firmware), or its firmware is older than v21, or no port 701 has the serial number given. The port is closed again 702 before any error is raised. 703 serial.SerialException: If the serial port cannot be opened, 704 e.g. because another program has it open (on Linux and 705 macOS, another session of these classes, which lock it). 706 """ 707 self._closed = True 708 self._gui = None 709 self._auto_sync = True 710 self.info = DeviceInfo() 711 self._create_settings() 712 port_name = _common.port_name_from(port_name, serial_number, "PulsePalDevice") 713 self.port = serial.Serial( 714 port_name, 715 self._BAUD_RATE, 716 timeout=timeout, 717 rtscts=True, 718 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 719 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 720 # it, such as another of these classes, gets an error instead 721 exclusive=True, 722 ) 723 self._closed = False 724 try: 725 self._start_session(port_name) 726 except BaseException: 727 # Otherwise the port stays open until the object is garbage 728 # collected, and a second attempt, or WavePalDevice, cannot open 729 # it. The device is in an unknown state, so it is not sent the 730 # disconnect op. 731 self._close(send_disconnect=False) 732 raise 733 734 def _create_settings(self): 735 """Create the parameter lists, holding the default values (nothing is sent).""" 736 for code, (name, _) in _OUTPUT_PARAMETERS.items(): 737 setattr(self, "_" + name, ChannelSettings( 738 name, [_DEFAULT_OUTPUT_VALUES[name]] * 4, self, f"_apply_{name}")) 739 self._trigger_mode = ChannelSettings( 740 "trigger_mode", ["Normal"] * 2, self, "_apply_trigger_mode") 741 742 def _start_session(self, port_name): 743 """Handshake, check the firmware, and program the defaults.""" 744 # Discard anything left unread by an earlier session (one stopped mid-reply, for example): 745 # it would be read as the handshake's reply. A reply the device sends after this, to a 746 # command it had not reached yet, is skipped by read_handshake_reply(). 747 self.port.reset_input_buffer() 748 # Op 103: the session's times are in the device's own timer cycles (10 us on Pulse Pal 3). Firmware from before 749 # October 2026, Wave Pal and Synth Pal ignore it, and answer op 72, whose times are in 50 us cycles. A device that 750 # answers op 103 too late counts the session's times in 50 us cycles too, after the op 72 that follows; either 751 # way op 94 reports the session's unit, which the class then uses. 752 self._write_serial((self._OP_MENU_BYTE, self._HANDSHAKE_DEVICE_CYCLES_OPCODE), "uint8") 753 reply = _common.read_handshake_reply(self.port, timeout=self._HANDSHAKE_DEVICE_CYCLES_TIMEOUT) 754 if reply is None: 755 self._write_serial((self._OP_MENU_BYTE, self._HANDSHAKE_OPCODE), "uint8") 756 reply = _common.read_handshake_reply(self.port) 757 if reply is None: 758 raise PulsePalError( 759 f"No reply to the handshake from the device on {port_name}. Is it a Pulse Pal? " 760 f"{_common.NO_REPLY_HINT}" 761 ) 762 handshake, firmware_version = reply 763 if handshake != self._HANDSHAKE_RESPONSE: 764 if handshake in _common.FIRMWARE_BY_HANDSHAKE_REPLY: 765 raise _common.other_firmware_error(handshake, firmware_version, port_name, "Pulse Pal") 766 raise PulsePalError( 767 "Incorrect handshake returned. Expected " 768 f"{self._HANDSHAKE_RESPONSE}, received {handshake}." 769 ) 770 771 if firmware_version < self._OLDEST_FIRMWARE_SUPPORTED: 772 raise PulsePalError( 773 "Old firmware detected, v" 774 f"{firmware_version}. v{self._OLDEST_FIRMWARE_SUPPORTED} or " 775 "newer is required." 776 ) 777 if firmware_version > self._CURRENT_FIRMWARE_VERSION: 778 # New firmware only adds commands (see /Firmware/PROTOCOL.md), so this class still works with it 779 warnings.warn( 780 f"Pulse Pal firmware v{firmware_version} is newer than this pulsepal package knows " 781 f"(v{self._CURRENT_FIRMWARE_VERSION}). It works with it, but update the package to use what " 782 "is new.", stacklevel=3) 783 if firmware_version < self._CURRENT_FIRMWARE_VERSION: 784 warnings.warn( 785 f"Pulse Pal firmware v{firmware_version} is supported, but v" 786 f"{self._CURRENT_FIRMWARE_VERSION} is available: see /MATLAB/FirmwareLoader.", stacklevel=3) 787 self.info.firmware_version = firmware_version 788 789 if self.info.firmware_version > 21: 790 self._write_serial((self._OP_MENU_BYTE, 94), "uint8") 791 self.info.hardware_version = self._read_serial(1, "uint8") 792 self.info.cycle_period_us = self._read_serial(1, "uint32") # The session's time unit (see op 103 above) 793 # Not 1 / (period / 1e6): at 10 us that is 99999.99999999999, and times read back from the device would 794 # convert to 0.00010000000000000002 s instead of 0.0001 s 795 self.info.cycle_frequency = 1000000 / self.info.cycle_period_us 796 self.info.n_custom_pulse_trains = self._read_serial(1, "uint8") 797 self.info.max_custom_pulses = self._read_serial(1, "uint32") 798 self._write_serial((self._OP_MENU_BYTE, 100), "uint8") # Circuit board revision 799 revision = self.port.read(1) 800 if len(revision) < 1: 801 raise PulsePalError( 802 f"The Pulse Pal on {port_name} did not report its circuit board revision." 803 ) 804 self._reply_pending = None # Read without _read_raw() (see _write_raw()) 805 self.info.circuit_board_revision = revision[0] 806 self._write_serial((self._OP_MENU_BYTE, _common.OP_SERIAL_NUMBER), "uint8") 807 self.info.serial_number = _common.read_serial_number(self._read_raw) 808 else: 809 self.info.hardware_version = 2 810 self.info.circuit_board_revision = 0 811 self.info.serial_number = "" # v21 runs on Pulse Pal 2, which has none 812 self.info.cycle_period_us = 50 813 self.info.cycle_frequency = 20000 814 self.info.n_custom_pulse_trains = 2 815 self.info.max_custom_pulses = 5000 816 self.info.min_pulse_width_us = ( 817 self._MIN_PULSE_CYCLES[self.info.hardware_version] * self.info.cycle_period_us 818 ) 819 if not self._has_param_sync(): 820 self.info.trigger_modes = TRIGGER_MODES[:3] 821 822 # Client name op + "PYTHON" in ASCII. 823 self._write_serial( 824 (self._OP_MENU_BYTE, 89, 80, 89, 84, 72, 79, 78), 825 "uint8", 826 ) 827 828 self.set_default_params() 829 830 @staticmethod 831 def serialportlist(ports_to_list="available"): 832 """Return the names of the USB serial ports on this computer. 833 834 Called on the class, without connecting to a device, to find the 835 port name to pass to `PulsePalDevice`: 836 837 ```python 838 from pulsepal import PulsePalDevice 839 840 ports = PulsePalDevice.serialportlist() 841 P = PulsePalDevice(ports[0]) 842 ``` 843 844 Args: 845 ports_to_list: `available` to list only the ports that are 846 not already in use, or `all` to list every USB serial 847 port. Not case sensitive. 848 849 Returns: 850 Sorted list of port names, such as `["COM3", "COM7"]` on 851 Windows or `["/dev/ttyACM0"]` on Linux. 852 853 Raises: 854 PulsePalError: If `ports_to_list` is not `available` or 855 `all`. 856 """ 857 return _common.serialportlist(ports_to_list) 858 859 # ------------------------------------------------------------------ 860 # Programming the device 861 # ------------------------------------------------------------------ 862 863 @property 864 def auto_sync(self): 865 """Whether assigning a parameter programs the device at once. 866 867 `True` (the default): each assignment, such as 868 `P.phase1_voltage[1] = 5`, programs the device at once, also while 869 a trigger channel is in param sync mode. It sends the channels 870 whose value it changes, and nothing for a channel that already 871 holds the value assigned: the other channels keep what the device 872 holds, e.g. values set with its joystick (read them with 873 `PulsePalDevice.sync_from_device`). 874 875 `False`: assignments change only this object's copy of the 876 parameters, checking each value, and 877 `PulsePalDevice.sync_to_device` sends all of them in one command. 878 Turning it back on does not send changes made meanwhile, so it 879 raises `pulsepal.PulsePalError` while there are any: call 880 `PulsePalDevice.sync_to_device` first to send them, or 881 `PulsePalDevice.sync_from_device` to discard them. A 882 `PulsePalDevice.batch` block does it all for you. `close()` warns of 883 changes never sent. 884 """ 885 return self._auto_sync 886 887 @auto_sync.setter 888 def auto_sync(self, value): 889 value = to_bool(value, "auto_sync") 890 if value and self._unsent: 891 # Turned on, auto_sync would leave them unsent, and the device playing its old parameters 892 discard = "sync_from_device() to discard them" 893 if "continuous_loop" in self._unsent: 894 discard += " (all but continuous_loop, which the device does not report)" 895 raise PulsePalError( 896 f"Changes made with auto_sync off have not been sent to the device: {self._unsent_names()}. Call " 897 f"sync_to_device() to send them, or {discard}, before turning auto_sync on." 898 ) 899 self._auto_sync = value 900 901 def batch(self): 902 """Change several parameters, and send them in one command. 903 904 Returns a context manager. In its block, assignments change only 905 this object's copy of the parameters (as with 906 `PulsePalDevice.auto_sync` off), and when the block ends, 907 `PulsePalDevice.sync_to_device` sends all of them at once. If the 908 block raises an error, nothing is sent, and the parameters go 909 back to what they were. 910 911 ```python 912 with P.batch(): 913 P.phase1_voltage = [5, 5, 2.5, 2.5] 914 P.phase1_duration[3] = 0.002 915 ``` 916 917 In param sync mode, the device stores the parameters for the next 918 rising edge on the param sync channel (see 919 `PulsePalDevice.trigger_mode`). 920 """ 921 return _common.batch(self) 922 923 def sync_to_device(self): 924 """Program the device with this object's copy of all parameters. 925 926 Sends every output and trigger parameter to the device in a single 927 command. Use it after assignments made with 928 `PulsePalDevice.auto_sync` off; with it on, they have already 929 reached the device. 930 931 ```python 932 P.auto_sync = False 933 P.phase1_voltage[1:5] = [5] * 4 934 P.sync_to_device() 935 ``` 936 937 On Pulse Pal 3, if either trigger channel is in param sync mode 938 (`PulsePalDevice.trigger_mode` `"Param Sync"`), the device stores 939 the parameters instead of programming them, and loads them on the 940 next rising edge of that channel. The device still checks every 941 value. This is the only method whose effect is deferred that way. 942 943 Raises: 944 PulsePalError: If a value is out of range for its parameter 945 (nothing is sent), or the device does not acknowledge the 946 command. 947 """ 948 self._check_sync() 949 self._send_sync() 950 951 def sync_from_device(self): 952 """Read all parameters from the device into this object's copy. 953 954 Overwrites the parameter lists with the program currently on the 955 device, e.g. after it has been reprogrammed from its thumb 956 joystick. `PulsePalDevice.continuous_loop` is left as it is: the 957 device does not report it. Changes made with 958 `PulsePalDevice.auto_sync` off and not yet sent are discarded, but 959 for `PulsePalDevice.continuous_loop`. 960 961 Requires firmware v22 or newer. 962 963 Raises: 964 PulsePalError: If the connected firmware is older than v22, 965 or the device does not return the full parameter set. 966 """ 967 self._require_firmware(22, "sync_from_device()") 968 for name, values in self._read_device_params().items(): 969 getattr(self, name)._store(values) 970 self._unsent = self._unsent & {"continuous_loop"} # Not read: a change to it is still unsent 971 972 def set_default_params(self): 973 """Program the device with the default parameters. 974 975 The defaults are monophasic +5 V pulses of 0.1 ms, 1 ms apart 976 (about 909 a second), for 1 second, resting at 0 V, on all four 977 output channels; for biphasic pulses, a phase 2 of -5 V and 0.1 ms 978 after 0.1 ms at rest. No output channel is linked to a trigger 979 channel, so a TTL starts nothing until a script links it, and both 980 trigger channels are in normal mode. They are the device's own 981 defaults, which it also loads at startup, after a comm failure 982 and after formatting its microSD card. They are sent at once, 983 also while `PulsePalDevice.auto_sync` is off. The constructor 984 calls it. 985 """ 986 for name, value in _DEFAULT_OUTPUT_VALUES.items(): 987 getattr(self, name)._store([value] * 4) 988 self._trigger_mode._store(["Normal"] * 2) 989 if self._has_param_sync(): 990 # A device left in param sync mode would store the sync below instead of running it, 991 # leaving the device on its old program until a TTL arrived. A trigger mode sent on 992 # its own is not deferred that way, so both trigger channels leave param sync mode 993 # first. See PulsePalDevice.trigger_mode. 994 self._send_trigger_modes(["Normal"] * 2, "set_default_params()") 995 self._send_sync() 996 997 def export_params(self): 998 """Return every parameter, as a dict of plain lists. 999 1000 Keyed by parameter name (`DeviceInfo.output_parameter_names`, then 1001 `"trigger_mode"`), with one value per channel and no unused index 1002 0. It holds only numbers, booleans and names, so it can be saved 1003 with `json` and logged with your data, to record exactly what the 1004 device played. `PulsePalDevice.import_params` programs it again. 1005 1006 ```python 1007 import json 1008 with open("trial_params.json", "w") as f: 1009 json.dump(P.export_params(), f) 1010 ``` 1011 """ 1012 names = [*self.info.output_parameter_names, "trigger_mode"] 1013 return {name: list(getattr(self, name))[1:] for name in names} 1014 1015 def import_params(self, params): 1016 """Program the device with parameters exported by `PulsePalDevice.export_params`. 1017 1018 All of them are checked first, and then sent in one command, as 1019 at the end of a `PulsePalDevice.batch` block: in param sync mode, 1020 the device stores them for the next sync edge. Parameters missing 1021 from `params` keep their values. A name that is not a parameter 1022 is skipped with a warning, so that parameters exported by a newer 1023 version of this package, which may have more, can still be 1024 imported; if none is, an error is raised. 1025 1026 A whole program file, as the GUIs save it, is imported too: its 1027 parameters, not its custom trains (a warning says so, if it has 1028 any). 1029 1030 ```python 1031 with open("MyProgram.json") as f: 1032 P.import_params(json.load(f)) 1033 ``` 1034 1035 Args: 1036 params: A dict of parameter name to one value per channel (a 1037 list, tuple or NumPy array), as `export_params` returns, 1038 or a program file's contents. The MATLAB class's names 1039 (`phase1Voltage`) are accepted too, so that the parameters 1040 its `exportParams()` returns, saved with `jsonencode`, can 1041 be imported. 1042 1043 Raises: 1044 PulsePalError: If a value is invalid, or no name in `params` is 1045 a parameter. Nothing is sent, and the parameters keep the 1046 values they had. 1047 """ 1048 params = _params_to_import(params) 1049 with self.batch(): 1050 for name, values in params.items(): 1051 setattr(self, name, values) 1052 1053 def set_fixed_voltage(self, channels, voltage): 1054 """Set output channels to a fixed voltage. 1055 1056 Each channel holds the voltage until it is set again or until a 1057 pulse train is triggered on it. 1058 1059 ```python 1060 P.set_fixed_voltage(4, 2.5) 1061 P.set_fixed_voltage([1, 3], -1) 1062 ``` 1063 1064 Args: 1065 channels: Output channel number, 1-4, or several as a list, 1066 tuple or NumPy array. 1067 voltage: Voltage to set on each of them, in volts [-10, 10]. 1068 1069 Raises: 1070 PulsePalError: If a channel number is not 1-4, the voltage is 1071 outside [-10, 10] (nothing is sent), or the device does not 1072 acknowledge the command. 1073 """ 1074 channels = _common.channel_numbers(channels) 1075 voltage_bits = self._volts_to_bits(voltage, "voltage") 1076 for channel in channels: 1077 self._write_serial( 1078 (self._OP_MENU_BYTE, 79, channel), 1079 "uint8", 1080 voltage_bits, 1081 "uint16", 1082 ) 1083 self._read_ack("set_fixed_voltage()") 1084 1085 def set_calibration(self, channel, voltage_offset): 1086 """Calibrate the zero code of an output channel. 1087 1088 The offset is added to every voltage the channel produces, to 1089 correct for DAC offset error. It is stored in the device's 1090 EEPROM and reloaded on boot, so it only needs to be set once. 1091 1092 Requires Pulse Pal hardware v3 or newer. 1093 1094 Args: 1095 channel: Output channel number, 1-4. 1096 voltage_offset: Offset to apply, in volts [-0.1, 0.1]. 1097 1098 Raises: 1099 PulsePalError: If the connected hardware is older than v3, 1100 `channel` is not 1-4, `voltage_offset` is outside 1101 [-0.1, 0.1], or the device does not acknowledge the 1102 command. 1103 """ 1104 if self.info.hardware_version < 3: 1105 raise PulsePalError( 1106 "set_calibration() requires hardware v3 or newer." 1107 ) 1108 if not (isinstance(channel, numbers.Integral) and channel in (1, 2, 3, 4)): 1109 raise PulsePalError("channel must be 1, 2, 3 or 4") 1110 if not (isinstance(voltage_offset, numbers.Real) and -0.1 <= voltage_offset <= 0.1): # Also refuses NaN 1111 raise PulsePalError( 1112 "voltage_offset for zero code calibration must be in range " 1113 "[-0.1, 0.1]" 1114 ) 1115 # To the nearest DAC code, halfway values to the even one, as the MATLAB class rounds it 1116 voltage_bits = int(round(voltage_offset * (1 / (20 / 65536)))) 1117 self._write_serial( 1118 (self._OP_MENU_BYTE, 96, channel - 1), 1119 "uint8", 1120 voltage_bits, 1121 "int16", 1122 ) 1123 self._read_ack("set_calibration()") 1124 1125 def set_screen_saver(self, enabled, timeout=1800): 1126 """Switch the device's screen saver on or off, and set its timeout. 1127 1128 With the screen saver on, the device dims its screen once it has 1129 been left alone for `timeout` seconds: no command from the 1130 computer, no rising edge on a trigger channel, and no joystick 1131 click or push. The next of these brings the screen back. Both 1132 settings are stored in the device's EEPROM and kept through power 1133 cycles; the screen saver can also be switched on and off from the 1134 device's joystick menu. A new device has it on, with 1800 s. 1135 1136 The device saves the settings once no channel is playing. Saving 1137 pauses its timer, usually for about 20 us but, once in about 2000 1138 changes, for tens of milliseconds, which would delay a trigger. 1139 Change the settings before an experiment rather than during one. 1140 1141 Requires firmware v22 or newer. Only Pulse Pal 3 has a screen 1142 saver: Pulse Pal 2 accepts `enabled=False` only. 1143 1144 ```python 1145 P.set_screen_saver(True, 300) # dim after 5 minutes 1146 P.set_screen_saver(False) 1147 ``` 1148 1149 Args: 1150 enabled: True (or 1) to switch the screen saver on, False (or 1151 0) to switch it off. 1152 timeout: Seconds without activity before the screen dims, a 1153 whole number from 1 to 65535. It is sent with every call, 1154 so leaving it out sets 1800 s. 1155 1156 Raises: 1157 PulsePalError: If the firmware is older than v22, the 1158 screen saver is switched on with hardware older than v3, 1159 a value is out of range, or the device does not 1160 acknowledge the command. 1161 """ 1162 self._require_firmware(22, "set_screen_saver()") 1163 enabled = to_bool(enabled, "enabled") 1164 if not ( 1165 isinstance(timeout, numbers.Real) 1166 and not isinstance(timeout, bool) 1167 and math.isfinite(timeout) 1168 and timeout == int(timeout) 1169 and 1 <= timeout <= 65535 1170 ): 1171 raise PulsePalError( 1172 "timeout must be a whole number of seconds from 1 to 65535" 1173 ) 1174 if enabled and (self.info.hardware_version or 2) < 3: 1175 raise PulsePalError( 1176 "The screen saver requires hardware v3 or newer: Pulse Pal 2 " 1177 "accepts set_screen_saver(False) only." 1178 ) 1179 self._write_serial( 1180 (self._OP_MENU_BYTE, 99, int(enabled)), 1181 "uint8", 1182 int(timeout), 1183 "uint16", 1184 ) 1185 self._read_ack("set_screen_saver()") 1186 1187 # ------------------------------------------------------------------ 1188 # Custom trains, playback and settings files 1189 # ------------------------------------------------------------------ 1190 1191 def send_custom_pulse_train( 1192 self, 1193 custom_train_id, 1194 pulse_times, 1195 voltages, 1196 ): 1197 """Load a custom pulse train onto the device. 1198 1199 A custom pulse train is an arbitrary list of pulse onset times 1200 and voltages, replacing the parametric pulse voltage and onset 1201 timing. Set `PulsePalDevice.custom_train_id` on an output channel 1202 to play the train there. Each pulse takes the channel's own phase 1203 durations; a biphasic pulse's second phase is its voltage with the 1204 sign reversed. A biphasic pulse always plays whole, and a pulse 1205 whose time comes while it plays is dropped, so that every pulse 1206 played is on time. 1207 1208 ```python 1209 P.send_custom_pulse_train( 1210 2, [0, 0.2, 0.5, 1], [8, 4, -3.5, -10] 1211 ) 1212 P.custom_train_id[1] = 2 1213 ``` 1214 1215 Loading a train stops the output channels playing it, and their 1216 next trigger plays the new train. On Pulse Pal 3, while a trigger 1217 channel is in param sync mode (see `PulsePalDevice.trigger_mode`), 1218 the device stores the train instead, as it stores the parameters 1219 `PulsePalDevice.sync_to_device` sends: channels go on playing the 1220 old train, and the next rising edge on the param sync channel 1221 makes the new one current. A channel playing the old train at that 1222 edge finishes it, and plays the new one from its next trigger. So 1223 the next trial's custom trains can be loaded during the current 1224 trial. Leaving param sync mode makes a stored train current at 1225 once. 1226 1227 Args: 1228 custom_train_id: Custom train to load, from 1 to 1229 `DeviceInfo.n_custom_pulse_trains` (2 on Pulse Pal 2, 1230 4 on Pulse Pal 3). 1231 pulse_times: Pulse onset times, in seconds, relative to the 1232 start of the train, increasing. Each is a multiple of 1233 `DeviceInfo.min_pulse_width_us` (10 us on Pulse Pal 3, 1234 100 us on Pulse Pal 2), from 0 to 1235 `MAX_TIME`. Accepts a list, tuple or 1-D NumPy array. 1236 voltages: Voltage of each pulse, in volts [-10, 10]. Must be 1237 the same length as `pulse_times`. 1238 1239 Raises: 1240 PulsePalError: If `custom_train_id` is out of range, 1241 `pulse_times` or `voltages` is not one-dimensional (a 2-D 1242 array is refused, not flattened), they differ in length, 1243 there are no pulses, or more than 1244 `DeviceInfo.max_custom_pulses`, a pulse time is not a 1245 multiple of `DeviceInfo.min_pulse_width_us`, is out of 1246 range or is not later than the one before it, or the 1247 device does not acknowledge the command. 1248 """ 1249 pulse_times_cycles = self._custom_times_to_cycles(pulse_times, "pulse_times") 1250 pulse_voltage_bits = self._volts_array_to_bits(voltages, "voltages") 1251 if len(pulse_times_cycles) != len(pulse_voltage_bits): 1252 raise PulsePalError( 1253 "pulse_times and voltages must be the same length." 1254 ) 1255 self._send_custom_train( 1256 custom_train_id, 1257 pulse_times_cycles, 1258 pulse_voltage_bits, 1259 "send_custom_pulse_train()", 1260 ) 1261 1262 def send_custom_waveform( 1263 self, 1264 custom_train_id, 1265 sampling_period, 1266 voltages, 1267 ): 1268 """Load an arbitrary waveform onto the device. 1269 1270 A convenience shorthand for 1271 `PulsePalDevice.send_custom_pulse_train` with evenly spaced, 1272 adjoining pulses, so that `voltages` is played as a waveform 1273 sampled every `sampling_period` seconds. It is loaded as a custom 1274 train is: see `PulsePalDevice.send_custom_pulse_train` for what 1275 happens to channels playing it, and in param sync mode. 1276 1277 Set the channel's `PulsePalDevice.phase1_duration` to 1278 `sampling_period` as well, so that each sample is held for the 1279 sampling period. 1280 1281 ```python 1282 import math 1283 1284 samples = [math.sin(i / 10.0) * 10 for i in range(1000)] 1285 P.send_custom_waveform(1, 0.001, samples) # 1 kHz 1286 P.custom_train_id[2] = 1 1287 P.phase1_duration[2] = 0.001 1288 ``` 1289 1290 Args: 1291 custom_train_id: Custom train to load, from 1 to 1292 `DeviceInfo.n_custom_pulse_trains` (2 on Pulse Pal 2, 1293 4 on Pulse Pal 3). 1294 sampling_period: In seconds: a multiple of 1295 `DeviceInfo.min_pulse_width_us` (10 us on Pulse Pal 3, 1296 100 us on Pulse Pal 2). Each voltage is 1297 held for this long. 1298 voltages: Waveform samples, in volts [-10, 10]. 1299 Accepts a list, tuple or 1-D NumPy array. 1300 1301 Raises: 1302 PulsePalError: If `custom_train_id` is out of range, there 1303 are no samples, or more than 1304 `DeviceInfo.max_custom_pulses`, `sampling_period` is not a 1305 positive multiple of `DeviceInfo.min_pulse_width_us`, the 1306 last sample would start after `MAX_TIME`, or the device 1307 does not acknowledge the command. 1308 """ 1309 pulse_voltage_bits = self._volts_array_to_bits(voltages, "voltages") 1310 period_cycles = self._custom_time_cycles(sampling_period, "sampling_period") 1311 if period_cycles == 0: 1312 raise PulsePalError( 1313 f"sampling_period must be a positive multiple of {self.info.min_pulse_width_us:g} us. " 1314 f"Received {sampling_period!r}." 1315 ) 1316 pulse_times = period_cycles * np.arange(len(pulse_voltage_bits), dtype=np.int64) 1317 self._send_custom_train( 1318 custom_train_id, 1319 pulse_times, 1320 pulse_voltage_bits, 1321 "send_custom_waveform()", 1322 ) 1323 1324 def trigger(self, channels): 1325 """Trigger output channels in software. 1326 1327 The channels start their pulse trains together, in the same timer 1328 cycle. A channel that is already playing a pulse train ignores 1329 the trigger, and `PulsePalDevice.stop` cancels a trigger that has 1330 not started its channel yet. 1331 1332 ```python 1333 P.trigger(3) # one channel 1334 P.trigger([1, 4]) # several: a list, tuple or NumPy array 1335 ``` 1336 1337 Args: 1338 channels: Output channel number, 1-4, or several as a list, 1339 tuple or NumPy array. 1340 1341 Raises: 1342 PulsePalError: If a channel number is not 1-4, or no channel 1343 is given. Nothing is triggered. 1344 """ 1345 self._write_serial((self._OP_MENU_BYTE, 77, _common.channel_bits(channels)), "uint8") 1346 1347 def stop(self, channels=None): 1348 """Stop pulse trains currently playing on the device. 1349 1350 The stopped channels return to their 1351 `PulsePalDevice.resting_voltage`. A soft trigger that has not 1352 started its channel yet is cancelled too. 1353 1354 ```python 1355 P.stop() # all output channels 1356 P.stop([1, 3, 4]) # channels 1, 3 and 4 1357 ``` 1358 1359 Args: 1360 channels: Output channel number, 1-4, or several as a list, 1361 tuple or NumPy array. `None`, the default, stops all 1362 channels. Stopping some channels requires firmware v22 1363 or newer. 1364 1365 Raises: 1366 PulsePalError: If a channel number is not 1-4, or channels are 1367 given and the firmware is older than v22. 1368 """ 1369 if self.info.firmware_version < 22: 1370 if channels is not None: 1371 raise PulsePalError("stop() cannot address individual channels before firmware v22") 1372 self._write_serial((self._OP_MENU_BYTE, 80), "uint8") # Stops all channels 1373 return 1374 bits = 0x0F if channels is None else _common.channel_bits(channels) 1375 self._write_serial((self._OP_MENU_BYTE, 98, bits), "uint8") 1376 1377 def status(self): 1378 """Read which channels are playing, and the counters since the last call. 1379 1380 The counters (see `DeviceStatus`) count from the previous call, or 1381 from the device's startup, and each call clears them. Calling it 1382 before and after a trial checks that the TTLs the trial sent 1383 arrived, and started the trains they should: 1384 1385 ```python 1386 P.status() # Clears the counters 1387 run_trial() # Your code: one TTL on trigger channel 1, linked to channels 1 and 2 1388 s = P.status() 1389 if s.trigger_edges[1] != 1 or s.channel_starts[1:3] != [1, 1]: 1390 print("The trial's trains did not all start") 1391 ``` 1392 1393 Requires firmware v22 or newer. 1394 1395 Returns: 1396 A `DeviceStatus`. 1397 1398 Raises: 1399 PulsePalError: If the firmware is older than v22, or the device 1400 does not reply. 1401 """ 1402 self._require_firmware(22, "status()") 1403 self._write_serial((self._OP_MENU_BYTE, 101), "uint8") 1404 values = struct.unpack(f"{self._ENDIANNESS}B7I", self._read_raw(self._STATUS_MESSAGE_BYTES)) 1405 return DeviceStatus( 1406 playing=[ch for ch in range(1, 5) if values[0] & (1 << (ch - 1))], 1407 trigger_edges=[None, *values[1:3]], 1408 channel_starts=[None, *values[3:7]], 1409 comm_failures=values[7], 1410 ) 1411 1412 def save_program_to_microsd(self, file_name): 1413 """Save the parameters to the device's microSD card, as a program. 1414 1415 The program can be loaded later with 1416 `PulsePalDevice.load_program_from_microsd` or with the joystick 1417 menu's Load Program item. A file of the same name is replaced. To 1418 keep the parameters on this computer instead, see 1419 `PulsePalDevice.export_params`. 1420 1421 ```python 1422 P.save_program_to_microsd("MyProtocol.pps") 1423 ``` 1424 1425 Args: 1426 file_name: At most 15 ASCII characters, ending in `.pps`. 1427 1428 Raises: 1429 PulsePalError: If the file name is invalid, or the device could 1430 not write the file to its microSD card. Pulse Pal 2 also 1431 refuses while a channel is playing: its card shares a bus with 1432 the outputs, so stop playback first. 1433 """ 1434 self._settings_file_op(file_name, 1, "save_program_to_microsd()") 1435 1436 def load_program_from_microsd(self, file_name): 1437 """Load a program from the device's microSD card. 1438 1439 The device plays the program, and the parameters are read back 1440 into this object (see `PulsePalDevice.sync_from_device`). The 1441 program's trigger modes take effect too, and it is loaded at once, 1442 also in param sync mode. 1443 1444 Args: 1445 file_name: The program's file name, ending in `.pps`, as saved 1446 by `PulsePalDevice.save_program_to_microsd` or the joystick 1447 menu. 1448 1449 Raises: 1450 PulsePalError: If the file name is invalid, or the device could 1451 not load the program: there is no such file on its microSD 1452 card, or it is not a valid Pulse Pal program; or, on Pulse 1453 Pal 2, a channel is playing (see `save_program_to_microsd`). 1454 The device then keeps its parameters, and so does this object. 1455 """ 1456 self._settings_file_op(file_name, 2, "load_program_from_microsd()") 1457 1458 def delete_program_from_microsd(self, file_name): 1459 """Delete a program from the device's microSD card. 1460 1461 Args: 1462 file_name: The program's file name, ending in `.pps`. 1463 1464 Raises: 1465 PulsePalError: If the file name is invalid, or the device could 1466 not delete the file, e.g. because there is no such file, or, 1467 on Pulse Pal 2, because a channel is playing. 1468 """ 1469 self._settings_file_op(file_name, 3, "delete_program_from_microsd()") 1470 1471 def _settings_file_op(self, file_name, op_byte, context): 1472 """Save (op_byte 1), load (2) or delete (3) a settings file, with op 90.""" 1473 valid = ( 1474 isinstance(file_name, str) 1475 and all(32 <= ord(character) <= 126 for character in file_name) # Printable ASCII 1476 and file_name.lower().endswith(".pps") 1477 and 4 < len(file_name) <= 15 1478 ) 1479 if not valid: 1480 raise PulsePalError( 1481 f"{context}: the file name must be 1 to 11 ASCII characters followed by .pps, " 1482 f"e.g. 'Protocol1.pps'. Received {file_name!r}." 1483 ) 1484 filename_bytes = file_name.encode("ascii") 1485 self._write_serial( 1486 (self._OP_MENU_BYTE, 90, op_byte, len(filename_bytes)), 1487 "uint8", 1488 list(filename_bytes), 1489 "uint8", 1490 ) 1491 if self.info.firmware_version > 21: 1492 # Sent after the file operation has finished. A refused load changes nothing on the device. 1493 refusal = { 1494 1: f"Pulse Pal could not save {file_name!r} to its microSD card. Is the card in place?", 1495 2: f"Pulse Pal could not load {file_name!r}: there is no such file on its microSD card, or it is not " 1496 "a valid Pulse Pal program. The device keeps its parameters.", 1497 3: f"Pulse Pal could not delete {file_name!r}: there is no such file on its microSD card.", 1498 }[op_byte] 1499 # Pulse Pal 2 replies 2 while a channel plays: its microSD card shares the SPI bus with the DAC 1500 busy = (f"Pulse Pal 2 does not use its microSD card while a channel is playing, because the card shares a " 1501 f"bus with the outputs, so {file_name!r} was not {('saved', 'loaded', 'deleted')[op_byte - 1]}. " 1502 "Stop playback first, with stop().") 1503 if op_byte == 2: 1504 busy += " The device keeps its parameters." 1505 self._read_ack(context, refusal_message=refusal, refusal_messages={2: busy}) 1506 else: 1507 self._reply_pending = None # Firmware v21 does not acknowledge op 90 (see _write_raw()) 1508 if op_byte == 2: 1509 time.sleep(0.1) # So allow time for the load 1510 if op_byte == 2: 1511 self.sync_from_device() 1512 1513 def format_microsd(self, *, confirm=True, timeout=30): 1514 """Format the device's microSD card. 1515 1516 Erases every program saved on the device and resets its 1517 parameters to the defaults. By default, asks at the console for 1518 confirmation first; a script that runs on its own, or an AI agent, 1519 passes `confirm=False`. 1520 1521 Requires Pulse Pal hardware v3 or newer. 1522 1523 Args: 1524 confirm: If `True`, ask at the console before anything is 1525 erased. If `False`, format at once. 1526 timeout: Seconds to wait for the device to report that 1527 formatting has finished. 1528 1529 Returns: 1530 `True` once the card has been formatted, or `False` if the 1531 user declines the confirmation prompt. 1532 1533 Raises: 1534 PulsePalError: If the connected hardware is older than v3, if 1535 the device reports that formatting failed, or if it does 1536 not report a result within `timeout` seconds. 1537 """ 1538 if self.info.hardware_version < 3: 1539 raise PulsePalError( 1540 "format_microsd() requires hardware v3 or newer." 1541 ) 1542 1543 if to_bool(confirm, "confirm"): 1544 print("*** Pulse Pal microSD Formatter ***") 1545 print("This will format Pulse Pal's microSD card,") 1546 print("erase all programs saved on the device") 1547 print("and reset all parameters to defaults.") 1548 reply = input("Do you want to continue (y/n)") 1549 if reply.strip().lower() != "y": 1550 print("Choice confirmed - microSD Card NOT formatted.") 1551 return False 1552 1553 self._write_serial((self._OP_MENU_BYTE, 97), "uint8", list(_FORMAT_KEY), "uint8") 1554 1555 # The device replies with lines of status text, the last of which 1556 # contains "!", and then a confirm byte (1 if the card was formatted, 1557 # 0 if not). The confirm byte is sent after the device has reloaded 1558 # its default parameters, so it can arrive well after the text. It 1559 # must be read here: left in the buffer, it would be taken as the 1560 # reply to the next command, and every reply after that would be 1561 # read one byte late. 1562 start = time.time() 1563 message = bytearray() 1564 flag_index = -1 1565 line_end = -1 1566 1567 while time.time() - start < timeout: 1568 n_waiting = self.port.in_waiting 1569 if n_waiting: 1570 message.extend(self.port.read(n_waiting)) 1571 flag_index = message.find(b"!") 1572 if flag_index >= 0: 1573 line_end = message.find(b"\n", flag_index) 1574 if line_end >= 0 and len(message) > line_end + 1: 1575 break 1576 time.sleep(0.01) 1577 if line_end < 0 or len(message) <= line_end + 1: 1578 raise PulsePalError( 1579 "Pulse Pal did not report the result of formatting " 1580 f"its microSD card within {timeout} s. {_common.NO_REPLY_HINT}" 1581 ) 1582 self._reply_pending = None # The whole reply is read (see _write_raw()) 1583 confirm_byte = message[line_end + 1] 1584 1585 text = bytes(message[:flag_index]).decode( 1586 "ascii", errors="replace" 1587 ).rstrip() 1588 if text: 1589 print(text) 1590 1591 # The device has loaded its own defaults. They are this class's, but the device keeps no record of 1592 # this object's other changes (continuous loop mode, for one), so the defaults are programmed, as the 1593 # MATLAB class does, and this object describes the device again. 1594 self.set_default_params() 1595 if confirm_byte != 1: 1596 raise PulsePalError( 1597 "Pulse Pal could not format its microSD card." 1598 ) 1599 return True 1600 1601 def gui(self, block=None, theme=None): 1602 """Open the Pulse Pal parameter GUI, or focus an open one. 1603 1604 The GUI edits its own copy of the parameters, and loads them to 1605 the device when its 'Load to Device' button is clicked. The 1606 window closes automatically when the device is closed or 1607 deleted. 1608 1609 Calling this while the GUI is already open focuses the existing 1610 window rather than opening a second one. 1611 1612 Args: 1613 block: If `True`, the call returns when the GUI is closed. If 1614 `False`, the call returns immediately, and the host 1615 application must run the Tk event loop. If `None`, the 1616 GUI blocks only when the host does not already provide a 1617 Tk event loop, e.g. when launched from a script. 1618 theme: `"light"` or `"dark"` to select the color theme, or 1619 `None` to match the desktop theme. Passing a theme to an 1620 already-open GUI recolors it in place. 1621 1622 Returns: 1623 The `pulsepal.gui.PulsePalGUI` instance driving the window. 1624 1625 Raises: 1626 ValueError: If the theme name is not recognized. 1627 """ 1628 gui = getattr(self, "_gui", None) 1629 if gui is not None and not gui.is_closed: 1630 if theme is not None: 1631 gui.set_theme(theme) 1632 gui.focus() 1633 return gui 1634 1635 from .gui import PulsePalGUI 1636 1637 gui = PulsePalGUI(self, theme=theme) 1638 self._gui = gui 1639 gui.start(block=block) 1640 return gui 1641 1642 # ------------------------------------------------------------------ 1643 # Connection 1644 # ------------------------------------------------------------------ 1645 1646 def close(self): 1647 """Close the connection to the device, and the GUI if open. 1648 1649 The device stops all output channels when the client disconnects, 1650 and shows its own name on its screen again. Safe to call more 1651 than once; later calls do nothing. Called automatically when 1652 leaving a `with` block and when the object is garbage collected, 1653 so pulse trains also stop when the last reference to the object 1654 goes, e.g. when a function that created it returns. 1655 """ 1656 self._close(send_disconnect=True) 1657 1658 def _close(self, send_disconnect): 1659 """Close the connection. send_disconnect: tell the device first (op 81). False when the device is in 1660 an unknown state, such as after a failed handshake.""" 1661 gui = getattr(self, "_gui", None) 1662 self._gui = None 1663 if gui is not None: 1664 try: 1665 gui.close() 1666 except Exception: 1667 # Cleanup must not raise; Tk may already be torn down 1668 pass 1669 1670 if getattr(self, "_closed", True): 1671 return 1672 self._closed = True 1673 if send_disconnect and self._unsent: 1674 _common.warn(f"PulsePalDevice closed with changes made with auto_sync off that were never sent to the " 1675 f"device: {self._unsent_names()}.") 1676 self._reply_pending = None # Nothing more is read, so a reply still to come needs no discarding 1677 try: 1678 if send_disconnect and self.port and self.port.is_open: 1679 self._write_serial((self._OP_MENU_BYTE, 81), "uint8") 1680 except Exception: 1681 pass # The port may already be gone, e.g. the cable was unplugged 1682 finally: 1683 if self.port and self.port.is_open: 1684 self.port.close() 1685 1686 def __enter__(self): 1687 """Enter a `with` block, returning the connected device.""" 1688 return self 1689 1690 def __exit__(self, exc_type, exc_value, traceback): 1691 """Disconnect and close the port when leaving a `with` block. 1692 1693 Returns: 1694 `False`, so any exception raised in the block propagates. 1695 """ 1696 self.close() 1697 return False 1698 1699 def __del__(self): 1700 """Disconnect and close the port when the object is collected.""" 1701 try: 1702 self.close() 1703 except Exception: 1704 # Destructors should not raise; the serial object may already be 1705 # gone during interpreter shutdown. 1706 pass 1707 1708 def __setattr__(self, name, value): 1709 """Refuse a public attribute that is not a setting, such as a misspelled one: it would program nothing (see 1710 _common.refuse_unknown_attribute()).""" 1711 if name[0] != "_": 1712 _common.refuse_unknown_attribute(self, name, _SETTING_NAMES) 1713 super().__setattr__(name, value) 1714 1715 def __repr__(self): 1716 """Describe the device and its parameters, e.g. with `print(P)`.""" 1717 port = getattr(getattr(self, "port", None), "port", None) 1718 lines = [ 1719 f"PulsePalDevice on {port} (Pulse Pal {self.info.hardware_version}, " 1720 f"firmware v{self.info.firmware_version})", 1721 f"auto_sync: {self._auto_sync}", 1722 ] 1723 for name in self.info.output_parameter_names: 1724 lines.append(f"{name}: {list(getattr(self, name))}") 1725 lines.append(f"trigger_mode: {list(self._trigger_mode)}") 1726 return "\n".join(lines) 1727 1728 # ------------------------------------------------------------------ 1729 # Internals: parameters 1730 # ------------------------------------------------------------------ 1731 1732 def _has_param_sync(self): 1733 """Param sync mode is on Pulse Pal 3, with firmware v22 or newer.""" 1734 return (self.info.hardware_version or 2) > 2 and (self.info.firmware_version or 0) > 21 1735 1736 def _normalize_output_value(self, code, value, channel): 1737 """Check a value for an output parameter, and return it as the parameter's list holds it: 1738 a time as the device plays it, rounded to its timer cycle. 1739 1740 Raises PulsePalError, before anything is sent, for a value the device cannot play. The 1741 device refuses most of them too, but it also resets them (see validateOutputParams() in 1742 /Firmware/PulsePal/USBOps.ino), so the local copy would describe a different program. 1743 """ 1744 name, kind = _OUTPUT_PARAMETERS[code] 1745 label = f"{name} on channel {channel}" 1746 if kind == "bool": 1747 value = to_bool(value, label) 1748 if code == _CONTINUOUS_LOOP_CODE and value and self.info.firmware_version < 22: 1749 raise PulsePalError("continuous_loop requires firmware v22 or newer.") 1750 return value 1751 if kind == "volts": 1752 self._volts_to_bits(value, label) 1753 return float(value) 1754 if kind in ("time", "pulse_time"): 1755 cycles = self._seconds_to_cycles(value, label) 1756 # Checked in whole cycles, as the device receives it: 100 * 1e-6 is just under 0.0001, 1757 # but is exactly 10 cycles of 10 us 1758 if kind == "pulse_time" and cycles < self._min_pulse_cycles(): 1759 raise PulsePalError( 1760 f"{label} must be at least {self._min_pulse_seconds()} s " 1761 f"({self._min_pulse_cycles_text()} of the device's {self.info.cycle_period_us} us " 1762 f"timer), so that trigger channels can detect the pulses. Received {value!r}." 1763 ) 1764 return self._cycles_to_seconds(cycles) # The time the device plays 1765 if kind == "train_id": 1766 self._check_whole_number(value, label, 0, self.info.n_custom_pulse_trains) 1767 return int(value) 1768 # "target" 1769 return to_name(value, CUSTOM_TRAIN_TARGETS, label, codes={"Pulses": 0, "Bursts": 1}) 1770 1771 def _normalize_trigger_mode(self, value): 1772 modes = self.info.trigger_modes 1773 return to_name(value, modes, "trigger_mode", codes={mode: TRIGGER_MODES.index(mode) for mode in modes}) 1774 1775 def _encode_output_param(self, code, values): 1776 """Convert output parameter values to device units. 1777 1778 Returns the converted values and the datatype the device reads for this 1779 parameter code: DAC codes for voltages, hardware timer cycles for times, 1780 and bytes for everything else. 1781 """ 1782 name, kind = _OUTPUT_PARAMETERS[code] 1783 if kind == "volts": 1784 return [self._volts_to_bits(v, name) for v in values], "uint16" 1785 if kind in ("time", "pulse_time"): 1786 return [self._seconds_to_cycles(v, name) for v in values], "uint32" 1787 if kind == "target": 1788 return [CUSTOM_TRAIN_TARGETS.index(v) for v in values], "uint8" 1789 return [int(v) for v in values], "uint8" 1790 1791 def _apply_output_param(self, code, values): 1792 """Check new values for all four channels of an output parameter, and with auto_sync on, 1793 program the channels whose value changes. Returns them as the parameter's list holds them.""" 1794 values = [self._normalize_output_value(code, v, ch) for ch, v in enumerate(values, start=1)] 1795 name = _OUTPUT_PARAMETERS[code][0] 1796 if not self._auto_sync: 1797 self._unsent = self._unsent | {name} # Until sync_to_device() sends it (see auto_sync) 1798 else: 1799 if code in (_BURST_DURATION_CODE, _CUSTOM_TRAIN_TARGET_CODE): 1800 # Sent alone, so it must suit the other parameter as the device holds it. In a batch() block, or 1801 # with auto_sync off, both are checked together by sync_to_device(). 1802 new = {name: values} 1803 self._check_custom_bursts(new.get("custom_train_target", list(self._custom_train_target)[1:]), 1804 new.get("burst_duration", list(self._burst_duration)[1:])) 1805 # Compared as the device receives them: 5 and 5.00001 V are the same DAC code 1806 old_data, _ = self._encode_output_param(code, list(getattr(self, name))[1:]) 1807 new_data, _ = self._encode_output_param(code, values) 1808 channels = [ch for ch in range(1, 5) if old_data[ch - 1] != new_data[ch - 1]] 1809 self._send_output_param(code, values, channels, f"setting {name}") 1810 return values 1811 1812 def _send_output_param(self, code, values, channels, context): 1813 """Program one output parameter on some output channels: op 91 if all four change, op 74 for each otherwise. 1814 1815 The other channels are left as the device holds them. Sending all four (op 91) for a change to one put this 1816 object's copy of the others back on the device, undoing what the joystick menu, or a program loaded with it, 1817 had set there. 1818 """ 1819 if self.info.firmware_version > 21 and len(channels) == 4: 1820 data, datatype = self._encode_output_param(code, values) 1821 self._write_serial((self._OP_MENU_BYTE, 91, code), "uint8", data, datatype) 1822 self._read_ack(context, on_refusal=lambda: self._refresh_output_param(code, channels)) 1823 return 1824 for channel in channels: 1825 # A refusal reads back every channel this assignment changes, so that the copy describes the device for 1826 # those already sent too 1827 self._send_output_param_to_channel(code, channel, values[channel - 1], context, refresh_channels=channels) 1828 1829 def _send_output_param_to_channel(self, code, channel, value, context, refresh_channels=None): 1830 """Program one output parameter on one channel (op 74).""" 1831 if code == _CONTINUOUS_LOOP_CODE and self.info.firmware_version < 22: 1832 return # Firmware v21 has no parameter 18, and _normalize_output_value() lets only False through 1833 data, datatype = self._encode_output_param(code, [value]) 1834 self._write_serial((self._OP_MENU_BYTE, 74, code, channel), "uint8", data, datatype) 1835 self._read_ack(context, on_refusal=lambda: self._refresh_output_param(code, refresh_channels or (channel,))) 1836 1837 def _apply_trigger_mode(self, values): 1838 """Check new trigger modes, and with auto_sync on, program the trigger channels whose mode changes.""" 1839 modes = [self._normalize_trigger_mode(v) for v in values] 1840 if not self._auto_sync: 1841 self._unsent = self._unsent | {"trigger_mode"} 1842 else: 1843 channels = [ch for ch in (1, 2) if modes[ch - 1] != self._trigger_mode[ch]] 1844 if len(channels) == 2: 1845 self._send_trigger_modes(modes, "setting trigger_mode") 1846 elif channels: # One: op 74, which leaves the other trigger channel as the device holds it 1847 channel = channels[0] 1848 self._write_serial((self._OP_MENU_BYTE, 74, _TRIGGER_MODE_CODE, channel, 1849 TRIGGER_MODES.index(modes[channel - 1])), "uint8") 1850 self._read_ack("setting trigger_mode", on_refusal=self._refresh_trigger_mode) 1851 return modes 1852 1853 def _send_trigger_modes(self, modes, context): 1854 """Program both trigger channels' modes: op 91, or op 74 per channel on firmware v21.""" 1855 codes = [TRIGGER_MODES.index(mode) for mode in modes] 1856 if self.info.firmware_version > 21: 1857 self._write_serial((self._OP_MENU_BYTE, 91, _TRIGGER_MODE_CODE, *codes), "uint8") 1858 self._read_ack(context, on_refusal=self._refresh_trigger_mode) 1859 else: 1860 for channel, mode_code in enumerate(codes, start=1): 1861 self._write_serial((self._OP_MENU_BYTE, 74, _TRIGGER_MODE_CODE, channel, mode_code), "uint8") 1862 self._read_ack(context) 1863 1864 def _snapshot(self): 1865 """All parameters, and which are unsent, for batch() to restore if its block fails.""" 1866 names = [*self.info.output_parameter_names, "trigger_mode"] 1867 return {name: list(getattr(self, name))[1:] for name in names}, self._unsent 1868 1869 def _restore(self, snapshot): 1870 params, self._unsent = snapshot 1871 for name, values in params.items(): 1872 getattr(self, name)._store(values) 1873 1874 def _unsent_names(self): 1875 """The settings assigned with auto_sync off and not yet sent, for messages, in parameter code order.""" 1876 return ", ".join(name for name in _SETTING_NAMES if name in self._unsent) 1877 1878 def _check_sync(self): 1879 """Check the whole local copy before sync_to_device() sends it.""" 1880 for code, (name, _) in _OUTPUT_PARAMETERS.items(): 1881 values = getattr(self, name) 1882 for ch in range(1, 5): 1883 self._normalize_output_value(code, values[ch], ch) 1884 for ch in (1, 2): 1885 self._normalize_trigger_mode(self._trigger_mode[ch]) 1886 self._check_custom_bursts(list(self._custom_train_target)[1:], list(self._burst_duration)[1:]) 1887 1888 @staticmethod 1889 def _check_custom_bursts(targets, burst_durations): 1890 """Refuse a custom train targeting bursts on a channel with no burst duration. 1891 1892 Its times are burst onsets, and a burst of no duration plays one timer cycle of a pulse at each: a 1893 program that cannot be what was meant. (The device once played one pulse and then left the channel 1894 "playing", ignoring triggers until it was stopped.) 1895 """ 1896 for channel, (target, burst) in enumerate(zip(targets, burst_durations), start=1): 1897 if target == "Bursts" and burst == 0: 1898 raise PulsePalError( 1899 f"Output channel {channel}: custom_train_target 'Bursts' makes the custom train's times burst " 1900 "onsets, so burst_duration must be above 0. Set burst_duration first, or set both in a batch() " 1901 "block." 1902 ) 1903 1904 def _send_sync(self): 1905 """Send every parameter in one command: op 92 on firmware v22 or newer, op 73 on v21.""" 1906 self._unsent = frozenset() # All are sent below 1907 if self.info.firmware_version > 21: 1908 self._sync_all_params() 1909 else: 1910 self._sync_all_params_legacy() 1911 self._read_ack("sync_to_device()", on_refusal=self._refresh_after_refused_sync) 1912 1913 def _device_bytes(self, name): 1914 """The byte values of a byte parameter for channels 1-4, as the device reads them.""" 1915 values = list(getattr(self, name))[1:] 1916 if name == "custom_train_target": 1917 return [CUSTOM_TRAIN_TARGETS.index(v) for v in values] 1918 return [int(v) for v in values] 1919 1920 def _sync_all_params(self): 1921 """Send all parameters using the packed sync op (firmware v22+). 1922 1923 Values are grouped by width so that the whole program travels as 1924 one uint32 block, one uint16 block and one uint8 block. See op 92 1925 in /Firmware/PROTOCOL.md. 1926 """ 1927 time_values = [] 1928 for name in _TIME_PARAMETERS: 1929 time_values.extend(self._seconds_to_cycles(v) for v in list(getattr(self, name))[1:]) 1930 voltage_values = [] 1931 for name in _VOLTAGE_PARAMETERS: 1932 voltage_values.extend(self._volts_to_bits(v) for v in list(getattr(self, name))[1:]) 1933 single_byte_values = [] 1934 for name in ("is_biphasic", "custom_train_id", "custom_train_target", "custom_train_loop", 1935 "continuous_loop", "link_trigger_channel1", "link_trigger_channel2"): 1936 single_byte_values.extend(self._device_bytes(name)) 1937 single_byte_values.extend(TRIGGER_MODES.index(mode) for mode in list(self._trigger_mode)[1:]) 1938 1939 self._write_serial( 1940 (self._OP_MENU_BYTE, 92), 1941 "uint8", 1942 time_values, 1943 "uint32", 1944 voltage_values, 1945 "uint16", 1946 single_byte_values, 1947 "uint8", 1948 ) 1949 1950 def _sync_all_params_legacy(self): 1951 """Send all parameters using the legacy sync op (firmware v21). 1952 1953 Equivalent to `_sync_all_params`, but lays the program out 1954 channel by channel as the older firmware expects. Continuous 1955 loop mode is not part of it. 1956 """ 1957 program_values_32 = [] 1958 program_values_16 = [] 1959 program_values_8 = [] 1960 for ch in range(1, 5): 1961 program_values_32.extend(self._seconds_to_cycles(getattr(self, name)[ch]) for name in _TIME_PARAMETERS) 1962 for ch in range(1, 5): 1963 program_values_16.extend(self._volts_to_bits(getattr(self, name)[ch]) for name in _VOLTAGE_PARAMETERS) 1964 byte_values = {name: self._device_bytes(name) for name in 1965 ("is_biphasic", "custom_train_id", "custom_train_target", "custom_train_loop")} 1966 for ch in range(4): 1967 program_values_8.extend(byte_values[name][ch] for name in byte_values) 1968 links = self._device_bytes("link_trigger_channel1") + self._device_bytes("link_trigger_channel2") 1969 modes = [TRIGGER_MODES.index(mode) for mode in list(self._trigger_mode)[1:]] 1970 1971 self._write_serial( 1972 (self._OP_MENU_BYTE, 73), 1973 "uint8", 1974 program_values_32, 1975 "uint32", 1976 program_values_16, 1977 "uint16", 1978 program_values_8 + links + modes, 1979 "uint8", 1980 ) 1981 1982 def _read_device_params(self): 1983 """Read the device's parameters (op 93). 1984 1985 Returns a dict of parameter values for channels 1-4 (trigger 1986 channels 1-2 for trigger_mode), as the parameter lists hold them. 1987 Continuous loop mode is not part of op 93, so `continuous_loop` is 1988 not included. 1989 """ 1990 self._write_serial((self._OP_MENU_BYTE, 93), "uint8") 1991 # The device sends the whole parameter set as one message, so read it in one go and 1992 # unpack it here. See "Op codes", op 93, in /Firmware/PROTOCOL.md. 1993 message = self._read_raw(self._PARAM_MESSAGE_BYTES) 1994 values = struct.unpack(f"{self._ENDIANNESS}32I12H26B", message) 1995 params = {} 1996 for index, name in enumerate(_TIME_PARAMETERS): 1997 params[name] = [self._cycles_to_seconds(x) for x in values[index * 4:index * 4 + 4]] 1998 for index, name in enumerate(_VOLTAGE_PARAMETERS): 1999 params[name] = [self._bits_to_volts(x) for x in values[32 + index * 4:32 + index * 4 + 4]] 2000 for index, name in enumerate(("is_biphasic", "custom_train_id", "custom_train_target", 2001 "custom_train_loop", "link_trigger_channel1", "link_trigger_channel2")): 2002 codes = values[44 + index * 4:44 + index * 4 + 4] 2003 if name == "custom_train_id": 2004 params[name] = list(codes) 2005 elif name == "custom_train_target": 2006 params[name] = [CUSTOM_TRAIN_TARGETS[min(code, 1)] for code in codes] 2007 else: 2008 params[name] = [bool(code) for code in codes] 2009 params["trigger_mode"] = [TRIGGER_MODES[min(code, 3)] for code in values[68:70]] 2010 return params 2011 2012 def _refresh_output_param(self, code, channels): 2013 """Read an output parameter back from the device into the local copy, for these channels. 2014 2015 Called after the device refused a value for them, which it resets. The parameter's other 2016 channels, and other parameters, keep their local values, which may hold edits not yet synced. 2017 """ 2018 name = _OUTPUT_PARAMETERS[code][0] 2019 if self.info.firmware_version < 22 or code == _CONTINUOUS_LOOP_CODE: 2020 return # No op 93 to read it with, or not in op 93 2021 device_values = self._read_device_params()[name] 2022 settings = getattr(self, name) 2023 local_values = list(settings)[1:] 2024 for ch in channels: 2025 local_values[ch - 1] = device_values[ch - 1] 2026 settings._store(local_values) 2027 2028 def _refresh_trigger_mode(self): 2029 """Read the trigger modes back from the device, after the device refused a new one.""" 2030 if self.info.firmware_version > 21: 2031 self._trigger_mode._store(self._read_device_params()["trigger_mode"]) 2032 2033 def _refresh_after_refused_sync(self): 2034 """Read the parameters back after the device refused a sync_to_device() set. 2035 2036 The device programs the set with the refused values reset, so the local copy is read back. 2037 In param sync mode the device stores the set instead, and op 93 would return the parameters 2038 playing now, so the local copy is left as it is. 2039 """ 2040 if self.info.firmware_version > 21 and "Param Sync" not in list(self._trigger_mode)[1:]: 2041 self.sync_from_device() 2042 2043 def _check_whole_number(self, value, name, low, high): 2044 """Raise PulsePalError unless value is a whole number from low to high.""" 2045 valid = ( 2046 isinstance(value, numbers.Real) 2047 and not isinstance(value, (bool, np.bool_)) 2048 and math.isfinite(value) 2049 and value == int(value) 2050 and low <= value <= high 2051 ) 2052 if not valid: 2053 raise PulsePalError( 2054 f"{name} must be a whole number from {low} to {high}. Received {value!r}." 2055 ) 2056 2057 def _min_pulse_cycles(self): 2058 """The shortest pulse phase, interval or train, in timer cycles: DeviceInfo.min_pulse_width_us. See 2059 _MIN_PULSE_CYCLES.""" 2060 return int(round(self.info.min_pulse_width_us / self.info.cycle_period_us)) 2061 2062 def _min_pulse_cycles_text(self): 2063 """_min_pulse_cycles() for messages: "1 cycle", "2 cycles".""" 2064 cycles = self._min_pulse_cycles() 2065 return f"{cycles} cycle" if cycles == 1 else f"{cycles} cycles" 2066 2067 def _min_pulse_seconds(self): 2068 """The shortest pulse phase, interval or train, in seconds: DeviceInfo.min_pulse_width_us.""" 2069 return self._cycles_to_seconds(self._min_pulse_cycles()) 2070 2071 def _require_firmware(self, minimum_version, context): 2072 """Raise unless the device firmware is new enough for an op.""" 2073 if ( 2074 self.info.firmware_version is None 2075 or self.info.firmware_version < minimum_version 2076 ): 2077 raise PulsePalError( 2078 f"{context} requires firmware v{minimum_version} or newer. " 2079 f"Detected firmware is v{self.info.firmware_version}." 2080 ) 2081 2082 # ------------------------------------------------------------------ 2083 # Internals: custom trains 2084 # ------------------------------------------------------------------ 2085 2086 def _send_custom_train(self, custom_train_id, pulse_times_cycles, 2087 pulse_voltage_bits, context): 2088 """Send a custom pulse train, already converted to device units (NumPy integer arrays). 2089 2090 Uses op 95 on firmware v22 or newer, and the legacy ops 75 and 76 2091 (trains 1 and 2 only) on firmware v21. 2092 """ 2093 n_trains = self.info.n_custom_pulse_trains 2094 valid_id = ( 2095 isinstance(custom_train_id, numbers.Integral) 2096 and not isinstance(custom_train_id, (bool, np.bool_)) 2097 and 1 <= custom_train_id <= n_trains 2098 ) 2099 if not valid_id: 2100 raise PulsePalError( 2101 f"{context}: custom_train_id must be an integer from 1 to " 2102 f"{n_trains}. Received {custom_train_id!r}." 2103 ) 2104 train_id = int(custom_train_id) 2105 n_pulses = len(pulse_times_cycles) 2106 if n_pulses == 0: 2107 # The device refuses an empty train too: it would hold no pulse times to play 2108 raise PulsePalError( 2109 f"{context}: a custom train needs at least one pulse. To play nothing on a channel, set its " 2110 "custom_train_id to a train that holds what it should play, or do not trigger it." 2111 ) 2112 if n_pulses > self.info.max_custom_pulses: 2113 raise PulsePalError( 2114 f"{context}: {n_pulses} pulses were given. Pulse Pal can " 2115 f"store up to {self.info.max_custom_pulses} pulses per " 2116 "custom train." 2117 ) 2118 # Times are multiples of _min_pulse_cycles() (see _custom_times_to_cycles()), so increasing times are far 2119 # enough apart for a trigger channel to detect each pulse. 2120 too_close = np.flatnonzero(np.diff(pulse_times_cycles) < self._min_pulse_cycles()) 2121 if too_close.size: 2122 i = int(too_close[0]) + 1 2123 raise PulsePalError( 2124 f"{context}: pulse times must increase, by at least " 2125 f"{self._min_pulse_seconds()} s ({self._min_pulse_cycles_text()} " 2126 f"of the device's {self.info.cycle_period_us} us " 2127 f"timer). Pulse {i + 1} is at " 2128 f"{self._cycles_to_seconds(pulse_times_cycles[i])} s, " 2129 f"and pulse {i} is at " 2130 f"{self._cycles_to_seconds(pulse_times_cycles[i - 1])} s." 2131 ) 2132 if pulse_times_cycles[-1] > self._max_time_cycles(): 2133 raise PulsePalError( 2134 f"{context}: the last pulse starts at {self._cycles_to_seconds(pulse_times_cycles[-1])} s. " 2135 f"Pulse times must be at most {self.info.max_time} s." 2136 ) 2137 2138 if self.info.firmware_version > 21: 2139 header = bytes([self._OP_MENU_BYTE, 95, train_id - 1]) 2140 else: 2141 header = bytes([self._OP_MENU_BYTE, 74 + train_id]) 2142 self._write_raw( 2143 header + struct.pack("<I", n_pulses) 2144 + np.asarray(pulse_times_cycles).astype("<u4").tobytes() 2145 + np.asarray(pulse_voltage_bits).astype("<u2").tobytes() 2146 ) 2147 self._read_ack(context) 2148 2149 def _number_array(self, values, name, description): 2150 """Return values (a scalar, a list, a tuple or a 1-D NumPy array) as a float64 array, refusing booleans, 2151 strings and anything else that is not a number, as the scalar conversions do. 2152 2153 An array of two or more dimensions, or a list of lists, is refused rather than flattened: one value per 2154 pulse is a list, and a 2-D one is a mistake, such as both rows of a stereo waveform. (The MATLAB class, which 2155 took a matrix's longest side as its pulse count and sent all of it, sent the rest to the device as commands.) 2156 """ 2157 if isinstance(values, np.ndarray): 2158 self._check_one_dimensional(values, name) 2159 if values.dtype.kind in "iuf": 2160 return values.ravel().astype(float) 2161 items = self._as_list(values) 2162 refused_types = tuple(kind for kind in {type(item) for item in items} # Each type once: fast for long lists 2163 if issubclass(kind, (bool, np.bool_, str, bytes))) 2164 if refused_types: 2165 item = next(item for item in items if isinstance(item, refused_types)) 2166 raise PulsePalError(f"{name} must be {description}. Received {item!r}.") 2167 try: 2168 array = np.asarray(items, dtype=float) 2169 except (TypeError, ValueError): 2170 for item in items: 2171 try: 2172 float(item) 2173 except (TypeError, ValueError): 2174 raise PulsePalError(f"{name} must be {description}. Received {item!r}.") from None 2175 raise 2176 self._check_one_dimensional(array, name) # A list of lists 2177 return array 2178 2179 @staticmethod 2180 def _check_one_dimensional(array, name): 2181 if array.ndim > 1: 2182 raise PulsePalError( 2183 f"{name} must be one-dimensional, one value per pulse: a list, a tuple or a 1-D NumPy array. " 2184 f"Received an array of shape {array.shape}." 2185 ) 2186 2187 @staticmethod 2188 def _first(values, refused): 2189 """The first value at a True position of the boolean array `refused`, for an error message.""" 2190 i = int(np.flatnonzero(refused)[0]) 2191 item = values.ravel()[i] if isinstance(values, np.ndarray) else PulsePalDevice._as_list(values)[i] 2192 return item.item() if isinstance(item, np.generic) else item 2193 2194 def _custom_times_to_cycles(self, values, name): 2195 """Convert custom pulse times to timer cycles, all at once: _custom_time_cycles() for each of them. 2196 2197 Each must be a time the device can play, and a whole number of DeviceInfo.min_pulse_width_us to the nearest 2198 microsecond, as in the MATLAB class. Returns an int64 array. 2199 """ 2200 seconds = self._number_array(values, name, "a time of 0 s or more") 2201 refused = ~(np.isfinite(seconds) & (seconds >= 0)) 2202 if refused.any(): 2203 raise PulsePalError(f"{name} must be a time of 0 s or more. Received {self._first(values, refused)!r}.") 2204 # The same arithmetic as _seconds_to_cycles() and _custom_time_cycles(): NumPy's round() also takes a 2205 # halfway value to the even integer 2206 refused = np.round(seconds * float(self.info.cycle_frequency)) > self._max_time_cycles() 2207 if refused.any(): 2208 raise PulsePalError( 2209 f"{name} must be at most {self.info.max_time} s, the longest time the device's screen " 2210 f"shows. Received {self._first(values, refused)!r}." 2211 ) 2212 microseconds = np.round(seconds * 1e6) 2213 step_us = int(self.info.min_pulse_width_us) 2214 refused = np.mod(microseconds, step_us) != 0 2215 if refused.any(): 2216 raise PulsePalError( 2217 f"{name} must be multiples of {step_us} us ({step_us / 1e6:g} s). " 2218 f"Received {self._first(values, refused)!r}." 2219 ) 2220 return (microseconds // int(self.info.cycle_period_us)).astype(np.int64) 2221 2222 def _volts_array_to_bits(self, values, name): 2223 """Convert voltages to DAC codes, all at once: _volts_to_bits() for each of them. Returns an int64 array.""" 2224 volts = self._number_array(values, name, "in [-10, 10] V") 2225 refused = ~((volts >= -10) & (volts <= 10)) # Also refuses NaN 2226 if refused.any(): 2227 raise PulsePalError(f"{name} must be in [-10, 10] V. Received {self._first(values, refused)!r}.") 2228 return np.round((volts + 10) / 20 * self._DAC_BITMAX).astype(np.int64) 2229 2230 # ------------------------------------------------------------------ 2231 # Internals: serial port and units 2232 # ------------------------------------------------------------------ 2233 2234 def _write_serial(self, *args): 2235 """Write one or more data/type pairs to the serial port, in one write.""" 2236 if len(args) % 2 != 0: 2237 raise PulsePalError( 2238 "Serial writes require data/type argument pairs." 2239 ) 2240 2241 payload = bytearray() 2242 for i in range(0, len(args), 2): 2243 payload.extend(self._pack_values(args[i], args[i + 1])) 2244 self._write_raw(bytes(payload)) 2245 2246 def _write_raw(self, payload): 2247 """Write a command to the serial port, in one write. 2248 2249 A command whose reply was not read in full, because the read timed out or was interrupted (Ctrl-C), leaves 2250 that reply, or the rest of it, to arrive later. Read as the reply to the next command, it put every reply 2251 after it out of step: status() reported channels playing that were not, and assignments the device took 2252 raised refusals. So it is discarded first (_discard_lost_reply()). Then this command's reply, if it has one, 2253 is pending until it has been read: _read_raw() marks it read, and a method that reads a reply otherwise 2254 (format_microsd()) sets _reply_pending back to None itself. 2255 """ 2256 if self._reply_pending is not None: 2257 self._discard_lost_reply() 2258 if payload[1] not in _OPS_WITHOUT_TRACKED_REPLY: 2259 self._reply_pending = payload[1] # Before the write: a Ctrl-C can come as soon as it returns 2260 bytes_written = self.port.write(payload) 2261 if bytes_written != len(payload): 2262 raise PulsePalError( 2263 f"Wrote {bytes_written} byte(s), expected to write " 2264 f"{len(payload)} byte(s)." 2265 ) 2266 2267 def _discard_lost_reply(self): 2268 """Discard what arrives of a reply that was not read in full (see _write_raw()), and bring this object back 2269 in step with the device. 2270 2271 The command may have changed the parameters, unknown to this object: an assignment interrupted after its 2272 write, for one, which the device took and this object did not. So after one of _PARAMETER_OPS, they are read 2273 back (op 93), on firmware v22 or newer and outside param sync mode, where op 93 returns the parameters playing 2274 rather than a set stored for the next sync edge. 2275 """ 2276 op = self._reply_pending 2277 self._reply_pending = None 2278 n_late = len(_common.read_until_quiet(self.port)) 2279 read_back = (op in _PARAMETER_OPS and self.info.firmware_version > 21 2280 and "Param Sync" not in list(self._trigger_mode)[1:]) 2281 _common.warn_lost_reply(f"op {op}", n_late, 2282 ", and the parameters were read back from the device" if read_back else "") 2283 if read_back: 2284 try: 2285 self.sync_from_device() 2286 except PulsePalError as exc: 2287 raise PulsePalError( 2288 f"Pulse Pal did not send its parameters, read back after the reply to an earlier command was " 2289 f"lost. {_common.NO_REPLY_HINT}" 2290 ) from exc 2291 2292 def _read_raw(self, n_bytes): 2293 """Read exactly n_bytes from the serial port: the whole reply to the command last sent, which is then no 2294 longer pending (see _write_raw()). Only connecting reads a reply in more than one call.""" 2295 message_bytes = self.port.read(n_bytes) 2296 if len(message_bytes) < n_bytes: 2297 raise PulsePalError( 2298 f"Pulse Pal sent {len(message_bytes)} of the {n_bytes} byte(s) of its reply within " 2299 f"{self.port.timeout} s. {_common.NO_REPLY_HINT}" 2300 ) 2301 self._reply_pending = None 2302 return message_bytes 2303 2304 def _read_serial(self, n_values, datatype): 2305 """Read values from the serial port and unpack them with struct.""" 2306 fmt = self._STRUCT_FORMATS[datatype] 2307 n_values = int(n_values) 2308 n_bytes = n_values * struct.calcsize(fmt) 2309 message_bytes = self._read_raw(n_bytes) 2310 2311 values = struct.unpack( 2312 f"{self._ENDIANNESS}{n_values}{fmt}", 2313 message_bytes, 2314 ) 2315 if n_values == 1: 2316 return values[0] 2317 return list(values) 2318 2319 def _read_ack(self, context, on_refusal=None, refusal_message=None, refusal_messages=None): 2320 """Read a one-byte acknowledgement from the device. 2321 2322 The device replies 1 if it executed the command, or 0 if it rejected 2323 the command because a value was out of range. On a 0, on_refusal() 2324 is called before the error is raised: the device resets a value it 2325 refuses, so callers use it to read the value back into the local copy. 2326 refusal_message replaces the error's usual message, for a command 2327 refused for another reason. refusal_messages maps other replies to 2328 their own message, e.g. op 90's 2. 2329 """ 2330 try: 2331 acknowledgement = self._read_serial(1, "uint8") 2332 except PulsePalError as exc: 2333 raise PulsePalError( 2334 f"Pulse Pal did not confirm {context}: no reply arrived within {self.port.timeout} s. " 2335 f"{_common.NO_REPLY_HINT}" 2336 ) from exc 2337 if acknowledgement != 1: 2338 refusal = PulsePalError( 2339 (refusal_messages or {}).get(acknowledgement) or 2340 refusal_message or 2341 f"Pulse Pal rejected the command sent by {context}. " 2342 "This usually means that a channel number, parameter code or " 2343 "value was out of range for the connected device." 2344 ) 2345 if on_refusal is not None: 2346 try: 2347 on_refusal() 2348 except PulsePalError as exc: 2349 raise refusal from exc 2350 raise refusal 2351 2352 def _pack_values(self, values, datatype): 2353 """Pack scalar, list/tuple, or NumPy array values into bytes.""" 2354 fmt = self._STRUCT_FORMATS[datatype] 2355 values_list = self._as_list(values) 2356 min_value, max_value = self._TYPE_RANGES[datatype] 2357 normalized = [] 2358 for value in values_list: 2359 value = int(value) 2360 if not min_value <= value <= max_value: 2361 raise PulsePalError( 2362 f"Value {value} is out of range for {datatype} " 2363 f"({min_value} to {max_value})." 2364 ) 2365 normalized.append(value) 2366 return struct.pack(f"{self._ENDIANNESS}{len(normalized)}{fmt}", *normalized) 2367 2368 @staticmethod 2369 def _as_list(values): 2370 """Return values as a flat list, wrapping scalars in one.""" 2371 if isinstance(values, np.ndarray): 2372 return values.ravel().tolist() 2373 if isinstance(values, (bytes, bytearray, str)): 2374 return [values] 2375 if isinstance(values, numbers.Number): 2376 return [values] 2377 try: 2378 return list(values) 2379 except TypeError: 2380 return [values] 2381 2382 def _volts_to_bits(self, value, name="voltage"): 2383 """Convert -10 V to +10 V to the corresponding DAC code. 2384 2385 Rounds to the nearest code, halves to even (round()), as the MATLAB and C++ classes do. 2386 Raises PulsePalError for a voltage outside [-10, 10] or not a number: clamping it would 2387 play a different voltage. 2388 """ 2389 try: 2390 volts = float(value) 2391 except (TypeError, ValueError): 2392 volts = float("nan") 2393 if isinstance(value, (bool, np.bool_, str)) or not -10 <= volts <= 10: # Also refuses NaN 2394 raise PulsePalError(f"{name} must be in [-10, 10] V. Received {value!r}.") 2395 return int(round((volts + 10) / 20 * self._DAC_BITMAX)) 2396 2397 def _bits_to_volts(self, value): 2398 """Convert a DAC code to volts, snapping to a clean value within 1 LSB.""" 2399 raw_volts = (float(value) / self._DAC_BITMAX * 20) - 10 2400 lsb_volts = 20.0 / self._DAC_BITMAX 2401 # The nearest 3 decimal number (e.g. 5.000, 4.255), if the code is within 1 LSB of it 2402 clean_volts = round(raw_volts, 3) 2403 if abs(raw_volts - clean_volts) <= lsb_volts: 2404 return clean_volts 2405 return round(raw_volts, 4) 2406 2407 def _seconds_to_cycles(self, value, name="time"): 2408 """Convert seconds to the corresponding refresh-cycle count. 2409 2410 Rounds to the nearest cycle, halves to even (round()), as the MATLAB and C++ classes do: 2411 125 us, 2.5 cycles, is 2 cycles in all three. Raises PulsePalError for a time that is 2412 negative, not a number, or longer than DeviceInfo.max_time. 2413 """ 2414 try: 2415 seconds = float(value) 2416 except (TypeError, ValueError): 2417 seconds = float("nan") 2418 if isinstance(value, (bool, np.bool_, str)) or not (math.isfinite(seconds) and seconds >= 0): 2419 raise PulsePalError(f"{name} must be a time of 0 s or more. Received {value!r}.") 2420 cycles = int(round(seconds * float(self.info.cycle_frequency))) 2421 if cycles > self._max_time_cycles(): 2422 raise PulsePalError( 2423 f"{name} must be at most {self.info.max_time} s, the longest time the device's screen " 2424 f"shows. Received {value!r}." 2425 ) 2426 return cycles 2427 2428 def _max_time_cycles(self): 2429 """DeviceInfo.max_time in timer cycles: 999999990 on a 10 us timer, 199999998 on a 50 us timer.""" 2430 return int(round(self.info.max_time * float(self.info.cycle_frequency))) 2431 2432 def _custom_time_cycles(self, value, name): 2433 """Convert a custom pulse time or sampling period to timer cycles. 2434 2435 It must be a whole number of DeviceInfo.min_pulse_width_us, to the nearest microsecond, 2436 as in the MATLAB class. A time between two steps is refused, not rounded 2437 as the time parameters are, so that a train plays as it was written. 2438 """ 2439 self._seconds_to_cycles(value, name) # Checks that it is a time the device can play 2440 microseconds = int(round(float(value) * 1e6)) 2441 step_us = int(self.info.min_pulse_width_us) 2442 if microseconds % step_us: 2443 raise PulsePalError( 2444 f"{name} must be multiples of {step_us} us ({step_us / 1e6:g} s). Received {value!r}." 2445 ) 2446 return microseconds // int(self.info.cycle_period_us) 2447 2448 def _cycles_to_seconds(self, value): 2449 """Convert hardware timer cycle counts to seconds.""" 2450 return float(value) / float(self.info.cycle_frequency)
A class to control a Pulse Pal device on a USB serial port.
Creating an instance opens the serial port, exchanges a handshake
with the device, verifies that its firmware is supported, reads the
device properties into PulsePalDevice.info, and programs the
device with the default parameters (see
PulsePalDevice.set_default_params).
from pulsepal import PulsePalDevice
P = PulsePalDevice("COM3")
P.phase1_voltage[1] = 5
P.trigger(1)
P.close()
Here, replace "COM3" with Pulse Pal's USB serial port name.
To view a list of available ports, use
PulsePalDevice.serialportlist().
Pulse Pal's port may not be visible if it is connected to another
instance of PulsePalDevice or an external application. Use
PulsePalDevice.serialportlist("all") to view all ports.
If you see multiple available ports, disconnect Pulse Pal's USB plug
and run serialportlist() again. Notice which port disappears from the
list.
PulsePalDevice is also a context manager, which closes the connection on exit even if an error is raised:
with PulsePalDevice("COM3") as P:
P.trigger(1)
The attributes below named after Pulse Pal parameters are lists
indexed by channel number, with index 0 unused (see "Parameters"
above). print(P) shows them all, and PulsePalDevice.export_params
returns them as a dict.
672 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 673 """Open a connection to a Pulse Pal device. 674 675 Opens the serial port, exchanges the handshake, verifies the 676 firmware version, reads the device properties into 677 `PulsePalDevice.info`, and programs the device with the default 678 parameters. 679 680 ```python 681 P = PulsePalDevice("COM3") 682 P = PulsePalDevice(serial_number="20345420") # Wherever it is plugged in 683 ``` 684 685 Args: 686 port_name: USB serial port for the Pulse Pal device, such as 687 `COM3` on Windows or `/dev/ttyACM0` on Linux. 688 serial_number: Instead of `port_name`, the device's USB serial 689 number (`DeviceInfo.serial_number`), which finds its port. 690 Pulse Pal 3 only: Pulse Pal 2 has none. 691 timeout: Serial read timeout, in seconds. 692 693 Firmware newer than this module knows (`v22`) is used with a 694 warning: new firmware only adds commands, so everything here still 695 works with it. Update the package to use what is new. 696 697 Raises: 698 PulsePalError: If the device does not return the expected 699 handshake (for example, it runs Wave Pal or Synth Pal 700 firmware), or its firmware is older than v21, or no port 701 has the serial number given. The port is closed again 702 before any error is raised. 703 serial.SerialException: If the serial port cannot be opened, 704 e.g. because another program has it open (on Linux and 705 macOS, another session of these classes, which lock it). 706 """ 707 self._closed = True 708 self._gui = None 709 self._auto_sync = True 710 self.info = DeviceInfo() 711 self._create_settings() 712 port_name = _common.port_name_from(port_name, serial_number, "PulsePalDevice") 713 self.port = serial.Serial( 714 port_name, 715 self._BAUD_RATE, 716 timeout=timeout, 717 rtscts=True, 718 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 719 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 720 # it, such as another of these classes, gets an error instead 721 exclusive=True, 722 ) 723 self._closed = False 724 try: 725 self._start_session(port_name) 726 except BaseException: 727 # Otherwise the port stays open until the object is garbage 728 # collected, and a second attempt, or WavePalDevice, cannot open 729 # it. The device is in an unknown state, so it is not sent the 730 # disconnect op. 731 self._close(send_disconnect=False) 732 raise
Open a connection to a Pulse Pal device.
Opens the serial port, exchanges the handshake, verifies the
firmware version, reads the device properties into
PulsePalDevice.info, and programs the device with the default
parameters.
P = PulsePalDevice("COM3")
P = PulsePalDevice(serial_number="20345420") # Wherever it is plugged in
Arguments:
- port_name: USB serial port for the Pulse Pal device, such as
COM3on Windows or/dev/ttyACM0on Linux. - serial_number: Instead of
port_name, the device's USB serial number (DeviceInfo.serial_number), which finds its port. Pulse Pal 3 only: Pulse Pal 2 has none. - timeout: Serial read timeout, in seconds.
Firmware newer than this module knows (v22) is used with a
warning: new firmware only adds commands, so everything here still
works with it. Update the package to use what is new.
Raises:
- PulsePalError: If the device does not return the expected handshake (for example, it runs Wave Pal or Synth Pal firmware), or its firmware is older than v21, or no port has the serial number given. The port is closed again before any error is raised.
- 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).
830 @staticmethod 831 def serialportlist(ports_to_list="available"): 832 """Return the names of the USB serial ports on this computer. 833 834 Called on the class, without connecting to a device, to find the 835 port name to pass to `PulsePalDevice`: 836 837 ```python 838 from pulsepal import PulsePalDevice 839 840 ports = PulsePalDevice.serialportlist() 841 P = PulsePalDevice(ports[0]) 842 ``` 843 844 Args: 845 ports_to_list: `available` to list only the ports that are 846 not already in use, or `all` to list every USB serial 847 port. Not case sensitive. 848 849 Returns: 850 Sorted list of port names, such as `["COM3", "COM7"]` on 851 Windows or `["/dev/ttyACM0"]` on Linux. 852 853 Raises: 854 PulsePalError: If `ports_to_list` is not `available` or 855 `all`. 856 """ 857 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 PulsePalDevice:
from pulsepal import PulsePalDevice
ports = PulsePalDevice.serialportlist()
P = PulsePalDevice(ports[0])
Arguments:
- ports_to_list:
availableto list only the ports that are not already in use, orallto list every USB serial port. Not case sensitive.
Returns:
Sorted list of port names, such as
["COM3", "COM7"]on Windows or["/dev/ttyACM0"]on Linux.
Raises:
- PulsePalError: If
ports_to_listis notavailableorall.
863 @property 864 def auto_sync(self): 865 """Whether assigning a parameter programs the device at once. 866 867 `True` (the default): each assignment, such as 868 `P.phase1_voltage[1] = 5`, programs the device at once, also while 869 a trigger channel is in param sync mode. It sends the channels 870 whose value it changes, and nothing for a channel that already 871 holds the value assigned: the other channels keep what the device 872 holds, e.g. values set with its joystick (read them with 873 `PulsePalDevice.sync_from_device`). 874 875 `False`: assignments change only this object's copy of the 876 parameters, checking each value, and 877 `PulsePalDevice.sync_to_device` sends all of them in one command. 878 Turning it back on does not send changes made meanwhile, so it 879 raises `pulsepal.PulsePalError` while there are any: call 880 `PulsePalDevice.sync_to_device` first to send them, or 881 `PulsePalDevice.sync_from_device` to discard them. A 882 `PulsePalDevice.batch` block does it all for you. `close()` warns of 883 changes never sent. 884 """ 885 return self._auto_sync
Whether assigning a parameter programs the device at once.
True (the default): each assignment, such as
P.phase1_voltage[1] = 5, programs the device at once, also while
a trigger channel is in param sync mode. It sends the channels
whose value it changes, and nothing for a channel that already
holds the value assigned: the other channels keep what the device
holds, e.g. values set with its joystick (read them with
PulsePalDevice.sync_from_device).
False: assignments change only this object's copy of the
parameters, checking each value, and
PulsePalDevice.sync_to_device sends all of them in one command.
Turning it back on does not send changes made meanwhile, so it
raises pulsepal.PulsePalError while there are any: call
PulsePalDevice.sync_to_device first to send them, or
PulsePalDevice.sync_from_device to discard them. A
PulsePalDevice.batch block does it all for you. close() warns of
changes never sent.
901 def batch(self): 902 """Change several parameters, and send them in one command. 903 904 Returns a context manager. In its block, assignments change only 905 this object's copy of the parameters (as with 906 `PulsePalDevice.auto_sync` off), and when the block ends, 907 `PulsePalDevice.sync_to_device` sends all of them at once. If the 908 block raises an error, nothing is sent, and the parameters go 909 back to what they were. 910 911 ```python 912 with P.batch(): 913 P.phase1_voltage = [5, 5, 2.5, 2.5] 914 P.phase1_duration[3] = 0.002 915 ``` 916 917 In param sync mode, the device stores the parameters for the next 918 rising edge on the param sync channel (see 919 `PulsePalDevice.trigger_mode`). 920 """ 921 return _common.batch(self)
Change several parameters, and send them in one command.
Returns a context manager. In its block, assignments change only
this object's copy of the parameters (as with
PulsePalDevice.auto_sync off), and when the block ends,
PulsePalDevice.sync_to_device sends all of them at once. If the
block raises an error, nothing is sent, and the parameters go
back to what they were.
with P.batch():
P.phase1_voltage = [5, 5, 2.5, 2.5]
P.phase1_duration[3] = 0.002
In param sync mode, the device stores the parameters for the next
rising edge on the param sync channel (see
PulsePalDevice.trigger_mode).
923 def sync_to_device(self): 924 """Program the device with this object's copy of all parameters. 925 926 Sends every output and trigger parameter to the device in a single 927 command. Use it after assignments made with 928 `PulsePalDevice.auto_sync` off; with it on, they have already 929 reached the device. 930 931 ```python 932 P.auto_sync = False 933 P.phase1_voltage[1:5] = [5] * 4 934 P.sync_to_device() 935 ``` 936 937 On Pulse Pal 3, if either trigger channel is in param sync mode 938 (`PulsePalDevice.trigger_mode` `"Param Sync"`), the device stores 939 the parameters instead of programming them, and loads them on the 940 next rising edge of that channel. The device still checks every 941 value. This is the only method whose effect is deferred that way. 942 943 Raises: 944 PulsePalError: If a value is out of range for its parameter 945 (nothing is sent), or the device does not acknowledge the 946 command. 947 """ 948 self._check_sync() 949 self._send_sync()
Program the device with this object's copy of all parameters.
Sends every output and trigger parameter to the device in a single
command. Use it after assignments made with
PulsePalDevice.auto_sync off; with it on, they have already
reached the device.
P.auto_sync = False
P.phase1_voltage[1:5] = [5] * 4
P.sync_to_device()
On Pulse Pal 3, if either trigger channel is in param sync mode
(PulsePalDevice.trigger_mode "Param Sync"), the device stores
the parameters instead of programming them, and loads them on the
next rising edge of that channel. The device still checks every
value. This is the only method whose effect is deferred that way.
Raises:
- PulsePalError: If a value is out of range for its parameter (nothing is sent), or the device does not acknowledge the command.
951 def sync_from_device(self): 952 """Read all parameters from the device into this object's copy. 953 954 Overwrites the parameter lists with the program currently on the 955 device, e.g. after it has been reprogrammed from its thumb 956 joystick. `PulsePalDevice.continuous_loop` is left as it is: the 957 device does not report it. Changes made with 958 `PulsePalDevice.auto_sync` off and not yet sent are discarded, but 959 for `PulsePalDevice.continuous_loop`. 960 961 Requires firmware v22 or newer. 962 963 Raises: 964 PulsePalError: If the connected firmware is older than v22, 965 or the device does not return the full parameter set. 966 """ 967 self._require_firmware(22, "sync_from_device()") 968 for name, values in self._read_device_params().items(): 969 getattr(self, name)._store(values) 970 self._unsent = self._unsent & {"continuous_loop"} # Not read: a change to it is still unsent
Read all parameters from the device into this object's copy.
Overwrites the parameter lists with the program currently on the
device, e.g. after it has been reprogrammed from its thumb
joystick. PulsePalDevice.continuous_loop is left as it is: the
device does not report it. Changes made with
PulsePalDevice.auto_sync off and not yet sent are discarded, but
for PulsePalDevice.continuous_loop.
Requires firmware v22 or newer.
Raises:
- PulsePalError: If the connected firmware is older than v22, or the device does not return the full parameter set.
972 def set_default_params(self): 973 """Program the device with the default parameters. 974 975 The defaults are monophasic +5 V pulses of 0.1 ms, 1 ms apart 976 (about 909 a second), for 1 second, resting at 0 V, on all four 977 output channels; for biphasic pulses, a phase 2 of -5 V and 0.1 ms 978 after 0.1 ms at rest. No output channel is linked to a trigger 979 channel, so a TTL starts nothing until a script links it, and both 980 trigger channels are in normal mode. They are the device's own 981 defaults, which it also loads at startup, after a comm failure 982 and after formatting its microSD card. They are sent at once, 983 also while `PulsePalDevice.auto_sync` is off. The constructor 984 calls it. 985 """ 986 for name, value in _DEFAULT_OUTPUT_VALUES.items(): 987 getattr(self, name)._store([value] * 4) 988 self._trigger_mode._store(["Normal"] * 2) 989 if self._has_param_sync(): 990 # A device left in param sync mode would store the sync below instead of running it, 991 # leaving the device on its old program until a TTL arrived. A trigger mode sent on 992 # its own is not deferred that way, so both trigger channels leave param sync mode 993 # first. See PulsePalDevice.trigger_mode. 994 self._send_trigger_modes(["Normal"] * 2, "set_default_params()") 995 self._send_sync()
Program the device with the default parameters.
The defaults are monophasic +5 V pulses of 0.1 ms, 1 ms apart
(about 909 a second), for 1 second, resting at 0 V, on all four
output channels; for biphasic pulses, a phase 2 of -5 V and 0.1 ms
after 0.1 ms at rest. No output channel is linked to a trigger
channel, so a TTL starts nothing until a script links it, and both
trigger channels are in normal mode. They are the device's own
defaults, which it also loads at startup, after a comm failure
and after formatting its microSD card. They are sent at once,
also while PulsePalDevice.auto_sync is off. The constructor
calls it.
997 def export_params(self): 998 """Return every parameter, as a dict of plain lists. 999 1000 Keyed by parameter name (`DeviceInfo.output_parameter_names`, then 1001 `"trigger_mode"`), with one value per channel and no unused index 1002 0. It holds only numbers, booleans and names, so it can be saved 1003 with `json` and logged with your data, to record exactly what the 1004 device played. `PulsePalDevice.import_params` programs it again. 1005 1006 ```python 1007 import json 1008 with open("trial_params.json", "w") as f: 1009 json.dump(P.export_params(), f) 1010 ``` 1011 """ 1012 names = [*self.info.output_parameter_names, "trigger_mode"] 1013 return {name: list(getattr(self, name))[1:] for name in names}
Return every parameter, as a dict of plain lists.
Keyed by parameter name (DeviceInfo.output_parameter_names, then
"trigger_mode"), with one value per channel and no unused index
- It holds only numbers, booleans and names, so it can be saved
with
jsonand logged with your data, to record exactly what the device played.PulsePalDevice.import_paramsprograms it again.
import json
with open("trial_params.json", "w") as f:
json.dump(P.export_params(), f)
1015 def import_params(self, params): 1016 """Program the device with parameters exported by `PulsePalDevice.export_params`. 1017 1018 All of them are checked first, and then sent in one command, as 1019 at the end of a `PulsePalDevice.batch` block: in param sync mode, 1020 the device stores them for the next sync edge. Parameters missing 1021 from `params` keep their values. A name that is not a parameter 1022 is skipped with a warning, so that parameters exported by a newer 1023 version of this package, which may have more, can still be 1024 imported; if none is, an error is raised. 1025 1026 A whole program file, as the GUIs save it, is imported too: its 1027 parameters, not its custom trains (a warning says so, if it has 1028 any). 1029 1030 ```python 1031 with open("MyProgram.json") as f: 1032 P.import_params(json.load(f)) 1033 ``` 1034 1035 Args: 1036 params: A dict of parameter name to one value per channel (a 1037 list, tuple or NumPy array), as `export_params` returns, 1038 or a program file's contents. The MATLAB class's names 1039 (`phase1Voltage`) are accepted too, so that the parameters 1040 its `exportParams()` returns, saved with `jsonencode`, can 1041 be imported. 1042 1043 Raises: 1044 PulsePalError: If a value is invalid, or no name in `params` is 1045 a parameter. Nothing is sent, and the parameters keep the 1046 values they had. 1047 """ 1048 params = _params_to_import(params) 1049 with self.batch(): 1050 for name, values in params.items(): 1051 setattr(self, name, values)
Program the device with parameters exported by PulsePalDevice.export_params.
All of them are checked first, and then sent in one command, as
at the end of a PulsePalDevice.batch block: in param sync mode,
the device stores them for the next sync edge. Parameters missing
from params keep their values. A name that is not a parameter
is skipped with a warning, so that parameters exported by a newer
version of this package, which may have more, can still be
imported; if none is, an error is raised.
A whole program file, as the GUIs save it, is imported too: its parameters, not its custom trains (a warning says so, if it has any).
with open("MyProgram.json") as f:
P.import_params(json.load(f))
Arguments:
- params: A dict of parameter name to one value per channel (a
list, tuple or NumPy array), as
export_paramsreturns, or a program file's contents. The MATLAB class's names (phase1Voltage) are accepted too, so that the parameters itsexportParams()returns, saved withjsonencode, can be imported.
Raises:
- PulsePalError: If a value is invalid, or no name in
paramsis a parameter. Nothing is sent, and the parameters keep the values they had.
1053 def set_fixed_voltage(self, channels, voltage): 1054 """Set output channels to a fixed voltage. 1055 1056 Each channel holds the voltage until it is set again or until a 1057 pulse train is triggered on it. 1058 1059 ```python 1060 P.set_fixed_voltage(4, 2.5) 1061 P.set_fixed_voltage([1, 3], -1) 1062 ``` 1063 1064 Args: 1065 channels: Output channel number, 1-4, or several as a list, 1066 tuple or NumPy array. 1067 voltage: Voltage to set on each of them, in volts [-10, 10]. 1068 1069 Raises: 1070 PulsePalError: If a channel number is not 1-4, the voltage is 1071 outside [-10, 10] (nothing is sent), or the device does not 1072 acknowledge the command. 1073 """ 1074 channels = _common.channel_numbers(channels) 1075 voltage_bits = self._volts_to_bits(voltage, "voltage") 1076 for channel in channels: 1077 self._write_serial( 1078 (self._OP_MENU_BYTE, 79, channel), 1079 "uint8", 1080 voltage_bits, 1081 "uint16", 1082 ) 1083 self._read_ack("set_fixed_voltage()")
Set output channels to a fixed voltage.
Each channel holds the voltage until it is set again or until a pulse train is triggered on it.
P.set_fixed_voltage(4, 2.5)
P.set_fixed_voltage([1, 3], -1)
Arguments:
- channels: Output channel number, 1-4, or several as a list, tuple or NumPy array.
- voltage: Voltage to set on each of them, in volts [-10, 10].
Raises:
- PulsePalError: If a channel number is not 1-4, the voltage is outside [-10, 10] (nothing is sent), or the device does not acknowledge the command.
1085 def set_calibration(self, channel, voltage_offset): 1086 """Calibrate the zero code of an output channel. 1087 1088 The offset is added to every voltage the channel produces, to 1089 correct for DAC offset error. It is stored in the device's 1090 EEPROM and reloaded on boot, so it only needs to be set once. 1091 1092 Requires Pulse Pal hardware v3 or newer. 1093 1094 Args: 1095 channel: Output channel number, 1-4. 1096 voltage_offset: Offset to apply, in volts [-0.1, 0.1]. 1097 1098 Raises: 1099 PulsePalError: If the connected hardware is older than v3, 1100 `channel` is not 1-4, `voltage_offset` is outside 1101 [-0.1, 0.1], or the device does not acknowledge the 1102 command. 1103 """ 1104 if self.info.hardware_version < 3: 1105 raise PulsePalError( 1106 "set_calibration() requires hardware v3 or newer." 1107 ) 1108 if not (isinstance(channel, numbers.Integral) and channel in (1, 2, 3, 4)): 1109 raise PulsePalError("channel must be 1, 2, 3 or 4") 1110 if not (isinstance(voltage_offset, numbers.Real) and -0.1 <= voltage_offset <= 0.1): # Also refuses NaN 1111 raise PulsePalError( 1112 "voltage_offset for zero code calibration must be in range " 1113 "[-0.1, 0.1]" 1114 ) 1115 # To the nearest DAC code, halfway values to the even one, as the MATLAB class rounds it 1116 voltage_bits = int(round(voltage_offset * (1 / (20 / 65536)))) 1117 self._write_serial( 1118 (self._OP_MENU_BYTE, 96, channel - 1), 1119 "uint8", 1120 voltage_bits, 1121 "int16", 1122 ) 1123 self._read_ack("set_calibration()")
Calibrate the zero code of an output channel.
The offset is added to every voltage the channel produces, to correct for DAC offset error. It is stored in the device's EEPROM and reloaded on boot, so it only needs to be set once.
Requires Pulse Pal hardware v3 or newer.
Arguments:
- channel: Output channel number, 1-4.
- voltage_offset: Offset to apply, in volts [-0.1, 0.1].
Raises:
- PulsePalError: If the connected hardware is older than v3,
channelis not 1-4,voltage_offsetis outside [-0.1, 0.1], or the device does not acknowledge the command.
1125 def set_screen_saver(self, enabled, timeout=1800): 1126 """Switch the device's screen saver on or off, and set its timeout. 1127 1128 With the screen saver on, the device dims its screen once it has 1129 been left alone for `timeout` seconds: no command from the 1130 computer, no rising edge on a trigger channel, and no joystick 1131 click or push. The next of these brings the screen back. Both 1132 settings are stored in the device's EEPROM and kept through power 1133 cycles; the screen saver can also be switched on and off from the 1134 device's joystick menu. A new device has it on, with 1800 s. 1135 1136 The device saves the settings once no channel is playing. Saving 1137 pauses its timer, usually for about 20 us but, once in about 2000 1138 changes, for tens of milliseconds, which would delay a trigger. 1139 Change the settings before an experiment rather than during one. 1140 1141 Requires firmware v22 or newer. Only Pulse Pal 3 has a screen 1142 saver: Pulse Pal 2 accepts `enabled=False` only. 1143 1144 ```python 1145 P.set_screen_saver(True, 300) # dim after 5 minutes 1146 P.set_screen_saver(False) 1147 ``` 1148 1149 Args: 1150 enabled: True (or 1) to switch the screen saver on, False (or 1151 0) to switch it off. 1152 timeout: Seconds without activity before the screen dims, a 1153 whole number from 1 to 65535. It is sent with every call, 1154 so leaving it out sets 1800 s. 1155 1156 Raises: 1157 PulsePalError: If the firmware is older than v22, the 1158 screen saver is switched on with hardware older than v3, 1159 a value is out of range, or the device does not 1160 acknowledge the command. 1161 """ 1162 self._require_firmware(22, "set_screen_saver()") 1163 enabled = to_bool(enabled, "enabled") 1164 if not ( 1165 isinstance(timeout, numbers.Real) 1166 and not isinstance(timeout, bool) 1167 and math.isfinite(timeout) 1168 and timeout == int(timeout) 1169 and 1 <= timeout <= 65535 1170 ): 1171 raise PulsePalError( 1172 "timeout must be a whole number of seconds from 1 to 65535" 1173 ) 1174 if enabled and (self.info.hardware_version or 2) < 3: 1175 raise PulsePalError( 1176 "The screen saver requires hardware v3 or newer: Pulse Pal 2 " 1177 "accepts set_screen_saver(False) only." 1178 ) 1179 self._write_serial( 1180 (self._OP_MENU_BYTE, 99, int(enabled)), 1181 "uint8", 1182 int(timeout), 1183 "uint16", 1184 ) 1185 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 the
computer, no rising edge on a trigger channel, and no joystick
click or push. The next of these brings the screen back. Both
settings are stored in the device's EEPROM and kept through power
cycles; the screen saver can also be switched on and off from the
device's joystick menu. A new device has it on, with 1800 s.
The device saves the settings once no channel is playing. Saving pauses its timer, usually for about 20 us but, once in about 2000 changes, for tens of milliseconds, which would delay a trigger. Change the settings before an experiment rather than during one.
Requires firmware v22 or newer. Only Pulse Pal 3 has a screen
saver: Pulse Pal 2 accepts enabled=False only.
P.set_screen_saver(True, 300) # dim after 5 minutes
P.set_screen_saver(False)
Arguments:
- enabled: True (or 1) to switch the screen saver on, False (or 0) to switch it off.
- timeout: Seconds without activity before the screen dims, a whole number from 1 to 65535. It is sent with every call, so leaving it out sets 1800 s.
Raises:
- PulsePalError: If the firmware is older than v22, the screen saver is switched on with hardware older than v3, a value is out of range, or the device does not acknowledge the command.
1191 def send_custom_pulse_train( 1192 self, 1193 custom_train_id, 1194 pulse_times, 1195 voltages, 1196 ): 1197 """Load a custom pulse train onto the device. 1198 1199 A custom pulse train is an arbitrary list of pulse onset times 1200 and voltages, replacing the parametric pulse voltage and onset 1201 timing. Set `PulsePalDevice.custom_train_id` on an output channel 1202 to play the train there. Each pulse takes the channel's own phase 1203 durations; a biphasic pulse's second phase is its voltage with the 1204 sign reversed. A biphasic pulse always plays whole, and a pulse 1205 whose time comes while it plays is dropped, so that every pulse 1206 played is on time. 1207 1208 ```python 1209 P.send_custom_pulse_train( 1210 2, [0, 0.2, 0.5, 1], [8, 4, -3.5, -10] 1211 ) 1212 P.custom_train_id[1] = 2 1213 ``` 1214 1215 Loading a train stops the output channels playing it, and their 1216 next trigger plays the new train. On Pulse Pal 3, while a trigger 1217 channel is in param sync mode (see `PulsePalDevice.trigger_mode`), 1218 the device stores the train instead, as it stores the parameters 1219 `PulsePalDevice.sync_to_device` sends: channels go on playing the 1220 old train, and the next rising edge on the param sync channel 1221 makes the new one current. A channel playing the old train at that 1222 edge finishes it, and plays the new one from its next trigger. So 1223 the next trial's custom trains can be loaded during the current 1224 trial. Leaving param sync mode makes a stored train current at 1225 once. 1226 1227 Args: 1228 custom_train_id: Custom train to load, from 1 to 1229 `DeviceInfo.n_custom_pulse_trains` (2 on Pulse Pal 2, 1230 4 on Pulse Pal 3). 1231 pulse_times: Pulse onset times, in seconds, relative to the 1232 start of the train, increasing. Each is a multiple of 1233 `DeviceInfo.min_pulse_width_us` (10 us on Pulse Pal 3, 1234 100 us on Pulse Pal 2), from 0 to 1235 `MAX_TIME`. Accepts a list, tuple or 1-D NumPy array. 1236 voltages: Voltage of each pulse, in volts [-10, 10]. Must be 1237 the same length as `pulse_times`. 1238 1239 Raises: 1240 PulsePalError: If `custom_train_id` is out of range, 1241 `pulse_times` or `voltages` is not one-dimensional (a 2-D 1242 array is refused, not flattened), they differ in length, 1243 there are no pulses, or more than 1244 `DeviceInfo.max_custom_pulses`, a pulse time is not a 1245 multiple of `DeviceInfo.min_pulse_width_us`, is out of 1246 range or is not later than the one before it, or the 1247 device does not acknowledge the command. 1248 """ 1249 pulse_times_cycles = self._custom_times_to_cycles(pulse_times, "pulse_times") 1250 pulse_voltage_bits = self._volts_array_to_bits(voltages, "voltages") 1251 if len(pulse_times_cycles) != len(pulse_voltage_bits): 1252 raise PulsePalError( 1253 "pulse_times and voltages must be the same length." 1254 ) 1255 self._send_custom_train( 1256 custom_train_id, 1257 pulse_times_cycles, 1258 pulse_voltage_bits, 1259 "send_custom_pulse_train()", 1260 )
Load a custom pulse train onto the device.
A custom pulse train is an arbitrary list of pulse onset times
and voltages, replacing the parametric pulse voltage and onset
timing. Set PulsePalDevice.custom_train_id on an output channel
to play the train there. Each pulse takes the channel's own phase
durations; a biphasic pulse's second phase is its voltage with the
sign reversed. A biphasic pulse always plays whole, and a pulse
whose time comes while it plays is dropped, so that every pulse
played is on time.
P.send_custom_pulse_train(
2, [0, 0.2, 0.5, 1], [8, 4, -3.5, -10]
)
P.custom_train_id[1] = 2
Loading a train stops the output channels playing it, and their
next trigger plays the new train. On Pulse Pal 3, while a trigger
channel is in param sync mode (see PulsePalDevice.trigger_mode),
the device stores the train instead, as it stores the parameters
PulsePalDevice.sync_to_device sends: channels go on playing the
old train, and the next rising edge on the param sync channel
makes the new one current. A channel playing the old train at that
edge finishes it, and plays the new one from its next trigger. So
the next trial's custom trains can be loaded during the current
trial. Leaving param sync mode makes a stored train current at
once.
Arguments:
- custom_train_id: Custom train to load, from 1 to
DeviceInfo.n_custom_pulse_trains(2 on Pulse Pal 2, 4 on Pulse Pal 3). - pulse_times: Pulse onset times, in seconds, relative to the
start of the train, increasing. Each is a multiple of
DeviceInfo.min_pulse_width_us(10 us on Pulse Pal 3, 100 us on Pulse Pal 2), from 0 toMAX_TIME. Accepts a list, tuple or 1-D NumPy array. - voltages: Voltage of each pulse, in volts [-10, 10]. Must be
the same length as
pulse_times.
Raises:
- PulsePalError: If
custom_train_idis out of range,pulse_timesorvoltagesis not one-dimensional (a 2-D array is refused, not flattened), they differ in length, there are no pulses, or more thanDeviceInfo.max_custom_pulses, a pulse time is not a multiple ofDeviceInfo.min_pulse_width_us, is out of range or is not later than the one before it, or the device does not acknowledge the command.
1262 def send_custom_waveform( 1263 self, 1264 custom_train_id, 1265 sampling_period, 1266 voltages, 1267 ): 1268 """Load an arbitrary waveform onto the device. 1269 1270 A convenience shorthand for 1271 `PulsePalDevice.send_custom_pulse_train` with evenly spaced, 1272 adjoining pulses, so that `voltages` is played as a waveform 1273 sampled every `sampling_period` seconds. It is loaded as a custom 1274 train is: see `PulsePalDevice.send_custom_pulse_train` for what 1275 happens to channels playing it, and in param sync mode. 1276 1277 Set the channel's `PulsePalDevice.phase1_duration` to 1278 `sampling_period` as well, so that each sample is held for the 1279 sampling period. 1280 1281 ```python 1282 import math 1283 1284 samples = [math.sin(i / 10.0) * 10 for i in range(1000)] 1285 P.send_custom_waveform(1, 0.001, samples) # 1 kHz 1286 P.custom_train_id[2] = 1 1287 P.phase1_duration[2] = 0.001 1288 ``` 1289 1290 Args: 1291 custom_train_id: Custom train to load, from 1 to 1292 `DeviceInfo.n_custom_pulse_trains` (2 on Pulse Pal 2, 1293 4 on Pulse Pal 3). 1294 sampling_period: In seconds: a multiple of 1295 `DeviceInfo.min_pulse_width_us` (10 us on Pulse Pal 3, 1296 100 us on Pulse Pal 2). Each voltage is 1297 held for this long. 1298 voltages: Waveform samples, in volts [-10, 10]. 1299 Accepts a list, tuple or 1-D NumPy array. 1300 1301 Raises: 1302 PulsePalError: If `custom_train_id` is out of range, there 1303 are no samples, or more than 1304 `DeviceInfo.max_custom_pulses`, `sampling_period` is not a 1305 positive multiple of `DeviceInfo.min_pulse_width_us`, the 1306 last sample would start after `MAX_TIME`, or the device 1307 does not acknowledge the command. 1308 """ 1309 pulse_voltage_bits = self._volts_array_to_bits(voltages, "voltages") 1310 period_cycles = self._custom_time_cycles(sampling_period, "sampling_period") 1311 if period_cycles == 0: 1312 raise PulsePalError( 1313 f"sampling_period must be a positive multiple of {self.info.min_pulse_width_us:g} us. " 1314 f"Received {sampling_period!r}." 1315 ) 1316 pulse_times = period_cycles * np.arange(len(pulse_voltage_bits), dtype=np.int64) 1317 self._send_custom_train( 1318 custom_train_id, 1319 pulse_times, 1320 pulse_voltage_bits, 1321 "send_custom_waveform()", 1322 )
Load an arbitrary waveform onto the device.
A convenience shorthand for
PulsePalDevice.send_custom_pulse_train with evenly spaced,
adjoining pulses, so that voltages is played as a waveform
sampled every sampling_period seconds. It is loaded as a custom
train is: see PulsePalDevice.send_custom_pulse_train for what
happens to channels playing it, and in param sync mode.
Set the channel's PulsePalDevice.phase1_duration to
sampling_period as well, so that each sample is held for the
sampling period.
import math
samples = [math.sin(i / 10.0) * 10 for i in range(1000)]
P.send_custom_waveform(1, 0.001, samples) # 1 kHz
P.custom_train_id[2] = 1
P.phase1_duration[2] = 0.001
Arguments:
- custom_train_id: Custom train to load, from 1 to
DeviceInfo.n_custom_pulse_trains(2 on Pulse Pal 2, 4 on Pulse Pal 3). - sampling_period: In seconds: a multiple of
DeviceInfo.min_pulse_width_us(10 us on Pulse Pal 3, 100 us on Pulse Pal 2). Each voltage is held for this long. - voltages: Waveform samples, in volts [-10, 10]. Accepts a list, tuple or 1-D NumPy array.
Raises:
- PulsePalError: If
custom_train_idis out of range, there are no samples, or more thanDeviceInfo.max_custom_pulses,sampling_periodis not a positive multiple ofDeviceInfo.min_pulse_width_us, the last sample would start afterMAX_TIME, or the device does not acknowledge the command.
1324 def trigger(self, channels): 1325 """Trigger output channels in software. 1326 1327 The channels start their pulse trains together, in the same timer 1328 cycle. A channel that is already playing a pulse train ignores 1329 the trigger, and `PulsePalDevice.stop` cancels a trigger that has 1330 not started its channel yet. 1331 1332 ```python 1333 P.trigger(3) # one channel 1334 P.trigger([1, 4]) # several: a list, tuple or NumPy array 1335 ``` 1336 1337 Args: 1338 channels: Output channel number, 1-4, or several as a list, 1339 tuple or NumPy array. 1340 1341 Raises: 1342 PulsePalError: If a channel number is not 1-4, or no channel 1343 is given. Nothing is triggered. 1344 """ 1345 self._write_serial((self._OP_MENU_BYTE, 77, _common.channel_bits(channels)), "uint8")
Trigger output channels in software.
The channels start their pulse trains together, in the same timer
cycle. A channel that is already playing a pulse train ignores
the trigger, and PulsePalDevice.stop cancels a trigger that has
not started its channel yet.
P.trigger(3) # one channel
P.trigger([1, 4]) # several: 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 number is not 1-4, or no channel is given. Nothing is triggered.
1347 def stop(self, channels=None): 1348 """Stop pulse trains currently playing on the device. 1349 1350 The stopped channels return to their 1351 `PulsePalDevice.resting_voltage`. A soft trigger that has not 1352 started its channel yet is cancelled too. 1353 1354 ```python 1355 P.stop() # all output channels 1356 P.stop([1, 3, 4]) # channels 1, 3 and 4 1357 ``` 1358 1359 Args: 1360 channels: Output channel number, 1-4, or several as a list, 1361 tuple or NumPy array. `None`, the default, stops all 1362 channels. Stopping some channels requires firmware v22 1363 or newer. 1364 1365 Raises: 1366 PulsePalError: If a channel number is not 1-4, or channels are 1367 given and the firmware is older than v22. 1368 """ 1369 if self.info.firmware_version < 22: 1370 if channels is not None: 1371 raise PulsePalError("stop() cannot address individual channels before firmware v22") 1372 self._write_serial((self._OP_MENU_BYTE, 80), "uint8") # Stops all channels 1373 return 1374 bits = 0x0F if channels is None else _common.channel_bits(channels) 1375 self._write_serial((self._OP_MENU_BYTE, 98, bits), "uint8")
Stop pulse trains currently playing on the device.
The stopped channels return to their
PulsePalDevice.resting_voltage. A soft trigger that has not
started its channel yet is cancelled too.
P.stop() # all output channels
P.stop([1, 3, 4]) # channels 1, 3 and 4
Arguments:
- channels: Output channel number, 1-4, or several as a list,
tuple or NumPy array.
None, the default, stops all channels. Stopping some channels requires firmware v22 or newer.
Raises:
- PulsePalError: If a channel number is not 1-4, or channels are given and the firmware is older than v22.
1377 def status(self): 1378 """Read which channels are playing, and the counters since the last call. 1379 1380 The counters (see `DeviceStatus`) count from the previous call, or 1381 from the device's startup, and each call clears them. Calling it 1382 before and after a trial checks that the TTLs the trial sent 1383 arrived, and started the trains they should: 1384 1385 ```python 1386 P.status() # Clears the counters 1387 run_trial() # Your code: one TTL on trigger channel 1, linked to channels 1 and 2 1388 s = P.status() 1389 if s.trigger_edges[1] != 1 or s.channel_starts[1:3] != [1, 1]: 1390 print("The trial's trains did not all start") 1391 ``` 1392 1393 Requires firmware v22 or newer. 1394 1395 Returns: 1396 A `DeviceStatus`. 1397 1398 Raises: 1399 PulsePalError: If the firmware is older than v22, or the device 1400 does not reply. 1401 """ 1402 self._require_firmware(22, "status()") 1403 self._write_serial((self._OP_MENU_BYTE, 101), "uint8") 1404 values = struct.unpack(f"{self._ENDIANNESS}B7I", self._read_raw(self._STATUS_MESSAGE_BYTES)) 1405 return DeviceStatus( 1406 playing=[ch for ch in range(1, 5) if values[0] & (1 << (ch - 1))], 1407 trigger_edges=[None, *values[1:3]], 1408 channel_starts=[None, *values[3:7]], 1409 comm_failures=values[7], 1410 )
Read which channels are playing, and the counters since the last call.
The counters (see DeviceStatus) count from the previous call, or
from the device's startup, and each call clears them. Calling it
before and after a trial checks that the TTLs the trial sent
arrived, and started the trains they should:
P.status() # Clears the counters
run_trial() # Your code: one TTL on trigger channel 1, linked to channels 1 and 2
s = P.status()
if s.trigger_edges[1] != 1 or s.channel_starts[1:3] != [1, 1]:
print("The trial's trains did not all start")
Requires firmware v22 or newer.
Returns:
A
DeviceStatus.
Raises:
- PulsePalError: If the firmware is older than v22, or the device does not reply.
1412 def save_program_to_microsd(self, file_name): 1413 """Save the parameters to the device's microSD card, as a program. 1414 1415 The program can be loaded later with 1416 `PulsePalDevice.load_program_from_microsd` or with the joystick 1417 menu's Load Program item. A file of the same name is replaced. To 1418 keep the parameters on this computer instead, see 1419 `PulsePalDevice.export_params`. 1420 1421 ```python 1422 P.save_program_to_microsd("MyProtocol.pps") 1423 ``` 1424 1425 Args: 1426 file_name: At most 15 ASCII characters, ending in `.pps`. 1427 1428 Raises: 1429 PulsePalError: If the file name is invalid, or the device could 1430 not write the file to its microSD card. Pulse Pal 2 also 1431 refuses while a channel is playing: its card shares a bus with 1432 the outputs, so stop playback first. 1433 """ 1434 self._settings_file_op(file_name, 1, "save_program_to_microsd()")
Save the parameters to the device's microSD card, as a program.
The program can be loaded later with
PulsePalDevice.load_program_from_microsd or with the joystick
menu's Load Program item. A file of the same name is replaced. To
keep the parameters on this computer instead, see
PulsePalDevice.export_params.
P.save_program_to_microsd("MyProtocol.pps")
Arguments:
- file_name: At most 15 ASCII characters, ending in
.pps.
Raises:
- PulsePalError: If the file name is invalid, or the device could not write the file to its microSD card. Pulse Pal 2 also refuses while a channel is playing: its card shares a bus with the outputs, so stop playback first.
1436 def load_program_from_microsd(self, file_name): 1437 """Load a program from the device's microSD card. 1438 1439 The device plays the program, and the parameters are read back 1440 into this object (see `PulsePalDevice.sync_from_device`). The 1441 program's trigger modes take effect too, and it is loaded at once, 1442 also in param sync mode. 1443 1444 Args: 1445 file_name: The program's file name, ending in `.pps`, as saved 1446 by `PulsePalDevice.save_program_to_microsd` or the joystick 1447 menu. 1448 1449 Raises: 1450 PulsePalError: If the file name is invalid, or the device could 1451 not load the program: there is no such file on its microSD 1452 card, or it is not a valid Pulse Pal program; or, on Pulse 1453 Pal 2, a channel is playing (see `save_program_to_microsd`). 1454 The device then keeps its parameters, and so does this object. 1455 """ 1456 self._settings_file_op(file_name, 2, "load_program_from_microsd()")
Load a program from the device's microSD card.
The device plays the program, and the parameters are read back
into this object (see PulsePalDevice.sync_from_device). The
program's trigger modes take effect too, and it is loaded at once,
also in param sync mode.
Arguments:
- file_name: The program's file name, ending in
.pps, as saved byPulsePalDevice.save_program_to_microsdor the joystick menu.
Raises:
- PulsePalError: If the file name is invalid, or the device could
not load the program: there is no such file on its microSD
card, or it is not a valid Pulse Pal program; or, on Pulse
Pal 2, a channel is playing (see
save_program_to_microsd). The device then keeps its parameters, and so does this object.
1458 def delete_program_from_microsd(self, file_name): 1459 """Delete a program from the device's microSD card. 1460 1461 Args: 1462 file_name: The program's file name, ending in `.pps`. 1463 1464 Raises: 1465 PulsePalError: If the file name is invalid, or the device could 1466 not delete the file, e.g. because there is no such file, or, 1467 on Pulse Pal 2, because a channel is playing. 1468 """ 1469 self._settings_file_op(file_name, 3, "delete_program_from_microsd()")
Delete a program from the device's microSD card.
Arguments:
- file_name: The program's file name, ending in
.pps.
Raises:
- PulsePalError: If the file name is invalid, or the device could not delete the file, e.g. because there is no such file, or, on Pulse Pal 2, because a channel is playing.
1513 def format_microsd(self, *, confirm=True, timeout=30): 1514 """Format the device's microSD card. 1515 1516 Erases every program saved on the device and resets its 1517 parameters to the defaults. By default, asks at the console for 1518 confirmation first; a script that runs on its own, or an AI agent, 1519 passes `confirm=False`. 1520 1521 Requires Pulse Pal hardware v3 or newer. 1522 1523 Args: 1524 confirm: If `True`, ask at the console before anything is 1525 erased. If `False`, format at once. 1526 timeout: Seconds to wait for the device to report that 1527 formatting has finished. 1528 1529 Returns: 1530 `True` once the card has been formatted, or `False` if the 1531 user declines the confirmation prompt. 1532 1533 Raises: 1534 PulsePalError: If the connected hardware is older than v3, if 1535 the device reports that formatting failed, or if it does 1536 not report a result within `timeout` seconds. 1537 """ 1538 if self.info.hardware_version < 3: 1539 raise PulsePalError( 1540 "format_microsd() requires hardware v3 or newer." 1541 ) 1542 1543 if to_bool(confirm, "confirm"): 1544 print("*** Pulse Pal microSD Formatter ***") 1545 print("This will format Pulse Pal's microSD card,") 1546 print("erase all programs saved on the device") 1547 print("and reset all parameters to defaults.") 1548 reply = input("Do you want to continue (y/n)") 1549 if reply.strip().lower() != "y": 1550 print("Choice confirmed - microSD Card NOT formatted.") 1551 return False 1552 1553 self._write_serial((self._OP_MENU_BYTE, 97), "uint8", list(_FORMAT_KEY), "uint8") 1554 1555 # The device replies with lines of status text, the last of which 1556 # contains "!", and then a confirm byte (1 if the card was formatted, 1557 # 0 if not). The confirm byte is sent after the device has reloaded 1558 # its default parameters, so it can arrive well after the text. It 1559 # must be read here: left in the buffer, it would be taken as the 1560 # reply to the next command, and every reply after that would be 1561 # read one byte late. 1562 start = time.time() 1563 message = bytearray() 1564 flag_index = -1 1565 line_end = -1 1566 1567 while time.time() - start < timeout: 1568 n_waiting = self.port.in_waiting 1569 if n_waiting: 1570 message.extend(self.port.read(n_waiting)) 1571 flag_index = message.find(b"!") 1572 if flag_index >= 0: 1573 line_end = message.find(b"\n", flag_index) 1574 if line_end >= 0 and len(message) > line_end + 1: 1575 break 1576 time.sleep(0.01) 1577 if line_end < 0 or len(message) <= line_end + 1: 1578 raise PulsePalError( 1579 "Pulse Pal did not report the result of formatting " 1580 f"its microSD card within {timeout} s. {_common.NO_REPLY_HINT}" 1581 ) 1582 self._reply_pending = None # The whole reply is read (see _write_raw()) 1583 confirm_byte = message[line_end + 1] 1584 1585 text = bytes(message[:flag_index]).decode( 1586 "ascii", errors="replace" 1587 ).rstrip() 1588 if text: 1589 print(text) 1590 1591 # The device has loaded its own defaults. They are this class's, but the device keeps no record of 1592 # this object's other changes (continuous loop mode, for one), so the defaults are programmed, as the 1593 # MATLAB class does, and this object describes the device again. 1594 self.set_default_params() 1595 if confirm_byte != 1: 1596 raise PulsePalError( 1597 "Pulse Pal could not format its microSD card." 1598 ) 1599 return True
Format the device's microSD card.
Erases every program saved on the device and resets its
parameters to the defaults. By default, asks at the console for
confirmation first; a script that runs on its own, or an AI agent,
passes confirm=False.
Requires Pulse Pal hardware v3 or newer.
Arguments:
- confirm: If
True, ask at the console before anything is erased. IfFalse, format at once. - timeout: Seconds to wait for the device to report that formatting has finished.
Returns:
Trueonce the card has been formatted, orFalseif the user declines the confirmation prompt.
Raises:
- PulsePalError: If the connected hardware is older than v3, if
the device reports that formatting failed, or if it does
not report a result within
timeoutseconds.
1601 def gui(self, block=None, theme=None): 1602 """Open the Pulse Pal parameter GUI, or focus an open one. 1603 1604 The GUI edits its own copy of the parameters, and loads them to 1605 the device when its 'Load to Device' button is clicked. The 1606 window closes automatically when the device is closed or 1607 deleted. 1608 1609 Calling this while the GUI is already open focuses the existing 1610 window rather than opening a second one. 1611 1612 Args: 1613 block: If `True`, the call returns when the GUI is closed. If 1614 `False`, the call returns immediately, and the host 1615 application must run the Tk event loop. If `None`, the 1616 GUI blocks only when the host does not already provide a 1617 Tk event loop, e.g. when launched from a script. 1618 theme: `"light"` or `"dark"` to select the color theme, or 1619 `None` to match the desktop theme. Passing a theme to an 1620 already-open GUI recolors it in place. 1621 1622 Returns: 1623 The `pulsepal.gui.PulsePalGUI` instance driving the window. 1624 1625 Raises: 1626 ValueError: If the theme name is not recognized. 1627 """ 1628 gui = getattr(self, "_gui", None) 1629 if gui is not None and not gui.is_closed: 1630 if theme is not None: 1631 gui.set_theme(theme) 1632 gui.focus() 1633 return gui 1634 1635 from .gui import PulsePalGUI 1636 1637 gui = PulsePalGUI(self, theme=theme) 1638 self._gui = gui 1639 gui.start(block=block) 1640 return gui
Open the Pulse Pal parameter GUI, or focus an open one.
The GUI edits its own copy of the parameters, and loads them to the device when its 'Load to Device' button is clicked. The window closes automatically when the device is closed or deleted.
Calling this while the GUI is already open focuses the existing window rather than opening a second one.
Arguments:
- block: If
True, the call returns when the GUI is closed. IfFalse, the call returns immediately, and the host application must run the Tk event loop. IfNone, the GUI blocks only when the host does not already provide a Tk event loop, e.g. when launched from a script. - theme:
"light"or"dark"to select the color theme, orNoneto match the desktop theme. Passing a theme to an already-open GUI recolors it in place.
Returns:
The
pulsepal.gui.PulsePalGUIinstance driving the window.
Raises:
- ValueError: If the theme name is not recognized.
1646 def close(self): 1647 """Close the connection to the device, and the GUI if open. 1648 1649 The device stops all output channels when the client disconnects, 1650 and shows its own name on its screen again. Safe to call more 1651 than once; later calls do nothing. Called automatically when 1652 leaving a `with` block and when the object is garbage collected, 1653 so pulse trains also stop when the last reference to the object 1654 goes, e.g. when a function that created it returns. 1655 """ 1656 self._close(send_disconnect=True)
Close the connection to the device, and the GUI if open.
The device stops all output channels when the client disconnects,
and shows its own name on its screen again. Safe to call more
than once; later calls do nothing. Called automatically when
leaving a with block and when the object is garbage collected,
so pulse trains also stop when the last reference to the object
goes, e.g. when a function that created it returns.
264class WavePalDevice: 265 """A class to control a Wave Pal on a USB serial port. 266 267 Creating an instance opens the serial port, checks that the device 268 runs Wave Pal firmware, reads its properties into 269 `WavePalDevice.info`, shows "PYTHON Connected" on the device's 270 screen, stops any playback and programs the default settings (see 271 `WavePalDevice.set_default_params`). 272 273 ```python 274 from pulsepal import WavePalDevice 275 276 W = WavePalDevice("COM3") 277 W.load_waveform(1, [0, 1, 2, 3, 4, 5, 0], 1) # waveform 1, played by channel 1 278 W.trigger(1) 279 W.close() 280 ``` 281 282 Replace "COM3" with the device's USB serial port name, which 283 `WavePalDevice.serialportlist` lists. `WavePalDevice` is also a 284 context manager, which closes the port on exit. 285 286 Closing the connection stops playback, as it does on Pulse Pal, and 287 puts the device's own name back on its screen. The device keeps its 288 settings and waveforms, so TTL triggers still play them. 289 """ 290 291 port: "serial.Serial" 292 """The open `serial.Serial` port connected to the device.""" 293 294 info: DeviceInfo 295 """Properties of the connected device. See `DeviceInfo`.""" 296 297 _CURRENT_FIRMWARE_VERSION = 1 298 _BAUD_RATE = 12000000 # USB serial ignores the baud rate 299 300 _OP_MENU_BYTE = _common.OP_MENU_BYTE 301 _OP_HANDSHAKE = _common.OP_HANDSHAKE 302 _OP_DISCONNECT = 81 303 _OP_SET_CLIENT_NAME = 89 304 _OP_SET_SCREEN_SAVER = 99 305 _OP_HARDWARE_REVISION = 100 306 _OP_HARDWARE_INFO = ord("N") 307 _OP_SET_SAMPLING_RATE = ord("S") 308 _OP_SET_OUTPUT_RANGE = ord("R") 309 _OP_LOAD_WAVEFORM = ord("L") 310 _OP_SET_CURRENT_WAVEFORMS = ord("C") 311 _OP_TRIGGER = ord("P") 312 _OP_STOP = ord("X") 313 _OP_SET_FIXED_VOLTAGE = ord("!") 314 _OP_SET_LOOP_MODE = ord("O") 315 _OP_SET_LOOP_DURATION = ord("D") 316 _OP_SET_TRIGGER_MODE = ord("T") 317 _OP_SET_TRIGGER_LINKS = ord("I") 318 _OP_GET_STATUS = ord("G") 319 _OP_GET_PLAYBACK_CHECKSUMS = ord("Z") 320 321 _HANDSHAKE_REPLY = 87 # 'W' 322 _HARDWARE_INFO_FORMAT = "<BBIIIIB" 323 _STATUS_FORMAT = "<B4I4II4BBB4B" 324 _DAC_BITMAX = 65535 325 _UINT32_MAX = 2**32 - 1 326 _ALL_CHANNELS = 0x0F 327 # The ops that carry a setting, which _discard_lost_reply() sends again if the reply to one is lost 328 _SETTING_OPS = frozenset(map(ord, "SRCODTI")) 329 330 _reply_pending = None # The op code of a command whose reply has not been read in full yet (see _write_command()) 331 332 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 333 """Open a connection to a Wave Pal. 334 335 Args: 336 port_name: USB serial port of the device, such as `COM3` on 337 Windows or `/dev/ttyACM0` on Linux. 338 serial_number: Instead of `port_name`, the device's USB serial 339 number (`DeviceInfo.serial_number`), which finds its port 340 wherever it is plugged in. 341 timeout: Serial read timeout, in seconds. Loading a long 342 waveform onto a slow microSD card can take a few seconds. 343 344 Firmware newer than this module knows is used with a warning: new 345 firmware only adds commands. Update the package to use what is new. 346 347 Raises: 348 PulsePalError: If the device does not reply to the handshake, or 349 runs other firmware. 350 serial.SerialException: If the serial port cannot be opened, 351 e.g. because another program has it open (on Linux and 352 macOS, another session of these classes, which lock it). 353 """ 354 self._closed = True 355 self.info = DeviceInfo() 356 self._sampling_rate = None 357 self._output_range = None 358 self._waveforms = [] # Indexed by waveform number, once the device has said how many it stores 359 self._current_waveform = ChannelSettings( 360 "current_waveform", [0] * 4, self, "_apply_current_waveform") 361 self._loop_mode = ChannelSettings( 362 "loop_mode", [False] * 4, self, "_apply_loop_mode") 363 self._loop_duration = ChannelSettings( 364 "loop_duration", [0.0] * 4, self, "_apply_loop_duration") 365 self._trigger_mode = ChannelSettings( 366 "trigger_mode", ["Normal"] * 2, self, "_apply_trigger_mode") 367 self._link_trigger_channel1 = ChannelSettings( 368 "link_trigger_channel1", [False] * 4, self, 369 "_apply_trigger_channel1_links") 370 self._link_trigger_channel2 = ChannelSettings( 371 "link_trigger_channel2", [False] * 4, self, 372 "_apply_trigger_channel2_links") 373 374 port_name = _common.port_name_from(port_name, serial_number, "WavePalDevice") 375 self.port = serial.Serial( 376 port_name, 377 self._BAUD_RATE, 378 timeout=timeout, 379 rtscts=True, 380 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 381 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 382 # it, such as another of these classes, gets an error instead 383 exclusive=True, 384 ) 385 self._closed = False 386 try: 387 # Discard anything left in the buffer by an earlier session 388 self.port.reset_input_buffer() 389 self._handshake() 390 self._read_hardware_info() 391 self._waveforms = [None] * (self.info.n_waveforms + 1) 392 # Client name op + "PYTHON" in ASCII, shown as "PYTHON Connected" 393 self._write_command(self._OP_SET_CLIENT_NAME, b"PYTHON") 394 self.stop() 395 self.set_default_params() 396 except BaseException: 397 # Op 81 means something else to other devices, so it is sent 398 # only once the device has identified itself as a Wave Pal 399 self._close(send_disconnect=self.info.firmware_version is not None) 400 raise 401 402 @staticmethod 403 def serialportlist(ports_to_list="available"): 404 """Return the names of the USB serial ports on this computer. 405 406 Called on the class, without connecting to a device, to find the 407 port name to pass to `WavePalDevice`. 408 409 Args: 410 ports_to_list: `available` to list only the ports that are 411 not already in use, or `all` to list every USB serial 412 port. Not case sensitive. 413 414 Returns: 415 Sorted list of port names, such as `["COM3", "COM7"]`. 416 417 Raises: 418 PulsePalError: If `ports_to_list` is not `available` or `all`. 419 """ 420 return _common.serialportlist(ports_to_list) 421 422 # ------------------------------------------------------------------ 423 # Settings 424 # ------------------------------------------------------------------ 425 426 def set_default_params(self): 427 """Program the default settings on the device. 428 429 The defaults are both trigger channels in normal mode, a 10 kHz 430 sampling rate, the -10 V to 10 V output range, loop mode off with 431 loop durations of 0, no output channel linked to a trigger channel 432 (so a TTL starts nothing until a script links it), and no current 433 waveform on any channel. They match the settings the device starts with. The 434 trigger modes are sent first: a trigger channel leaving param sync 435 mode makes what was stored for its next edge current, and the 436 settings after it then apply at once. 437 438 Loaded waveforms are kept, and loaded again if the output range 439 changes (see `WavePalDevice.output_range`). 440 441 Raises: 442 PulsePalError: If a loaded waveform does not fit the default 443 output range, -10 V to 10 V. 444 """ 445 self.trigger_mode = ["Normal"] * 2 446 self.sampling_rate = 10000 447 self.output_range = "-10V:10V" 448 self.loop_mode = [False] * 4 449 self.loop_duration = [0] * 4 450 self._set_trigger_links([False] * 4, [False] * 4) 451 self.current_waveform = [0] * 4 452 453 @property 454 def sampling_rate(self): 455 """Sampling rate of all output channels, in Hz. 456 457 A whole number from 1 to `DeviceInfo.max_sampling_rate`. It can be 458 changed during playback. The rate played can differ slightly from 459 the rate set: see `WavePalDevice.actual_sampling_rate`. 460 """ 461 return self._sampling_rate 462 463 @sampling_rate.setter 464 def sampling_rate(self, rate): 465 rate_hz = self._normalize_sampling_rate(rate) 466 # Loop durations are sent in samples, so check that they still fit 467 # before anything is changed 468 loop_samples = None 469 if self._sampling_rate is not None: 470 loop_samples = self._durations_to_samples( 471 self._loop_duration[1:], rate_hz) 472 self._write_command(self._OP_SET_SAMPLING_RATE, 473 struct.pack("<I", rate_hz)) 474 self._read_ack("setting sampling_rate") 475 self._sampling_rate = rate_hz 476 if loop_samples is not None: 477 self._send_loop_duration_samples(loop_samples) 478 479 @property 480 def actual_sampling_rate(self): 481 """The sampling rate the device plays, in Hz. 482 483 The device divides `DeviceInfo.sample_clock_hz` (24 MHz) by a 484 whole number, so the rate played is the nearest one of those to 485 `WavePalDevice.sampling_rate`. For example, 44100 Hz plays at 486 44117.6 Hz. Rates that divide 24 MHz exactly, such as 10 kHz, 487 25 kHz or 100 kHz, play exactly. 488 """ 489 return self._actual_rate(self._sampling_rate) 490 491 @property 492 def output_range(self): 493 """Voltage range of all output channels. 494 495 One of `DeviceInfo.output_ranges`: `"0V:5V"`, `"0V:10V"`, 496 `"-5V:5V"` or `"-10V:10V"`. The smallest range that fits the 497 waveforms gives the finest voltage steps. 498 499 Changing the range stops playback, sets all outputs to 0 V, and 500 loads the waveforms again, encoded for the new range. It raises 501 an error, and changes nothing, if a loaded waveform does not fit 502 the new range, or if a trigger channel is in param sync mode, 503 where the loads would wait for a TTL edge. 504 """ 505 return self._output_range 506 507 @output_range.setter 508 def output_range(self, range_name): 509 name = self._check_output_range(range_name, self._trigger_mode[1:]) 510 changed = name != self._output_range 511 range_index = list(OUTPUT_RANGES).index(name) 512 self._write_command(self._OP_SET_OUTPUT_RANGE, bytes([range_index])) 513 self._read_ack("setting output_range") 514 self._output_range = name 515 if changed: 516 # The device unloads the waveforms when the range changes, 517 # because their samples encode voltages in the old range. The 518 # channels keep their current waveforms. 519 waveforms = self._waveforms 520 self._waveforms = [None] * len(waveforms) 521 for number, waveform in enumerate(waveforms): 522 if waveform is not None: 523 self.load_waveform(number, waveform) 524 525 @property 526 def current_waveform(self): 527 """The waveform each output channel plays, by waveform number. 528 529 Indexed by channel number (see "Channel settings" above). Each 530 value is the number of a waveform loaded with 531 `WavePalDevice.load_waveform`, 1 to `DeviceInfo.n_waveforms`, or 532 `0` for none, the default: a channel with no waveform ignores 533 triggers. Several channels can play the same waveform. 534 535 A channel that is playing finishes its waveform: the new one plays 536 from the channel's next start. In param sync mode, a change waits 537 for the next rising edge on the param sync trigger channel (see 538 `WavePalDevice.trigger_mode`). 539 540 ```python 541 W.current_waveform[1] = 2 # channel 1 plays waveform 2 542 W.current_waveform = [1, 1, 1, 1] # all four play waveform 1 543 ``` 544 """ 545 return self._current_waveform 546 547 @current_waveform.setter 548 def current_waveform(self, values): 549 self._current_waveform._assign(values) 550 551 @property 552 def loop_mode(self): 553 """Whether each output channel loops its waveform. 554 555 Indexed by channel number (see "Channel settings" above). `True` 556 repeats the waveform until `WavePalDevice.loop_duration` has 557 elapsed, or until stopped if the loop duration is 0. `False` plays 558 it once. A change applies to playback in progress. 559 """ 560 return self._loop_mode 561 562 @loop_mode.setter 563 def loop_mode(self, values): 564 self._loop_mode._assign(values) 565 566 @property 567 def loop_duration(self): 568 """How long each output channel loops its waveform, in seconds. 569 570 Indexed by channel number (see "Channel settings" above). Applies 571 in loop mode only, and counts from the trigger. The channel stops 572 when it has elapsed, part way through the waveform if need be. `0` 573 loops until the channel is stopped. Durations are rounded to a 574 whole number of samples, and a nonzero duration lasts at least 575 one sample. 576 """ 577 return self._loop_duration 578 579 @loop_duration.setter 580 def loop_duration(self, values): 581 self._loop_duration._assign(values) 582 583 @property 584 def trigger_mode(self): 585 """How each trigger channel acts on the output channels linked to it. 586 587 Indexed by trigger channel number, 1 or 2 (index 0 is unused), as 588 in Pulse Pal. A TTL edge on a trigger channel acts on the output 589 channels linked to it (`WavePalDevice.link_trigger_channel1`, 590 `WavePalDevice.link_trigger_channel2`): 591 592 - `"Normal"`: a rising edge starts the linked channels' waveforms. 593 Channels that are playing ignore it. 594 - `"Toggle"`: a rising edge starts the linked channels, or stops 595 those that are playing. 596 - `"Gated"`: a rising edge starts the linked channels, and a 597 falling edge stops them, unless the other trigger channel is also 598 gated, linked to them, and still high. With loop mode on and a 599 loop duration of 0, a waveform plays for exactly as long as the 600 TTL is high. 601 - `"Param Sync"`: a rising edge starts and stops nothing (the 602 channel's links are ignored). It makes the waveforms loaded, 603 and the `WavePalDevice.current_waveform` set, since the previous 604 edge current. This is how the next trial's waveforms are loaded 605 during the current trial, and played from the instant it starts. 606 - `"Master"`: a rising edge starts the linked channels, and 607 restarts those that are playing from their first sample. 608 609 The first four are Pulse Pal's trigger modes. Names are not case 610 sensitive. 611 612 While a trigger channel is in param sync mode, 613 `WavePalDevice.load_waveform` loads into a second memory block 614 that the device keeps for each waveform. Channels go on playing 615 the waveform it replaces, and triggers still start it, until the 616 edge. A channel that is playing at the edge finishes what it plays, 617 and plays the new waveform from its next start. A later load of the 618 same waveform before the edge replaces the earlier one, and stops 619 any channel still playing the waveform from before the previous 620 edge, since the device reuses that memory block. Changes to 621 `WavePalDevice.current_waveform` wait for the edge in the same 622 way. Other settings apply at once. 623 624 When neither trigger channel is in param sync mode any more, what 625 was waiting for an edge becomes current at once, so 626 `WavePalDevice.waveforms` and `WavePalDevice.current_waveform` 627 describe the device again. (Pulse Pal and Synth Pal discard their 628 stored parameters and settings then; Pulse Pal makes its stored 629 custom trains current, as this class does its waveforms.) 630 `WavePalDevice.output_range` cannot change 631 in param sync mode. To make the waveforms current on the same edge 632 that starts the channels, wire the TTL to the other trigger channel 633 as well: the param sync edge is handled first. 634 635 ```python 636 W.trigger_mode[2] = "Param Sync" # sent at once 637 W.load_waveform(1, next_trial_wave) # waits for trigger channel 2's next rising edge 638 ``` 639 """ 640 return self._trigger_mode 641 642 @trigger_mode.setter 643 def trigger_mode(self, values): 644 self._trigger_mode._assign(values) 645 646 @property 647 def link_trigger_channel1(self): 648 """Whether each output channel is triggered by trigger channel 1. 649 650 Indexed by channel number (see "Channel settings" above). By 651 default no output channel is linked to either trigger channel, so 652 a TTL starts nothing until a script links it. 653 """ 654 return self._link_trigger_channel1 655 656 @link_trigger_channel1.setter 657 def link_trigger_channel1(self, values): 658 self._link_trigger_channel1._assign(values) 659 660 @property 661 def link_trigger_channel2(self): 662 """Whether each output channel is triggered by trigger channel 2. 663 664 Indexed by channel number (see "Channel settings" above). 665 """ 666 return self._link_trigger_channel2 667 668 @link_trigger_channel2.setter 669 def link_trigger_channel2(self, values): 670 self._link_trigger_channel2._assign(values) 671 672 def set_screen_saver(self, enabled, timeout=1800): 673 """Switch the device's screen saver on or off, and set its timeout. 674 675 With the screen saver on, the device dims its screen once it has 676 been left alone for `timeout` seconds: no command from a computer, 677 no rising edge on a trigger channel, and no joystick click or 678 push. The next of these brings the screen back. The device keeps 679 both settings in its EEPROM, shared with Pulse Pal and Synth Pal 680 firmware, and saves them once no channel is playing. A new device 681 starts with the screen saver on and 1800 s. 682 683 Args: 684 enabled: `True` to switch the screen saver on, `False` to 685 switch it off. 686 timeout: Seconds without activity before the screen dims, 1 to 687 65535. It is sent with every call, so leaving it out sets 688 1800 s. 689 690 Raises: 691 PulsePalError: If a value is out of range. 692 """ 693 state = to_bool(enabled, "enabled") 694 if isinstance(timeout, (bool, np.bool_)) \ 695 or not isinstance(timeout, numbers.Integral) \ 696 or not 1 <= timeout <= 65535: 697 raise PulsePalError( 698 "timeout must be a whole number of seconds from 1 to 65535. " 699 f"Received {timeout!r}." 700 ) 701 self._write_command(self._OP_SET_SCREEN_SAVER, 702 struct.pack("<BH", int(state), int(timeout))) 703 self._read_ack("set_screen_saver()") 704 705 def export_params(self): 706 """Return every setting, as a dict of plain values and lists. 707 708 Keyed by setting name: `"sampling_rate"` and `"output_range"`, then 709 the channel settings, each with one value per channel and no 710 unused index 0. It holds only numbers, booleans and names, so it 711 can be saved with `json` and logged with your data. The waveforms 712 are not part of it: `WavePalDevice.waveforms` holds them. 713 714 ```python 715 import json 716 with open("session_settings.json", "w") as f: 717 json.dump(W.export_params(), f) 718 ``` 719 """ 720 params = {"sampling_rate": self._sampling_rate, "output_range": self._output_range} 721 for name in _CHANNEL_SETTINGS: 722 params[name] = list(getattr(self, name))[1:] 723 return params 724 725 def import_params(self, params): 726 """Program the device with settings exported by `WavePalDevice.export_params`. 727 728 All of them are checked first, and nothing is sent if one is 729 invalid. They are then sent in an order that lets each take 730 effect: a trigger channel leaving param sync mode first, then the 731 sampling rate, the output range and the channel settings, and a 732 trigger channel entering param sync mode last. So 733 `WavePalDevice.current_waveform` applies at once, unless a trigger 734 channel was in param sync mode and stays in it: then it waits for 735 the next edge, as an assignment would. Settings missing from 736 `params` keep their values. A name that is not a setting is 737 skipped with a warning, so that settings exported by a newer 738 version of this package, which may have more, can still be 739 imported. 740 741 The waveforms are not settings: load them with 742 `WavePalDevice.load_waveform` first, since a current waveform must 743 be a loaded one. 744 745 ```python 746 import json 747 with open("session_settings.json") as f: 748 W.import_params(json.load(f)) 749 ``` 750 751 Args: 752 params: A dict of setting name to its value: a number for 753 `"sampling_rate"`, a name for `"output_range"`, and one 754 value per channel (a list, tuple or NumPy array) for the 755 others, as `export_params` returns. 756 757 Raises: 758 PulsePalError: If a value is invalid (nothing is sent), or the 759 device rejects a setting. 760 """ 761 names = {"sampling_rate", "output_range", *_CHANNEL_SETTINGS} 762 unknown = [name for name in params if name not in names] 763 if unknown: 764 warnings.warn(f"import_params(): skipped {', '.join(map(repr, unknown))}, which this version of " 765 "WavePalDevice does not have.", stacklevel=2) 766 new = {} 767 for name in _CHANNEL_SETTINGS: 768 if name in params: 769 n = 2 if name == "trigger_mode" else self.info.n_channels 770 new[name] = _common.one_value_per_channel(name, params[name], n) 771 if "current_waveform" in new: 772 new["current_waveform"] = self._normalize_current_waveform(new["current_waveform"]) 773 if "loop_mode" in new: 774 new["loop_mode"] = [to_bool(value, "loop_mode") for value in new["loop_mode"]] 775 if "loop_duration" in new: 776 new["loop_duration"] = self._normalize_loop_durations(new["loop_duration"]) 777 for name in ("link_trigger_channel1", "link_trigger_channel2"): 778 if name in new: 779 new[name] = [to_bool(value, name) for value in new[name]] 780 modes = list(self._trigger_mode)[1:] 781 if "trigger_mode" in new: 782 modes = [to_name(value, TRIGGER_MODES, "trigger mode") for value in new["trigger_mode"]] 783 # The trigger modes the other settings are sent in: those of channels leaving param sync mode are sent 784 # first, those of channels entering it last 785 first_modes = [new_mode if (old == "Param Sync" and new_mode != "Param Sync") else old 786 for old, new_mode in zip(self._trigger_mode[1:], modes)] 787 rate = self._sampling_rate 788 if "sampling_rate" in params: 789 rate = self._normalize_sampling_rate(params["sampling_rate"]) 790 range_name = self._output_range 791 if "output_range" in params: 792 range_name = self._check_output_range(params["output_range"], first_modes) 793 self._durations_to_samples(new.get("loop_duration", self._loop_duration[1:]), rate) # They fit at the rate 794 795 if first_modes != list(self._trigger_mode)[1:]: 796 self.trigger_mode = first_modes 797 if rate != self._sampling_rate: 798 self.sampling_rate = rate 799 if range_name != self._output_range: 800 self.output_range = range_name 801 for name in ("loop_mode", "loop_duration"): 802 if name in new: 803 setattr(self, name, new[name]) 804 if "link_trigger_channel1" in new or "link_trigger_channel2" in new: 805 self._set_trigger_links(new.get("link_trigger_channel1", self._link_trigger_channel1[1:]), 806 new.get("link_trigger_channel2", self._link_trigger_channel2[1:])) 807 if "current_waveform" in new: 808 self.current_waveform = new["current_waveform"] 809 if modes != first_modes: 810 self.trigger_mode = modes 811 812 @property 813 def waveforms(self): 814 """The waveforms loaded with `WavePalDevice.load_waveform`. 815 816 A list indexed by waveform number, 1 to `DeviceInfo.n_waveforms`, 817 with index 0 unused. Each element is a read-only NumPy array of 818 voltages, or `None` if this object has not loaded that waveform. In 819 param sync mode, it holds the waveforms loaded since the last edge 820 before the device plays them. `WavePalDevice.status` shows what 821 the device itself holds. 822 """ 823 return list(self._waveforms) 824 825 # ------------------------------------------------------------------ 826 # Waveforms and playback 827 # ------------------------------------------------------------------ 828 829 def load_waveform(self, waveform_number, waveform, channels=None): 830 """Load one of the device's waveforms, and choose the channels that play it. 831 832 The samples are written to the device's microSD card, and the 833 first `DeviceInfo.buffer_samples` of them are also kept in its RAM. 834 Outside param sync mode, loading stops the channels playing that 835 waveform, and the new one plays from their next start. Other 836 channels keep playing. In param sync mode, the new waveform waits 837 for the next rising edge on the param sync trigger channel, and 838 channels go on playing the old one until then (see 839 `WavePalDevice.trigger_mode`). 840 841 ```python 842 t = np.arange(10000) / W.sampling_rate 843 W.load_waveform(2, 3 * np.sin(2 * np.pi * 100 * t), [1, 3]) 844 ``` 845 846 Args: 847 waveform_number: The waveform to load, 1 to 848 `DeviceInfo.n_waveforms`. 849 waveform: Voltages of the samples, played at 850 `WavePalDevice.sampling_rate`. A list, tuple or NumPy 851 array of 1 to `DeviceInfo.max_samples` values, all within 852 `WavePalDevice.output_range`. 853 channels: Output channels that play this waveform, from their 854 next start: one channel number 1-4, or several as a list, 855 tuple or NumPy array. It sets their 856 `WavePalDevice.current_waveform`, and leaves the other 857 channels' alone, so channels already playing this waveform 858 play the new one. `None`, the default, changes no channel. 859 860 Raises: 861 PulsePalError: If an argument is invalid, or the device could 862 not store the waveform. The waveform is then left out of 863 `WavePalDevice.waveforms`: outside param sync mode the 864 device has unloaded it, and in param sync mode it plays the 865 version made current at the last edge. 866 """ 867 number = self._waveform_number(waveform_number) 868 channel_list = None if channels is None else _common.channel_numbers(channels) 869 samples = np.array(waveform, dtype=float).ravel() 870 if not 1 <= samples.size <= self.info.max_samples: 871 raise PulsePalError( 872 f"A waveform must have 1 to {self.info.max_samples} samples. " 873 f"Received {samples.size}." 874 ) 875 codes = self._volts_to_codes(samples) 876 self._waveforms[number] = None # Until the device has stored it. Outside param sync mode, it unloads it first. 877 self._write_command( 878 self._OP_LOAD_WAVEFORM, 879 struct.pack("<BI", number, samples.size) + codes.tobytes(), 880 ) 881 self._read_ack("load_waveform()") 882 samples.flags.writeable = False 883 self._waveforms[number] = samples 884 if channel_list: 885 values = list(self._current_waveform[1:]) 886 for channel in channel_list: 887 values[channel - 1] = number 888 self._current_waveform._assign(values) 889 890 def trigger(self, channels): 891 """Trigger output channels in software. 892 893 Idle channels start their current waveforms from the first 894 sample, together. Channels that are playing ignore it, as Pulse 895 Pal's channels ignore a soft trigger while they play a pulse train. 896 897 ```python 898 W.trigger(1) 899 W.trigger([2, 4]) # a list, tuple or NumPy array 900 ``` 901 902 Args: 903 channels: Output channel number 1-4, or several as a list, 904 tuple or NumPy array. 905 906 Raises: 907 PulsePalError: If a channel has no waveform to play: its 908 `WavePalDevice.current_waveform` is 0, or a waveform this 909 object has not loaded. Nothing is sent. 910 """ 911 channel_list = _common.channel_numbers(channels) 912 for channel in channel_list: 913 number = self._current_waveform[channel] 914 if number == 0: 915 raise PulsePalError( 916 f"Channel {channel} has no waveform to play. Set current_waveform[{channel}] to a loaded " 917 f"waveform's number, or load one with load_waveform(number, waveform, {channel})." 918 ) 919 if self._waveforms[number] is None: 920 raise PulsePalError( 921 f"Channel {channel} plays waveform {number}, which is not loaded. Load it with " 922 f"load_waveform({number}, waveform)." 923 ) 924 self._write_command(self._OP_TRIGGER, bytes([_common.channel_bits(channel_list)])) 925 926 def stop(self, channels=None): 927 """Stop playback. The stopped channels output 0 V. 928 929 Args: 930 channels: Output channel number 1-4, or several as a list, 931 tuple or NumPy array. `None` stops all channels. 932 """ 933 bits = self._ALL_CHANNELS if channels is None else _common.channel_bits(channels) 934 self._write_command(self._OP_STOP, bytes([bits])) 935 936 def set_fixed_voltage(self, channels, voltage): 937 """Set output channels to a fixed voltage. 938 939 Stops playback on those channels. The voltage holds until the 940 channel is triggered or stopped. 941 942 Args: 943 channels: Output channel number 1-4, or several as a list, 944 tuple or NumPy array. 945 voltage: Voltage, within `WavePalDevice.output_range`. 946 947 Raises: 948 PulsePalError: If the voltage is outside the output range, or 949 the device rejects the command. 950 """ 951 bits = _common.channel_bits(channels) 952 code = int(self._volts_to_codes([voltage])[0]) 953 self._write_command(self._OP_SET_FIXED_VOLTAGE, 954 struct.pack("<BH", bits, code)) 955 self._read_ack("set_fixed_voltage()") 956 957 def status(self): 958 """Read the device's playback state. 959 960 Returns: 961 A `DeviceStatus`, with the channels that are playing, the 962 number of samples in each waveform, underrun counts, the 963 longest playback interrupt, each channel's current waveform, 964 and what waits for a param sync edge. 965 """ 966 self._write_command(self._OP_GET_STATUS) 967 values = struct.unpack( 968 self._STATUS_FORMAT, 969 self._read_raw(struct.calcsize(self._STATUS_FORMAT)), 970 ) 971 playing_bits = values[0] 972 stored_load_bits = values[14] 973 return DeviceStatus( 974 playing=[ch for ch in range(1, 5) 975 if playing_bits & (1 << (ch - 1))], 976 samples_loaded=[None, *values[1:5]], 977 underruns=[None, *values[5:9]], 978 longest_interrupt_us=values[9] / 1000, 979 current_waveform=[None, *values[10:14]], 980 stored_waveforms=[n for n in range(1, 5) if stored_load_bits & (1 << (n - 1))], 981 stored_current_waveform=[None, *values[16:20]] if values[15] else None, 982 ) 983 984 def _playback_checksums(self): 985 """For testing: what each channel played since it last started. 986 987 Returns two lists indexed by channel number, with index 0 unused: 988 the number of samples played, and the sum of their DAC codes 989 modulo 2**32. `tests/wavepal_hardware_test.py` compares them with 990 the waveforms it loaded, to check every sample the device played. 991 """ 992 self._write_command(self._OP_GET_PLAYBACK_CHECKSUMS) 993 values = struct.unpack("<8I", self._read_raw(32)) 994 return [None, *values[:4]], [None, *values[4:]] 995 996 # ------------------------------------------------------------------ 997 # Connection 998 # ------------------------------------------------------------------ 999 1000 def close(self): 1001 """Close the connection to the device. 1002 1003 The device stops playback on all channels, and shows its own name 1004 on its screen again, in place of "PYTHON Connected". It keeps its 1005 settings and waveforms, so TTL triggers still play them. Safe to 1006 call more than once. Called automatically when leaving a `with` 1007 block and when the object is garbage collected, so playback also 1008 stops when the last reference to the object goes. 1009 1010 """ 1011 self._close(send_disconnect=True) 1012 1013 def _close(self, send_disconnect): 1014 """Close the connection. send_disconnect: tell the device first (op 81). False when the device may not 1015 run this firmware: op 81 means something else to other devices.""" 1016 if getattr(self, "_closed", True): 1017 return 1018 self._closed = True 1019 self._reply_pending = None # Nothing more is read, so a reply still to come needs no discarding 1020 try: 1021 if send_disconnect and self.port and self.port.is_open: 1022 self._write_command(self._OP_DISCONNECT) 1023 except Exception: 1024 pass # The port may already be gone, e.g. the cable was unplugged 1025 finally: 1026 if self.port and self.port.is_open: 1027 self.port.close() 1028 1029 def __enter__(self): 1030 """Enter a `with` block, returning the connected device.""" 1031 return self 1032 1033 def __exit__(self, exc_type, exc_value, traceback): 1034 """Close the port when leaving a `with` block. 1035 1036 Returns: 1037 `False`, so any exception raised in the block propagates. 1038 """ 1039 self.close() 1040 return False 1041 1042 def __del__(self): 1043 """Close the port when the object is collected.""" 1044 try: 1045 self.close() 1046 except Exception: 1047 # Destructors should not raise; the serial object may already be 1048 # gone during interpreter shutdown. 1049 pass 1050 1051 def __setattr__(self, name, value): 1052 """Refuse a public attribute that is not a setting, such as a misspelled one: it would program nothing (see 1053 _common.refuse_unknown_attribute()).""" 1054 if name[0] != "_": 1055 _common.refuse_unknown_attribute(self, name, _SETTING_NAMES) 1056 super().__setattr__(name, value) 1057 1058 def __repr__(self): 1059 """Describe the device and its settings, e.g. with `print(W)`.""" 1060 port = getattr(getattr(self, "port", None), "port", None) 1061 loaded = ", ".join( 1062 f"{number}: {'none' if w is None else f'{w.size} samples'}" 1063 for number, w in enumerate(self._waveforms) if number > 0 1064 ) 1065 return ( 1066 f"WavePalDevice on {port} (Wave Pal firmware " 1067 f"v{self.info.firmware_version})\n" 1068 f"sampling_rate: {self._sampling_rate} Hz\n" 1069 f"output_range: {self._output_range}\n" 1070 f"current_waveform: {list(self._current_waveform)}\n" 1071 f"loop_mode: {list(self._loop_mode)}\n" 1072 f"loop_duration: {list(self._loop_duration)}\n" 1073 f"trigger_mode: {list(self._trigger_mode)}\n" 1074 f"link_trigger_channel1: {list(self._link_trigger_channel1)}\n" 1075 f"link_trigger_channel2: {list(self._link_trigger_channel2)}\n" 1076 f"waveforms: {loaded}" 1077 ) 1078 1079 # ------------------------------------------------------------------ 1080 # Internals 1081 # ------------------------------------------------------------------ 1082 1083 def _handshake(self): 1084 """Check that the device runs a supported Wave Pal firmware.""" 1085 self._write_command(self._OP_HANDSHAKE) 1086 handshake = _common.read_handshake_reply(self.port) # Skips a reply to an earlier session's command 1087 if handshake is None: 1088 raise PulsePalError( 1089 f"No reply from the device on {self.port.port}. Is it a " 1090 "Pulse Pal 3 running Wave Pal firmware?" 1091 f" {_common.NO_REPLY_HINT}" 1092 ) 1093 reply, version = handshake 1094 if reply != self._HANDSHAKE_REPLY: 1095 if reply in _common.FIRMWARE_BY_HANDSHAKE_REPLY: 1096 raise _common.other_firmware_error(reply, version, self.port.port, "Wave Pal") 1097 raise PulsePalError( 1098 "Incorrect handshake returned. Expected " 1099 f"{self._HANDSHAKE_REPLY}, received {reply}." 1100 ) 1101 if version > self._CURRENT_FIRMWARE_VERSION: 1102 # New firmware only adds commands (see PROTOCOL.md), so this class still works with it 1103 warnings.warn( 1104 f"Wave Pal firmware v{version} is newer than this pulsepal package knows " 1105 f"(v{self._CURRENT_FIRMWARE_VERSION}). It works with it, but update the package to use what is " 1106 "new.", stacklevel=3) 1107 self.info.firmware_version = version 1108 1109 def _read_hardware_info(self): 1110 self._write_command(self._OP_HARDWARE_INFO) 1111 ( 1112 self.info.hardware_version, 1113 self.info.n_channels, 1114 self.info.max_samples, 1115 self.info.max_sampling_rate, 1116 self.info.buffer_samples, 1117 self.info.sample_clock_hz, 1118 self.info.n_waveforms, 1119 ) = struct.unpack( 1120 self._HARDWARE_INFO_FORMAT, 1121 self._read_raw(struct.calcsize(self._HARDWARE_INFO_FORMAT)), 1122 ) 1123 self._write_command(self._OP_HARDWARE_REVISION) 1124 self.info.circuit_board_revision = self._read_raw(1)[0] 1125 self._write_command(_common.OP_SERIAL_NUMBER) 1126 self.info.serial_number = _common.read_serial_number(self._read_raw) 1127 1128 def _apply_current_waveform(self, values): 1129 waveform_numbers = self._normalize_current_waveform(values) 1130 self._write_command(self._OP_SET_CURRENT_WAVEFORMS, bytes(waveform_numbers)) 1131 self._read_ack("setting current_waveform") 1132 return waveform_numbers 1133 1134 def _normalize_current_waveform(self, values): 1135 """Check new current waveforms for all channels, and return them as the list holds them.""" 1136 waveform_numbers = [] 1137 for channel, value in enumerate(values, start=1): 1138 if (isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Integral) 1139 or not 0 <= value <= self.info.n_waveforms): 1140 raise PulsePalError( 1141 f"current_waveform values are waveform numbers, 1-{self.info.n_waveforms}, or 0 for none. " 1142 f"Received {value!r}." 1143 ) 1144 value = int(value) 1145 # A channel left on a waveform whose load failed is not refused here: trigger() refuses it 1146 if value != 0 and value != self._current_waveform[channel] and self._waveforms[value] is None: 1147 raise PulsePalError( 1148 f"Waveform {value} is not loaded. Load it with load_waveform({value}, waveform) first." 1149 ) 1150 waveform_numbers.append(value) 1151 return waveform_numbers 1152 1153 def _apply_loop_mode(self, values): 1154 modes = [to_bool(value, "loop_mode") for value in values] 1155 self._write_command(self._OP_SET_LOOP_MODE, bytes(map(int, modes))) 1156 self._read_ack("setting loop_mode") 1157 return modes 1158 1159 def _apply_loop_duration(self, values): 1160 durations = self._normalize_loop_durations(values) 1161 samples = self._durations_to_samples(durations, self._sampling_rate) 1162 self._send_loop_duration_samples(samples) 1163 return durations 1164 1165 def _normalize_loop_durations(self, values): 1166 """Check new loop durations for all channels, and return them as the list holds them, in seconds.""" 1167 durations = [] 1168 for value in values: 1169 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real): 1170 raise PulsePalError( 1171 f"loop_duration must be in seconds. Received {value!r}." 1172 ) 1173 duration = float(value) 1174 if not math.isfinite(duration) or duration < 0: 1175 raise PulsePalError( 1176 "loop_duration must be 0 (loop until stopped) or a " 1177 f"positive number of seconds. Received {value!r}." 1178 ) 1179 durations.append(duration) 1180 return durations 1181 1182 def _send_loop_duration_samples(self, samples): 1183 self._write_command(self._OP_SET_LOOP_DURATION, 1184 struct.pack("<4I", *samples)) 1185 self._read_ack("setting loop_duration") 1186 1187 def _durations_to_samples(self, durations, rate_hz): 1188 """Convert loop durations in seconds to samples at a sampling rate.""" 1189 actual_rate = self._actual_rate(rate_hz) 1190 samples = [] 1191 for duration in durations: 1192 n = round(duration * actual_rate) 1193 if duration > 0: 1194 n = max(n, 1) # 0 samples would mean "loop until stopped" 1195 if n > self._UINT32_MAX: 1196 raise PulsePalError( 1197 f"A loop_duration of {duration} s is too long at " 1198 f"{rate_hz} Hz. The longest is " 1199 f"{self._UINT32_MAX / actual_rate:.0f} s." 1200 ) 1201 samples.append(n) 1202 return samples 1203 1204 def _apply_trigger_mode(self, values): 1205 names = [to_name(value, TRIGGER_MODES, "trigger mode") for value in values] 1206 self._write_command( 1207 self._OP_SET_TRIGGER_MODE, 1208 bytes(_TRIGGER_MODE_CODES[name] for name in names), 1209 ) 1210 self._read_ack("setting trigger_mode") 1211 return names 1212 1213 # One op programs both trigger channels' links, so each list is sent 1214 # with the other's current values 1215 def _apply_trigger_channel1_links(self, values): 1216 links1 = [to_bool(v, "link_trigger_channel1") for v in values] 1217 self._send_trigger_links(links1, self._link_trigger_channel2[1:]) 1218 return links1 1219 1220 def _apply_trigger_channel2_links(self, values): 1221 links2 = [to_bool(v, "link_trigger_channel2") for v in values] 1222 self._send_trigger_links(self._link_trigger_channel1[1:], links2) 1223 return links2 1224 1225 def _set_trigger_links(self, links1, links2): 1226 """Program both trigger channels' links with one command.""" 1227 self._send_trigger_links(links1, links2) 1228 self._link_trigger_channel1._store(links1) 1229 self._link_trigger_channel2._store(links2) 1230 1231 def _send_trigger_links(self, links1, links2): 1232 self._write_command(self._OP_SET_TRIGGER_LINKS, 1233 bytes([*map(int, links1), *map(int, links2)])) 1234 self._read_ack("setting the trigger channel links") 1235 1236 def _actual_rate(self, rate_hz): 1237 # The sample period is the nearest whole number of clock cycles, computed in integers as the 1238 # firmware's setSamplingRate() does. A rate halfway between two (e.g. 3072 Hz: 7812.5 cycles) 1239 # rounds up there, where round() would round to even. 1240 clock = self.info.sample_clock_hz 1241 return clock / ((clock + rate_hz // 2) // rate_hz) 1242 1243 def _normalize_sampling_rate(self, rate): 1244 """Check a sampling rate, and return it as a whole number of Hz.""" 1245 try: 1246 rate_hz = int(rate) 1247 is_whole = rate_hz == rate and not isinstance(rate, (bool, str)) 1248 except (TypeError, ValueError, OverflowError): 1249 is_whole = False 1250 if not is_whole or not 1 <= rate_hz <= self.info.max_sampling_rate: 1251 raise PulsePalError( 1252 "sampling_rate must be a whole number of Hz from 1 to " 1253 f"{self.info.max_sampling_rate}. Received {rate!r}." 1254 ) 1255 return rate_hz 1256 1257 def _check_output_range(self, range_name, trigger_modes): 1258 """Check that the output range can be set, with these trigger modes, and return its canonical name.""" 1259 name = self._range_name(range_name) 1260 low, high = OUTPUT_RANGES[name] 1261 for number, waveform in enumerate(self._waveforms): 1262 if waveform is not None and ( 1263 waveform.min() < low or waveform.max() > high): 1264 raise PulsePalError( 1265 f"Waveform {number} spans " 1266 f"{waveform.min():g} V to {waveform.max():g} V, which " 1267 f"does not fit the {name} range. Load a new waveform " 1268 "first, or choose a wider range." 1269 ) 1270 if name != self._output_range and "Param Sync" in trigger_modes: 1271 raise PulsePalError( 1272 "output_range cannot change while a trigger channel is in param sync mode: the waveforms are " 1273 "loaded again for the new range, and in param sync mode they would wait for a TTL edge. Set " 1274 "trigger_mode to another mode first." 1275 ) 1276 return name 1277 1278 def _range_name(self, range_name): 1279 """Return the canonical name of an output range.""" 1280 if isinstance(range_name, str): 1281 for name in OUTPUT_RANGES: 1282 if name.lower() == range_name.replace(" ", "").lower(): 1283 return name 1284 raise PulsePalError( 1285 f"Unknown output range: {range_name!r}. Valid ranges are " 1286 f"{', '.join(OUTPUT_RANGES)}." 1287 ) 1288 1289 def _volts_to_codes(self, volts): 1290 """Convert voltages to DAC codes in the current output range.""" 1291 low, high = OUTPUT_RANGES[self._output_range] 1292 volts = np.asarray(volts, dtype=float) 1293 if not np.all(np.isfinite(volts)): 1294 raise PulsePalError("Voltages must be finite numbers.") 1295 if volts.min() < low or volts.max() > high: 1296 raise PulsePalError( 1297 f"Voltages must be within the output range, {low:g} V to " 1298 f"{high:g} V. Received {volts.min():g} V to " 1299 f"{volts.max():g} V. Change output_range to use a wider " 1300 "range." 1301 ) 1302 codes = np.round((volts - low) / (high - low) * self._DAC_BITMAX) 1303 return codes.astype("<u2") 1304 1305 def _waveform_number(self, number): 1306 if ( 1307 not isinstance(number, numbers.Integral) 1308 or isinstance(number, (bool, np.bool_)) 1309 or not 1 <= number <= self.info.n_waveforms 1310 ): 1311 raise PulsePalError( 1312 f"Waveforms are numbered 1-{self.info.n_waveforms}. Received {number!r}." 1313 ) 1314 return int(number) 1315 1316 def _write_command(self, op_code, data=b""): 1317 """Send one command, with its framing byte, in a single write. 1318 1319 A command whose reply was not read in full, because the read timed out or was interrupted (Ctrl-C), leaves 1320 that reply, or the rest of it, to arrive later. Read as the reply to the next command, it would put every 1321 reply after it out of step. So it is discarded first (_discard_lost_reply()), and this command's reply, if it 1322 has one, is pending until _read_raw() has read it, as in PulsePalDevice. 1323 """ 1324 if self._reply_pending is not None: 1325 self._discard_lost_reply() 1326 if op_code not in _OPS_WITHOUT_TRACKED_REPLY: 1327 self._reply_pending = op_code # Before the write: a Ctrl-C can come as soon as it returns 1328 message = bytes([self._OP_MENU_BYTE, op_code]) + data 1329 bytes_written = self.port.write(message) 1330 if bytes_written != len(message): 1331 raise PulsePalError( 1332 f"Wrote {bytes_written} byte(s), expected to write " 1333 f"{len(message)} byte(s)." 1334 ) 1335 1336 def _discard_lost_reply(self): 1337 """Discard what arrives of a reply that was not read in full (see _write_command()), and bring the device 1338 back in step with this object. 1339 1340 The command may have changed a setting, unknown to this object: an assignment interrupted after its write, 1341 for one, which the device took and this object did not. Wave Pal cannot report its settings, so the setting 1342 such a command carried is sent again as this object holds it. A change of output range unloads the device's 1343 waveforms, so after one they are loaded again too. A waveform whose load was lost is already left out of 1344 `waveforms`. 1345 """ 1346 op = self._reply_pending 1347 self._reply_pending = None 1348 n_late = len(_common.read_until_quiet(self.port)) 1349 send_again = op in self._SETTING_OPS 1350 _common.warn_lost_reply(f"op {chr(op)!r}", n_late, 1351 ", and the setting it carried was sent again as this object holds it" if send_again 1352 else "") 1353 if send_again: 1354 try: 1355 self._send_setting_again(op) 1356 except PulsePalError as exc: 1357 raise PulsePalError(f"After the reply to an earlier command was lost: {exc}") from exc 1358 1359 def _send_setting_again(self, op): 1360 """Send the setting that op carries, as this object holds it (see _discard_lost_reply()).""" 1361 if op == self._OP_SET_SAMPLING_RATE: 1362 self._write_command(op, struct.pack("<I", self._sampling_rate)) 1363 self._read_ack("setting sampling_rate") 1364 elif op == self._OP_SET_OUTPUT_RANGE: 1365 self._write_command(op, bytes([list(OUTPUT_RANGES).index(self._output_range)])) 1366 self._read_ack("setting output_range") 1367 waveforms = self._waveforms 1368 self._waveforms = [None] * len(waveforms) 1369 for number, waveform in enumerate(waveforms): 1370 if waveform is not None: 1371 self.load_waveform(number, waveform) 1372 elif op == self._OP_SET_CURRENT_WAVEFORMS: 1373 self._write_command(op, bytes(self._current_waveform[1:])) 1374 self._read_ack("setting current_waveform") 1375 elif op == self._OP_SET_LOOP_MODE: 1376 self._write_command(op, bytes(map(int, self._loop_mode[1:]))) 1377 self._read_ack("setting loop_mode") 1378 elif op == self._OP_SET_LOOP_DURATION: 1379 self._send_loop_duration_samples(self._durations_to_samples(self._loop_duration[1:], self._sampling_rate)) 1380 elif op == self._OP_SET_TRIGGER_MODE: 1381 self._write_command(op, bytes(_TRIGGER_MODE_CODES[name] for name in self._trigger_mode[1:])) 1382 self._read_ack("setting trigger_mode") 1383 else: # Trigger links 1384 self._send_trigger_links(self._link_trigger_channel1[1:], self._link_trigger_channel2[1:]) 1385 1386 def _read_raw(self, n_bytes): 1387 """Read exactly n_bytes from the serial port: the whole reply to the command last sent, which is then no 1388 longer pending (see _write_command()). Only connecting reads a reply in more than one call.""" 1389 message = self.port.read(n_bytes) 1390 if len(message) < n_bytes: 1391 raise PulsePalError( 1392 f"Wave Pal sent {len(message)} of the {n_bytes} byte(s) of its reply within " 1393 f"{self.port.timeout} s. {_common.NO_REPLY_HINT}" 1394 ) 1395 self._reply_pending = None 1396 return message 1397 1398 def _read_ack(self, context): 1399 """Read a one-byte confirmation: 1 if the device executed the 1400 command, 0 if it rejected it.""" 1401 try: 1402 reply = self._read_raw(1)[0] 1403 except PulsePalError as exc: 1404 raise PulsePalError( 1405 f"Wave Pal did not confirm {context}: no reply arrived within {self.port.timeout} s. " 1406 f"{_common.NO_REPLY_HINT}" 1407 ) from exc 1408 if reply != 1: 1409 raise PulsePalError( 1410 f"Wave Pal rejected {context}. A value was out of range, " 1411 "or a waveform could not be written to the microSD card." 1412 )
A class to control a Wave Pal on a USB serial port.
Creating an instance opens the serial port, checks that the device
runs Wave Pal firmware, reads its properties into
WavePalDevice.info, shows "PYTHON Connected" on the device's
screen, stops any playback and programs the default settings (see
WavePalDevice.set_default_params).
from pulsepal import WavePalDevice
W = WavePalDevice("COM3")
W.load_waveform(1, [0, 1, 2, 3, 4, 5, 0], 1) # waveform 1, played by channel 1
W.trigger(1)
W.close()
Replace "COM3" with the device's USB serial port name, which
WavePalDevice.serialportlist lists. WavePalDevice is also a
context manager, which closes the port on exit.
Closing the connection stops playback, as it does on Pulse Pal, and puts the device's own name back on its screen. The device keeps its settings and waveforms, so TTL triggers still play them.
332 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 333 """Open a connection to a Wave Pal. 334 335 Args: 336 port_name: USB serial port of the device, such as `COM3` on 337 Windows or `/dev/ttyACM0` on Linux. 338 serial_number: Instead of `port_name`, the device's USB serial 339 number (`DeviceInfo.serial_number`), which finds its port 340 wherever it is plugged in. 341 timeout: Serial read timeout, in seconds. Loading a long 342 waveform onto a slow microSD card can take a few seconds. 343 344 Firmware newer than this module knows is used with a warning: new 345 firmware only adds commands. Update the package to use what is new. 346 347 Raises: 348 PulsePalError: If the device does not reply to the handshake, or 349 runs other firmware. 350 serial.SerialException: If the serial port cannot be opened, 351 e.g. because another program has it open (on Linux and 352 macOS, another session of these classes, which lock it). 353 """ 354 self._closed = True 355 self.info = DeviceInfo() 356 self._sampling_rate = None 357 self._output_range = None 358 self._waveforms = [] # Indexed by waveform number, once the device has said how many it stores 359 self._current_waveform = ChannelSettings( 360 "current_waveform", [0] * 4, self, "_apply_current_waveform") 361 self._loop_mode = ChannelSettings( 362 "loop_mode", [False] * 4, self, "_apply_loop_mode") 363 self._loop_duration = ChannelSettings( 364 "loop_duration", [0.0] * 4, self, "_apply_loop_duration") 365 self._trigger_mode = ChannelSettings( 366 "trigger_mode", ["Normal"] * 2, self, "_apply_trigger_mode") 367 self._link_trigger_channel1 = ChannelSettings( 368 "link_trigger_channel1", [False] * 4, self, 369 "_apply_trigger_channel1_links") 370 self._link_trigger_channel2 = ChannelSettings( 371 "link_trigger_channel2", [False] * 4, self, 372 "_apply_trigger_channel2_links") 373 374 port_name = _common.port_name_from(port_name, serial_number, "WavePalDevice") 375 self.port = serial.Serial( 376 port_name, 377 self._BAUD_RATE, 378 timeout=timeout, 379 rtscts=True, 380 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 381 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 382 # it, such as another of these classes, gets an error instead 383 exclusive=True, 384 ) 385 self._closed = False 386 try: 387 # Discard anything left in the buffer by an earlier session 388 self.port.reset_input_buffer() 389 self._handshake() 390 self._read_hardware_info() 391 self._waveforms = [None] * (self.info.n_waveforms + 1) 392 # Client name op + "PYTHON" in ASCII, shown as "PYTHON Connected" 393 self._write_command(self._OP_SET_CLIENT_NAME, b"PYTHON") 394 self.stop() 395 self.set_default_params() 396 except BaseException: 397 # Op 81 means something else to other devices, so it is sent 398 # only once the device has identified itself as a Wave Pal 399 self._close(send_disconnect=self.info.firmware_version is not None) 400 raise
Open a connection to a Wave Pal.
Arguments:
- port_name: USB serial port of the device, such as
COM3on Windows or/dev/ttyACM0on Linux. - serial_number: Instead of
port_name, the device's USB serial number (DeviceInfo.serial_number), which finds its port wherever it is plugged in. - timeout: Serial read timeout, in seconds. Loading a long waveform onto a slow microSD card can take a few seconds.
Firmware newer than this module knows is used with a warning: new firmware only adds commands. Update the package to use what is new.
Raises:
- PulsePalError: If the device does not reply to the handshake, or runs other firmware.
- serial.SerialException: If the serial port cannot be opened, e.g. because another program has it open (on Linux and macOS, another session of these classes, which lock it).
402 @staticmethod 403 def serialportlist(ports_to_list="available"): 404 """Return the names of the USB serial ports on this computer. 405 406 Called on the class, without connecting to a device, to find the 407 port name to pass to `WavePalDevice`. 408 409 Args: 410 ports_to_list: `available` to list only the ports that are 411 not already in use, or `all` to list every USB serial 412 port. Not case sensitive. 413 414 Returns: 415 Sorted list of port names, such as `["COM3", "COM7"]`. 416 417 Raises: 418 PulsePalError: If `ports_to_list` is not `available` or `all`. 419 """ 420 return _common.serialportlist(ports_to_list)
Return the names of the USB serial ports on this computer.
Called on the class, without connecting to a device, to find the
port name to pass to WavePalDevice.
Arguments:
- ports_to_list:
availableto list only the ports that are not already in use, orallto list every USB serial port. Not case sensitive.
Returns:
Sorted list of port names, such as
["COM3", "COM7"].
Raises:
- PulsePalError: If
ports_to_listis notavailableorall.
426 def set_default_params(self): 427 """Program the default settings on the device. 428 429 The defaults are both trigger channels in normal mode, a 10 kHz 430 sampling rate, the -10 V to 10 V output range, loop mode off with 431 loop durations of 0, no output channel linked to a trigger channel 432 (so a TTL starts nothing until a script links it), and no current 433 waveform on any channel. They match the settings the device starts with. The 434 trigger modes are sent first: a trigger channel leaving param sync 435 mode makes what was stored for its next edge current, and the 436 settings after it then apply at once. 437 438 Loaded waveforms are kept, and loaded again if the output range 439 changes (see `WavePalDevice.output_range`). 440 441 Raises: 442 PulsePalError: If a loaded waveform does not fit the default 443 output range, -10 V to 10 V. 444 """ 445 self.trigger_mode = ["Normal"] * 2 446 self.sampling_rate = 10000 447 self.output_range = "-10V:10V" 448 self.loop_mode = [False] * 4 449 self.loop_duration = [0] * 4 450 self._set_trigger_links([False] * 4, [False] * 4) 451 self.current_waveform = [0] * 4
Program the default settings on the device.
The defaults are both trigger channels in normal mode, a 10 kHz sampling rate, the -10 V to 10 V output range, loop mode off with loop durations of 0, no output channel linked to a trigger channel (so a TTL starts nothing until a script links it), and no current waveform on any channel. They match the settings the device starts with. The trigger modes are sent first: a trigger channel leaving param sync mode makes what was stored for its next edge current, and the settings after it then apply at once.
Loaded waveforms are kept, and loaded again if the output range
changes (see WavePalDevice.output_range).
Raises:
- PulsePalError: If a loaded waveform does not fit the default output range, -10 V to 10 V.
453 @property 454 def sampling_rate(self): 455 """Sampling rate of all output channels, in Hz. 456 457 A whole number from 1 to `DeviceInfo.max_sampling_rate`. It can be 458 changed during playback. The rate played can differ slightly from 459 the rate set: see `WavePalDevice.actual_sampling_rate`. 460 """ 461 return self._sampling_rate
Sampling rate of all output channels, in Hz.
A whole number from 1 to DeviceInfo.max_sampling_rate. It can be
changed during playback. The rate played can differ slightly from
the rate set: see WavePalDevice.actual_sampling_rate.
479 @property 480 def actual_sampling_rate(self): 481 """The sampling rate the device plays, in Hz. 482 483 The device divides `DeviceInfo.sample_clock_hz` (24 MHz) by a 484 whole number, so the rate played is the nearest one of those to 485 `WavePalDevice.sampling_rate`. For example, 44100 Hz plays at 486 44117.6 Hz. Rates that divide 24 MHz exactly, such as 10 kHz, 487 25 kHz or 100 kHz, play exactly. 488 """ 489 return self._actual_rate(self._sampling_rate)
The sampling rate the device plays, in Hz.
The device divides DeviceInfo.sample_clock_hz (24 MHz) by a
whole number, so the rate played is the nearest one of those to
WavePalDevice.sampling_rate. For example, 44100 Hz plays at
44117.6 Hz. Rates that divide 24 MHz exactly, such as 10 kHz,
25 kHz or 100 kHz, play exactly.
491 @property 492 def output_range(self): 493 """Voltage range of all output channels. 494 495 One of `DeviceInfo.output_ranges`: `"0V:5V"`, `"0V:10V"`, 496 `"-5V:5V"` or `"-10V:10V"`. The smallest range that fits the 497 waveforms gives the finest voltage steps. 498 499 Changing the range stops playback, sets all outputs to 0 V, and 500 loads the waveforms again, encoded for the new range. It raises 501 an error, and changes nothing, if a loaded waveform does not fit 502 the new range, or if a trigger channel is in param sync mode, 503 where the loads would wait for a TTL edge. 504 """ 505 return self._output_range
Voltage range of all output channels.
One of DeviceInfo.output_ranges: "0V:5V", "0V:10V",
"-5V:5V" or "-10V:10V". The smallest range that fits the
waveforms gives the finest voltage steps.
Changing the range stops playback, sets all outputs to 0 V, and loads the waveforms again, encoded for the new range. It raises an error, and changes nothing, if a loaded waveform does not fit the new range, or if a trigger channel is in param sync mode, where the loads would wait for a TTL edge.
525 @property 526 def current_waveform(self): 527 """The waveform each output channel plays, by waveform number. 528 529 Indexed by channel number (see "Channel settings" above). Each 530 value is the number of a waveform loaded with 531 `WavePalDevice.load_waveform`, 1 to `DeviceInfo.n_waveforms`, or 532 `0` for none, the default: a channel with no waveform ignores 533 triggers. Several channels can play the same waveform. 534 535 A channel that is playing finishes its waveform: the new one plays 536 from the channel's next start. In param sync mode, a change waits 537 for the next rising edge on the param sync trigger channel (see 538 `WavePalDevice.trigger_mode`). 539 540 ```python 541 W.current_waveform[1] = 2 # channel 1 plays waveform 2 542 W.current_waveform = [1, 1, 1, 1] # all four play waveform 1 543 ``` 544 """ 545 return self._current_waveform
The waveform each output channel plays, by waveform number.
Indexed by channel number (see "Channel settings" above). Each
value is the number of a waveform loaded with
WavePalDevice.load_waveform, 1 to DeviceInfo.n_waveforms, or
0 for none, the default: a channel with no waveform ignores
triggers. Several channels can play the same waveform.
A channel that is playing finishes its waveform: the new one plays
from the channel's next start. In param sync mode, a change waits
for the next rising edge on the param sync trigger channel (see
WavePalDevice.trigger_mode).
W.current_waveform[1] = 2 # channel 1 plays waveform 2
W.current_waveform = [1, 1, 1, 1] # all four play waveform 1
551 @property 552 def loop_mode(self): 553 """Whether each output channel loops its waveform. 554 555 Indexed by channel number (see "Channel settings" above). `True` 556 repeats the waveform until `WavePalDevice.loop_duration` has 557 elapsed, or until stopped if the loop duration is 0. `False` plays 558 it once. A change applies to playback in progress. 559 """ 560 return self._loop_mode
Whether each output channel loops its waveform.
Indexed by channel number (see "Channel settings" above). True
repeats the waveform until WavePalDevice.loop_duration has
elapsed, or until stopped if the loop duration is 0. False plays
it once. A change applies to playback in progress.
566 @property 567 def loop_duration(self): 568 """How long each output channel loops its waveform, in seconds. 569 570 Indexed by channel number (see "Channel settings" above). Applies 571 in loop mode only, and counts from the trigger. The channel stops 572 when it has elapsed, part way through the waveform if need be. `0` 573 loops until the channel is stopped. Durations are rounded to a 574 whole number of samples, and a nonzero duration lasts at least 575 one sample. 576 """ 577 return self._loop_duration
How long each output channel loops its waveform, in seconds.
Indexed by channel number (see "Channel settings" above). Applies
in loop mode only, and counts from the trigger. The channel stops
when it has elapsed, part way through the waveform if need be. 0
loops until the channel is stopped. Durations are rounded to a
whole number of samples, and a nonzero duration lasts at least
one sample.
583 @property 584 def trigger_mode(self): 585 """How each trigger channel acts on the output channels linked to it. 586 587 Indexed by trigger channel number, 1 or 2 (index 0 is unused), as 588 in Pulse Pal. A TTL edge on a trigger channel acts on the output 589 channels linked to it (`WavePalDevice.link_trigger_channel1`, 590 `WavePalDevice.link_trigger_channel2`): 591 592 - `"Normal"`: a rising edge starts the linked channels' waveforms. 593 Channels that are playing ignore it. 594 - `"Toggle"`: a rising edge starts the linked channels, or stops 595 those that are playing. 596 - `"Gated"`: a rising edge starts the linked channels, and a 597 falling edge stops them, unless the other trigger channel is also 598 gated, linked to them, and still high. With loop mode on and a 599 loop duration of 0, a waveform plays for exactly as long as the 600 TTL is high. 601 - `"Param Sync"`: a rising edge starts and stops nothing (the 602 channel's links are ignored). It makes the waveforms loaded, 603 and the `WavePalDevice.current_waveform` set, since the previous 604 edge current. This is how the next trial's waveforms are loaded 605 during the current trial, and played from the instant it starts. 606 - `"Master"`: a rising edge starts the linked channels, and 607 restarts those that are playing from their first sample. 608 609 The first four are Pulse Pal's trigger modes. Names are not case 610 sensitive. 611 612 While a trigger channel is in param sync mode, 613 `WavePalDevice.load_waveform` loads into a second memory block 614 that the device keeps for each waveform. Channels go on playing 615 the waveform it replaces, and triggers still start it, until the 616 edge. A channel that is playing at the edge finishes what it plays, 617 and plays the new waveform from its next start. A later load of the 618 same waveform before the edge replaces the earlier one, and stops 619 any channel still playing the waveform from before the previous 620 edge, since the device reuses that memory block. Changes to 621 `WavePalDevice.current_waveform` wait for the edge in the same 622 way. Other settings apply at once. 623 624 When neither trigger channel is in param sync mode any more, what 625 was waiting for an edge becomes current at once, so 626 `WavePalDevice.waveforms` and `WavePalDevice.current_waveform` 627 describe the device again. (Pulse Pal and Synth Pal discard their 628 stored parameters and settings then; Pulse Pal makes its stored 629 custom trains current, as this class does its waveforms.) 630 `WavePalDevice.output_range` cannot change 631 in param sync mode. To make the waveforms current on the same edge 632 that starts the channels, wire the TTL to the other trigger channel 633 as well: the param sync edge is handled first. 634 635 ```python 636 W.trigger_mode[2] = "Param Sync" # sent at once 637 W.load_waveform(1, next_trial_wave) # waits for trigger channel 2's next rising edge 638 ``` 639 """ 640 return self._trigger_mode
How each trigger channel acts on the output channels linked to it.
Indexed by trigger channel number, 1 or 2 (index 0 is unused), as
in Pulse Pal. A TTL edge on a trigger channel acts on the output
channels linked to it (WavePalDevice.link_trigger_channel1,
WavePalDevice.link_trigger_channel2):
"Normal": a rising edge starts the linked channels' waveforms. Channels that are playing ignore it."Toggle": a rising edge starts the linked channels, or stops those that are playing."Gated": a rising edge starts the linked channels, and a falling edge stops them, unless the other trigger channel is also gated, linked to them, and still high. With loop mode on and a loop duration of 0, a waveform plays for exactly as long as the TTL is high."Param Sync": a rising edge starts and stops nothing (the channel's links are ignored). It makes the waveforms loaded, and theWavePalDevice.current_waveformset, since the previous edge current. This is how the next trial's waveforms are loaded during the current trial, and played from the instant it starts."Master": a rising edge starts the linked channels, and restarts those that are playing from their first sample.
The first four are Pulse Pal's trigger modes. Names are not case sensitive.
While a trigger channel is in param sync mode,
WavePalDevice.load_waveform loads into a second memory block
that the device keeps for each waveform. Channels go on playing
the waveform it replaces, and triggers still start it, until the
edge. A channel that is playing at the edge finishes what it plays,
and plays the new waveform from its next start. A later load of the
same waveform before the edge replaces the earlier one, and stops
any channel still playing the waveform from before the previous
edge, since the device reuses that memory block. Changes to
WavePalDevice.current_waveform wait for the edge in the same
way. Other settings apply at once.
When neither trigger channel is in param sync mode any more, what
was waiting for an edge becomes current at once, so
WavePalDevice.waveforms and WavePalDevice.current_waveform
describe the device again. (Pulse Pal and Synth Pal discard their
stored parameters and settings then; Pulse Pal makes its stored
custom trains current, as this class does its waveforms.)
WavePalDevice.output_range cannot change
in param sync mode. To make the waveforms current on the same edge
that starts the channels, wire the TTL to the other trigger channel
as well: the param sync edge is handled first.
W.trigger_mode[2] = "Param Sync" # sent at once
W.load_waveform(1, next_trial_wave) # waits for trigger channel 2's next rising edge
646 @property 647 def link_trigger_channel1(self): 648 """Whether each output channel is triggered by trigger channel 1. 649 650 Indexed by channel number (see "Channel settings" above). By 651 default no output channel is linked to either trigger channel, so 652 a TTL starts nothing until a script links it. 653 """ 654 return self._link_trigger_channel1
Whether each output channel is triggered by trigger channel 1.
Indexed by channel number (see "Channel settings" above). By default no output channel is linked to either trigger channel, so a TTL starts nothing until a script links it.
660 @property 661 def link_trigger_channel2(self): 662 """Whether each output channel is triggered by trigger channel 2. 663 664 Indexed by channel number (see "Channel settings" above). 665 """ 666 return self._link_trigger_channel2
Whether each output channel is triggered by trigger channel 2.
Indexed by channel number (see "Channel settings" above).
672 def set_screen_saver(self, enabled, timeout=1800): 673 """Switch the device's screen saver on or off, and set its timeout. 674 675 With the screen saver on, the device dims its screen once it has 676 been left alone for `timeout` seconds: no command from a computer, 677 no rising edge on a trigger channel, and no joystick click or 678 push. The next of these brings the screen back. The device keeps 679 both settings in its EEPROM, shared with Pulse Pal and Synth Pal 680 firmware, and saves them once no channel is playing. A new device 681 starts with the screen saver on and 1800 s. 682 683 Args: 684 enabled: `True` to switch the screen saver on, `False` to 685 switch it off. 686 timeout: Seconds without activity before the screen dims, 1 to 687 65535. It is sent with every call, so leaving it out sets 688 1800 s. 689 690 Raises: 691 PulsePalError: If a value is out of range. 692 """ 693 state = to_bool(enabled, "enabled") 694 if isinstance(timeout, (bool, np.bool_)) \ 695 or not isinstance(timeout, numbers.Integral) \ 696 or not 1 <= timeout <= 65535: 697 raise PulsePalError( 698 "timeout must be a whole number of seconds from 1 to 65535. " 699 f"Received {timeout!r}." 700 ) 701 self._write_command(self._OP_SET_SCREEN_SAVER, 702 struct.pack("<BH", int(state), int(timeout))) 703 self._read_ack("set_screen_saver()")
Switch the device's screen saver on or off, and set its timeout.
With the screen saver on, the device dims its screen once it has
been left alone for timeout seconds: no command from a computer,
no rising edge on a trigger channel, and no joystick click or
push. The next of these brings the screen back. The device keeps
both settings in its EEPROM, shared with Pulse Pal and Synth Pal
firmware, and saves them once no channel is playing. A new device
starts with the screen saver on and 1800 s.
Arguments:
- enabled:
Trueto switch the screen saver on,Falseto switch it off. - timeout: Seconds without activity before the screen dims, 1 to
- It is sent with every call, so leaving it out sets 1800 s.
Raises:
- PulsePalError: If a value is out of range.
705 def export_params(self): 706 """Return every setting, as a dict of plain values and lists. 707 708 Keyed by setting name: `"sampling_rate"` and `"output_range"`, then 709 the channel settings, each with one value per channel and no 710 unused index 0. It holds only numbers, booleans and names, so it 711 can be saved with `json` and logged with your data. The waveforms 712 are not part of it: `WavePalDevice.waveforms` holds them. 713 714 ```python 715 import json 716 with open("session_settings.json", "w") as f: 717 json.dump(W.export_params(), f) 718 ``` 719 """ 720 params = {"sampling_rate": self._sampling_rate, "output_range": self._output_range} 721 for name in _CHANNEL_SETTINGS: 722 params[name] = list(getattr(self, name))[1:] 723 return params
Return every setting, as a dict of plain values and lists.
Keyed by setting name: "sampling_rate" and "output_range", then
the channel settings, each with one value per channel and no
unused index 0. It holds only numbers, booleans and names, so it
can be saved with json and logged with your data. The waveforms
are not part of it: WavePalDevice.waveforms holds them.
import json
with open("session_settings.json", "w") as f:
json.dump(W.export_params(), f)
725 def import_params(self, params): 726 """Program the device with settings exported by `WavePalDevice.export_params`. 727 728 All of them are checked first, and nothing is sent if one is 729 invalid. They are then sent in an order that lets each take 730 effect: a trigger channel leaving param sync mode first, then the 731 sampling rate, the output range and the channel settings, and a 732 trigger channel entering param sync mode last. So 733 `WavePalDevice.current_waveform` applies at once, unless a trigger 734 channel was in param sync mode and stays in it: then it waits for 735 the next edge, as an assignment would. Settings missing from 736 `params` keep their values. A name that is not a setting is 737 skipped with a warning, so that settings exported by a newer 738 version of this package, which may have more, can still be 739 imported. 740 741 The waveforms are not settings: load them with 742 `WavePalDevice.load_waveform` first, since a current waveform must 743 be a loaded one. 744 745 ```python 746 import json 747 with open("session_settings.json") as f: 748 W.import_params(json.load(f)) 749 ``` 750 751 Args: 752 params: A dict of setting name to its value: a number for 753 `"sampling_rate"`, a name for `"output_range"`, and one 754 value per channel (a list, tuple or NumPy array) for the 755 others, as `export_params` returns. 756 757 Raises: 758 PulsePalError: If a value is invalid (nothing is sent), or the 759 device rejects a setting. 760 """ 761 names = {"sampling_rate", "output_range", *_CHANNEL_SETTINGS} 762 unknown = [name for name in params if name not in names] 763 if unknown: 764 warnings.warn(f"import_params(): skipped {', '.join(map(repr, unknown))}, which this version of " 765 "WavePalDevice does not have.", stacklevel=2) 766 new = {} 767 for name in _CHANNEL_SETTINGS: 768 if name in params: 769 n = 2 if name == "trigger_mode" else self.info.n_channels 770 new[name] = _common.one_value_per_channel(name, params[name], n) 771 if "current_waveform" in new: 772 new["current_waveform"] = self._normalize_current_waveform(new["current_waveform"]) 773 if "loop_mode" in new: 774 new["loop_mode"] = [to_bool(value, "loop_mode") for value in new["loop_mode"]] 775 if "loop_duration" in new: 776 new["loop_duration"] = self._normalize_loop_durations(new["loop_duration"]) 777 for name in ("link_trigger_channel1", "link_trigger_channel2"): 778 if name in new: 779 new[name] = [to_bool(value, name) for value in new[name]] 780 modes = list(self._trigger_mode)[1:] 781 if "trigger_mode" in new: 782 modes = [to_name(value, TRIGGER_MODES, "trigger mode") for value in new["trigger_mode"]] 783 # The trigger modes the other settings are sent in: those of channels leaving param sync mode are sent 784 # first, those of channels entering it last 785 first_modes = [new_mode if (old == "Param Sync" and new_mode != "Param Sync") else old 786 for old, new_mode in zip(self._trigger_mode[1:], modes)] 787 rate = self._sampling_rate 788 if "sampling_rate" in params: 789 rate = self._normalize_sampling_rate(params["sampling_rate"]) 790 range_name = self._output_range 791 if "output_range" in params: 792 range_name = self._check_output_range(params["output_range"], first_modes) 793 self._durations_to_samples(new.get("loop_duration", self._loop_duration[1:]), rate) # They fit at the rate 794 795 if first_modes != list(self._trigger_mode)[1:]: 796 self.trigger_mode = first_modes 797 if rate != self._sampling_rate: 798 self.sampling_rate = rate 799 if range_name != self._output_range: 800 self.output_range = range_name 801 for name in ("loop_mode", "loop_duration"): 802 if name in new: 803 setattr(self, name, new[name]) 804 if "link_trigger_channel1" in new or "link_trigger_channel2" in new: 805 self._set_trigger_links(new.get("link_trigger_channel1", self._link_trigger_channel1[1:]), 806 new.get("link_trigger_channel2", self._link_trigger_channel2[1:])) 807 if "current_waveform" in new: 808 self.current_waveform = new["current_waveform"] 809 if modes != first_modes: 810 self.trigger_mode = modes
Program the device with settings exported by WavePalDevice.export_params.
All of them are checked first, and nothing is sent if one is
invalid. They are then sent in an order that lets each take
effect: a trigger channel leaving param sync mode first, then the
sampling rate, the output range and the channel settings, and a
trigger channel entering param sync mode last. So
WavePalDevice.current_waveform applies at once, unless a trigger
channel was in param sync mode and stays in it: then it waits for
the next edge, as an assignment would. Settings missing from
params keep their values. A name that is not a setting is
skipped with a warning, so that settings exported by a newer
version of this package, which may have more, can still be
imported.
The waveforms are not settings: load them with
WavePalDevice.load_waveform first, since a current waveform must
be a loaded one.
import json
with open("session_settings.json") as f:
W.import_params(json.load(f))
Arguments:
- params: A dict of setting name to its value: a number for
"sampling_rate", a name for"output_range", and one value per channel (a list, tuple or NumPy array) for the others, asexport_paramsreturns.
Raises:
- PulsePalError: If a value is invalid (nothing is sent), or the device rejects a setting.
812 @property 813 def waveforms(self): 814 """The waveforms loaded with `WavePalDevice.load_waveform`. 815 816 A list indexed by waveform number, 1 to `DeviceInfo.n_waveforms`, 817 with index 0 unused. Each element is a read-only NumPy array of 818 voltages, or `None` if this object has not loaded that waveform. In 819 param sync mode, it holds the waveforms loaded since the last edge 820 before the device plays them. `WavePalDevice.status` shows what 821 the device itself holds. 822 """ 823 return list(self._waveforms)
The waveforms loaded with WavePalDevice.load_waveform.
A list indexed by waveform number, 1 to DeviceInfo.n_waveforms,
with index 0 unused. Each element is a read-only NumPy array of
voltages, or None if this object has not loaded that waveform. In
param sync mode, it holds the waveforms loaded since the last edge
before the device plays them. WavePalDevice.status shows what
the device itself holds.
829 def load_waveform(self, waveform_number, waveform, channels=None): 830 """Load one of the device's waveforms, and choose the channels that play it. 831 832 The samples are written to the device's microSD card, and the 833 first `DeviceInfo.buffer_samples` of them are also kept in its RAM. 834 Outside param sync mode, loading stops the channels playing that 835 waveform, and the new one plays from their next start. Other 836 channels keep playing. In param sync mode, the new waveform waits 837 for the next rising edge on the param sync trigger channel, and 838 channels go on playing the old one until then (see 839 `WavePalDevice.trigger_mode`). 840 841 ```python 842 t = np.arange(10000) / W.sampling_rate 843 W.load_waveform(2, 3 * np.sin(2 * np.pi * 100 * t), [1, 3]) 844 ``` 845 846 Args: 847 waveform_number: The waveform to load, 1 to 848 `DeviceInfo.n_waveforms`. 849 waveform: Voltages of the samples, played at 850 `WavePalDevice.sampling_rate`. A list, tuple or NumPy 851 array of 1 to `DeviceInfo.max_samples` values, all within 852 `WavePalDevice.output_range`. 853 channels: Output channels that play this waveform, from their 854 next start: one channel number 1-4, or several as a list, 855 tuple or NumPy array. It sets their 856 `WavePalDevice.current_waveform`, and leaves the other 857 channels' alone, so channels already playing this waveform 858 play the new one. `None`, the default, changes no channel. 859 860 Raises: 861 PulsePalError: If an argument is invalid, or the device could 862 not store the waveform. The waveform is then left out of 863 `WavePalDevice.waveforms`: outside param sync mode the 864 device has unloaded it, and in param sync mode it plays the 865 version made current at the last edge. 866 """ 867 number = self._waveform_number(waveform_number) 868 channel_list = None if channels is None else _common.channel_numbers(channels) 869 samples = np.array(waveform, dtype=float).ravel() 870 if not 1 <= samples.size <= self.info.max_samples: 871 raise PulsePalError( 872 f"A waveform must have 1 to {self.info.max_samples} samples. " 873 f"Received {samples.size}." 874 ) 875 codes = self._volts_to_codes(samples) 876 self._waveforms[number] = None # Until the device has stored it. Outside param sync mode, it unloads it first. 877 self._write_command( 878 self._OP_LOAD_WAVEFORM, 879 struct.pack("<BI", number, samples.size) + codes.tobytes(), 880 ) 881 self._read_ack("load_waveform()") 882 samples.flags.writeable = False 883 self._waveforms[number] = samples 884 if channel_list: 885 values = list(self._current_waveform[1:]) 886 for channel in channel_list: 887 values[channel - 1] = number 888 self._current_waveform._assign(values)
Load one of the device's waveforms, and choose the channels that play it.
The samples are written to the device's microSD card, and the
first DeviceInfo.buffer_samples of them are also kept in its RAM.
Outside param sync mode, loading stops the channels playing that
waveform, and the new one plays from their next start. Other
channels keep playing. In param sync mode, the new waveform waits
for the next rising edge on the param sync trigger channel, and
channels go on playing the old one until then (see
WavePalDevice.trigger_mode).
t = np.arange(10000) / W.sampling_rate
W.load_waveform(2, 3 * np.sin(2 * np.pi * 100 * t), [1, 3])
Arguments:
- waveform_number: The waveform to load, 1 to
DeviceInfo.n_waveforms. - waveform: Voltages of the samples, played at
WavePalDevice.sampling_rate. A list, tuple or NumPy array of 1 toDeviceInfo.max_samplesvalues, all withinWavePalDevice.output_range. - channels: Output channels that play this waveform, from their
next start: one channel number 1-4, or several as a list,
tuple or NumPy array. It sets their
WavePalDevice.current_waveform, and leaves the other channels' alone, so channels already playing this waveform play the new one.None, the default, changes no channel.
Raises:
- PulsePalError: If an argument is invalid, or the device could
not store the waveform. The waveform is then left out of
WavePalDevice.waveforms: outside param sync mode the device has unloaded it, and in param sync mode it plays the version made current at the last edge.
890 def trigger(self, channels): 891 """Trigger output channels in software. 892 893 Idle channels start their current waveforms from the first 894 sample, together. Channels that are playing ignore it, as Pulse 895 Pal's channels ignore a soft trigger while they play a pulse train. 896 897 ```python 898 W.trigger(1) 899 W.trigger([2, 4]) # a list, tuple or NumPy array 900 ``` 901 902 Args: 903 channels: Output channel number 1-4, or several as a list, 904 tuple or NumPy array. 905 906 Raises: 907 PulsePalError: If a channel has no waveform to play: its 908 `WavePalDevice.current_waveform` is 0, or a waveform this 909 object has not loaded. Nothing is sent. 910 """ 911 channel_list = _common.channel_numbers(channels) 912 for channel in channel_list: 913 number = self._current_waveform[channel] 914 if number == 0: 915 raise PulsePalError( 916 f"Channel {channel} has no waveform to play. Set current_waveform[{channel}] to a loaded " 917 f"waveform's number, or load one with load_waveform(number, waveform, {channel})." 918 ) 919 if self._waveforms[number] is None: 920 raise PulsePalError( 921 f"Channel {channel} plays waveform {number}, which is not loaded. Load it with " 922 f"load_waveform({number}, waveform)." 923 ) 924 self._write_command(self._OP_TRIGGER, bytes([_common.channel_bits(channel_list)]))
Trigger output channels in software.
Idle channels start their current waveforms from the first sample, together. Channels that are playing ignore it, as Pulse Pal's channels ignore a soft trigger while they play a pulse train.
W.trigger(1)
W.trigger([2, 4]) # a list, tuple or NumPy array
Arguments:
- channels: Output channel number 1-4, or several as a list, tuple or NumPy array.
Raises:
- PulsePalError: If a channel has no waveform to play: its
WavePalDevice.current_waveformis 0, or a waveform this object has not loaded. Nothing is sent.
926 def stop(self, channels=None): 927 """Stop playback. The stopped channels output 0 V. 928 929 Args: 930 channels: Output channel number 1-4, or several as a list, 931 tuple or NumPy array. `None` stops all channels. 932 """ 933 bits = self._ALL_CHANNELS if channels is None else _common.channel_bits(channels) 934 self._write_command(self._OP_STOP, bytes([bits]))
Stop playback. The stopped channels output 0 V.
Arguments:
- channels: Output channel number 1-4, or several as a list,
tuple or NumPy array.
Nonestops all channels.
936 def set_fixed_voltage(self, channels, voltage): 937 """Set output channels to a fixed voltage. 938 939 Stops playback on those channels. The voltage holds until the 940 channel is triggered or stopped. 941 942 Args: 943 channels: Output channel number 1-4, or several as a list, 944 tuple or NumPy array. 945 voltage: Voltage, within `WavePalDevice.output_range`. 946 947 Raises: 948 PulsePalError: If the voltage is outside the output range, or 949 the device rejects the command. 950 """ 951 bits = _common.channel_bits(channels) 952 code = int(self._volts_to_codes([voltage])[0]) 953 self._write_command(self._OP_SET_FIXED_VOLTAGE, 954 struct.pack("<BH", bits, code)) 955 self._read_ack("set_fixed_voltage()")
Set output channels to a fixed voltage.
Stops playback on those channels. The voltage holds until the channel is triggered or stopped.
Arguments:
- channels: Output channel number 1-4, or several as a list, tuple or NumPy array.
- voltage: Voltage, within
WavePalDevice.output_range.
Raises:
- PulsePalError: If the voltage is outside the output range, or the device rejects the command.
957 def status(self): 958 """Read the device's playback state. 959 960 Returns: 961 A `DeviceStatus`, with the channels that are playing, the 962 number of samples in each waveform, underrun counts, the 963 longest playback interrupt, each channel's current waveform, 964 and what waits for a param sync edge. 965 """ 966 self._write_command(self._OP_GET_STATUS) 967 values = struct.unpack( 968 self._STATUS_FORMAT, 969 self._read_raw(struct.calcsize(self._STATUS_FORMAT)), 970 ) 971 playing_bits = values[0] 972 stored_load_bits = values[14] 973 return DeviceStatus( 974 playing=[ch for ch in range(1, 5) 975 if playing_bits & (1 << (ch - 1))], 976 samples_loaded=[None, *values[1:5]], 977 underruns=[None, *values[5:9]], 978 longest_interrupt_us=values[9] / 1000, 979 current_waveform=[None, *values[10:14]], 980 stored_waveforms=[n for n in range(1, 5) if stored_load_bits & (1 << (n - 1))], 981 stored_current_waveform=[None, *values[16:20]] if values[15] else None, 982 )
Read the device's playback state.
Returns:
A
DeviceStatus, with the channels that are playing, the number of samples in each waveform, underrun counts, the longest playback interrupt, each channel's current waveform, and what waits for a param sync edge.
1000 def close(self): 1001 """Close the connection to the device. 1002 1003 The device stops playback on all channels, and shows its own name 1004 on its screen again, in place of "PYTHON Connected". It keeps its 1005 settings and waveforms, so TTL triggers still play them. Safe to 1006 call more than once. Called automatically when leaving a `with` 1007 block and when the object is garbage collected, so playback also 1008 stops when the last reference to the object goes. 1009 1010 """ 1011 self._close(send_disconnect=True)
Close the connection to the device.
The device stops playback on all channels, and shows its own name
on its screen again, in place of "PYTHON Connected". It keeps its
settings and waveforms, so TTL triggers still play them. Safe to
call more than once. Called automatically when leaving a with
block and when the object is garbage collected, so playback also
stops when the last reference to the object goes.
288class SynthPalDevice: 289 """A class to control a Synth Pal on a USB serial port. 290 291 Creating an instance opens the serial port, checks that the device 292 runs Synth Pal firmware, reads its properties into 293 `SynthPalDevice.info`, shows "PYTHON Connected" on the device's 294 screen, stops any playback and programs the default settings (see 295 `SynthPalDevice.set_default_params`). 296 297 ```python 298 from pulsepal import SynthPalDevice 299 300 S = SynthPalDevice("COM3") 301 S.waveform[1] = "Triangle" 302 S.trigger(1) 303 S.close() 304 ``` 305 306 Replace "COM3" with the device's USB serial port name, which 307 `SynthPalDevice.serialportlist` lists. `SynthPalDevice` is also a 308 context manager, which closes the port on exit. 309 310 Closing the connection stops playback, as it does on Pulse Pal (each 311 channel over its off ramp), and puts the device's own name back on its 312 screen. The device keeps its settings, so TTL triggers still play the 313 channels. 314 """ 315 316 port: "serial.Serial" 317 """The open `serial.Serial` port connected to the device.""" 318 319 info: DeviceInfo 320 """Properties of the connected device. See `DeviceInfo`.""" 321 322 _CURRENT_FIRMWARE_VERSION = 1 323 _BAUD_RATE = 12000000 # USB serial ignores the baud rate 324 325 _OP_MENU_BYTE = _common.OP_MENU_BYTE 326 _OP_HANDSHAKE = _common.OP_HANDSHAKE 327 _OP_DISCONNECT = 81 328 _OP_SET_CLIENT_NAME = 89 329 _OP_SET_SCREEN_SAVER = 99 330 _OP_HARDWARE_REVISION = 100 331 _OP_HARDWARE_INFO = ord("N") 332 _OP_SET_FREQUENCY = ord("F") 333 _OP_SET_WAVEFORM = ord("W") 334 _OP_SET_AMPLITUDE = ord("A") 335 _OP_SET_RESTING_VOLTAGE = ord("V") 336 _OP_SET_MEAN_VOLTAGE = ord("M") 337 _OP_SET_PLAY_DURATION = ord("D") 338 _OP_SET_ON_RAMP_DURATION = ord("B") 339 _OP_SET_OFF_RAMP_DURATION = ord("E") 340 _OP_SET_TRIGGER_LINKS = ord("I") 341 _OP_SET_TRIGGER_MODE = ord("T") 342 _OP_SET_ALL_SETTINGS = ord("U") 343 _OP_GET_ALL_SETTINGS = ord("R") 344 # Every setting, as ops 85 ('U') and 82 ('R') carry them: frequency (centiHz), waveform codes, amplitudes, 345 # mean and resting voltages (uV), play, on ramp and off ramp durations (us), links, trigger modes 346 _SETTINGS_FORMAT = "<I4B4i4i4i4I4I4I8B2B" 347 _OP_TRIGGER = ord("P") 348 _OP_STOP = ord("X") 349 _OP_GET_STATUS = ord("G") 350 _OP_GET_PLAYBACK_CHECKSUMS = ord("Z") 351 352 _HANDSHAKE_REPLY = 83 # 'S' 353 _HARDWARE_INFO_FORMAT = "<BBIIIII" 354 _STATUS_FORMAT = "<BI4BII" 355 _ALL_CHANNELS = 0x0F 356 # The ops that change settings: if the reply to one is lost, they are read back (_discard_lost_reply()) 357 _SETTING_OPS = frozenset(map(ord, "FWAVMDBEITU")) 358 359 _reply_pending = None # The op code of a command whose reply has not been read in full yet (see _write_command()) 360 _unsent = frozenset() # The settings assigned with auto_sync off since the settings were last sent or read 361 362 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 363 """Open a connection to a Synth Pal. 364 365 Args: 366 port_name: USB serial port of the device, such as `COM3` on 367 Windows or `/dev/ttyACM0` on Linux. 368 serial_number: Instead of `port_name`, the device's USB serial 369 number (`DeviceInfo.serial_number`), which finds its port 370 wherever it is plugged in. 371 timeout: Serial read timeout, in seconds. 372 373 Firmware newer than this module knows is used with a warning: new 374 firmware only adds commands. Update the package to use what is new. 375 376 Raises: 377 PulsePalError: If the device does not reply to the handshake, or 378 runs other firmware. 379 serial.SerialException: If the serial port cannot be opened, 380 e.g. because another program has it open (on Linux and 381 macOS, another session of these classes, which lock it). 382 """ 383 self._closed = True 384 self._auto_sync = True 385 self.info = DeviceInfo() 386 self._frequency = None 387 self._samples_per_cycle = None 388 # What the device holds for each output channel's waveform, amplitude (in microvolts: a periodic 389 # waveform's peak to peak voltage, or a fixed voltage) and mean voltage (in microvolts), as far as this 390 # object has programmed them. _send_levels() changes them in an order the device accepts. 391 self._device_waveform = ["Sine"] * 4 392 self._device_amplitude_uv = [5_000_000] * 4 393 self._device_mean_uv = [0] * 4 394 self._waveform = ChannelSettings( 395 "waveform", ["Sine"] * 4, self, "_apply_waveform") 396 self._peak_to_peak = ChannelSettings( 397 "peak_to_peak", [5.0] * 4, self, "_apply_peak_to_peak") 398 self._fixed_voltage = ChannelSettings( 399 "fixed_voltage", [5.0] * 4, self, "_apply_fixed_voltage") 400 self._mean_voltage = ChannelSettings( 401 "mean_voltage", [0.0] * 4, self, "_apply_mean_voltage") 402 self._resting_voltage = ChannelSettings( 403 "resting_voltage", [0.0] * 4, self, "_apply_resting_voltage") 404 self._play_duration = ChannelSettings( 405 "play_duration", [1.0] * 4, self, "_apply_play_duration") 406 self._on_ramp_duration = ChannelSettings( 407 "on_ramp_duration", [0.0] * 4, self, "_apply_on_ramp_duration") 408 self._off_ramp_duration = ChannelSettings( 409 "off_ramp_duration", [0.0] * 4, self, "_apply_off_ramp_duration") 410 self._trigger_mode = ChannelSettings( 411 "trigger_mode", ["Normal"] * 2, self, "_apply_trigger_mode") 412 self._link_trigger_channel1 = ChannelSettings( 413 "link_trigger_channel1", [False] * 4, self, 414 "_apply_trigger_channel1_links") 415 self._link_trigger_channel2 = ChannelSettings( 416 "link_trigger_channel2", [False] * 4, self, 417 "_apply_trigger_channel2_links") 418 419 port_name = _common.port_name_from(port_name, serial_number, "SynthPalDevice") 420 self.port = serial.Serial( 421 port_name, 422 self._BAUD_RATE, 423 timeout=timeout, 424 rtscts=True, 425 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 426 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 427 # it, such as another of these classes, gets an error instead 428 exclusive=True, 429 ) 430 self._closed = False 431 try: 432 # Discard anything left in the buffer by an earlier session 433 self.port.reset_input_buffer() 434 self._handshake() 435 self._read_hardware_info() 436 # Client name op + "PYTHON" in ASCII, shown as "PYTHON Connected" 437 self._write_command(self._OP_SET_CLIENT_NAME, b"PYTHON") 438 self.stop() 439 self.set_default_params() 440 except BaseException: 441 # Op 81 means something else to other devices, so it is sent 442 # only once the device has identified itself as a Synth Pal 443 self._close(send_disconnect=self.info.firmware_version is not None) 444 raise 445 446 @staticmethod 447 def serialportlist(ports_to_list="available"): 448 """Return the names of the USB serial ports on this computer. 449 450 Called on the class, without connecting to a device, to find the 451 port name to pass to `SynthPalDevice`. 452 453 Args: 454 ports_to_list: `available` to list only the ports that are 455 not already in use, or `all` to list every USB serial 456 port. Not case sensitive. 457 458 Returns: 459 Sorted list of port names, such as `["COM3", "COM7"]`. 460 461 Raises: 462 PulsePalError: If `ports_to_list` is not `available` or `all`. 463 """ 464 return _common.serialportlist(ports_to_list) 465 466 # ------------------------------------------------------------------ 467 # Settings 468 # ------------------------------------------------------------------ 469 470 def set_default_params(self): 471 """Program the default settings on the device. 472 473 The defaults are a frequency of 100 Hz, and on every output 474 channel a sine wave of 5 V peak to peak around a mean voltage of 475 0 V, resting at 0 V, played for 1 second with no ramps, and a 476 fixed voltage of 5 V for when the waveform is "Fixed Voltage". 477 Both trigger channels are in normal mode, and no output channel is 478 linked to a trigger channel, so a TTL starts nothing until a script 479 links it. They match the settings the device starts with. They are sent at once, 480 also while `SynthPalDevice.auto_sync` is off. 481 """ 482 auto_sync = self._auto_sync 483 self._auto_sync = True 484 try: 485 self.frequency = 100 486 # In this order, each is valid whatever the device holds: a mean of 0 V goes with any 487 # amplitude, 5 V is then a valid amplitude for any waveform, and a sine wave is then valid 488 self._send_levels_in_order((("M", [0] * 4), ("A", [5_000_000] * 4), ("W", ["Sine"] * 4)), 489 force=True) 490 for settings, value in ((self._waveform, "Sine"), (self._peak_to_peak, 5.0), 491 (self._fixed_voltage, 5.0), (self._mean_voltage, 0.0)): 492 settings._store([value] * 4) 493 self.resting_voltage = [0] * 4 494 self.play_duration = [1] * 4 495 self.on_ramp_duration = [0] * 4 496 self.off_ramp_duration = [0] * 4 497 self.trigger_mode = ["Normal"] * 2 498 self._set_trigger_links([False] * 4, [False] * 4) 499 self._unsent = frozenset() # Every setting has been sent 500 finally: 501 self._auto_sync = auto_sync 502 503 @property 504 def frequency(self): 505 """Frequency of all output channels, in Hz. 506 507 1 to 20000 Hz, rounded to 0.01 Hz. It can be changed during 508 playback: playing channels carry on from the same point in their 509 cycle, at the new frequency, and keep the time they have left to 510 play. The frequency played is exact: the device's sample clock is a 511 whole multiple of it (see `SynthPalDevice.sampling_rate`). For 512 `"White Noise"` it sets only the sampling rate. 513 """ 514 return self._frequency 515 516 @frequency.setter 517 def frequency(self, value): 518 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real) \ 519 or not math.isfinite(value): 520 raise PulsePalError( 521 f"frequency must be a number of Hz. Received {value!r}.") 522 centihz = round(value * 100) 523 low = round(self.info.min_frequency * 100) 524 high = round(self.info.max_frequency * 100) 525 if not low <= centihz <= high: 526 raise PulsePalError( 527 f"frequency must be {self.info.min_frequency:g} to " 528 f"{self.info.max_frequency:g} Hz. Received {value!r}." 529 ) 530 if self._auto_sync: 531 self._write_command(self._OP_SET_FREQUENCY, 532 struct.pack("<I", centihz)) 533 confirmed, samples_per_cycle = struct.unpack( 534 "<BI", self._read_raw(5)) 535 if confirmed != 1: 536 raise PulsePalError( 537 f"Synth Pal rejected the frequency {value!r} Hz.") 538 else: 539 samples_per_cycle = _samples_per_cycle(centihz) 540 self._unsent = self._unsent | {"frequency"} # Until sync_to_device() sends it (see auto_sync) 541 self._frequency = centihz / 100 542 self._samples_per_cycle = samples_per_cycle 543 544 @property 545 def auto_sync(self): 546 """Whether assigning a setting programs the device at once. 547 548 `True` (the default): each assignment, such as 549 `S.peak_to_peak[1] = 2`, programs the device at once, as the 550 device's own op for that setting does, also while a trigger 551 channel is in param sync mode. 552 553 `False`: assignments change only this object's copy of the settings, 554 checking each value; `SynthPalDevice.sync_to_device` then sends all 555 of them at once. Use it to store the next trial's settings for a 556 param sync edge (see `SynthPalDevice.trigger_mode`), or to change 557 several settings in one command. Turning it back on does not send 558 changes made meanwhile, so it raises `pulsepal.PulsePalError` while 559 there are any: call `SynthPalDevice.sync_to_device` first to send 560 them, or `SynthPalDevice.sync_from_device` to discard them. A 561 `SynthPalDevice.batch` block does it all for you. `close()` warns of 562 changes never sent. `trigger`, `stop`, `status` and 563 `set_default_params` act at once either way. 564 """ 565 return self._auto_sync 566 567 @auto_sync.setter 568 def auto_sync(self, value): 569 value = to_bool(value, "auto_sync") 570 if value and self._unsent: 571 # Turned on, auto_sync would leave them unsent, and the device playing its old settings 572 raise PulsePalError( 573 f"Changes made with auto_sync off have not been sent to the device: {self._unsent_names()}. Call " 574 "sync_to_device() to send them, or sync_from_device() to discard them, before turning auto_sync on." 575 ) 576 self._auto_sync = value 577 578 def batch(self): 579 """Change several settings, and send them in one command. 580 581 Returns a context manager. In its block, assignments change only 582 this object's copy of the settings (as with 583 `SynthPalDevice.auto_sync` off), and when the block ends, 584 `SynthPalDevice.sync_to_device` sends all of them at once. If the 585 block raises an error, nothing is sent, and the settings go back 586 to what they were. 587 588 ```python 589 with S.batch(): 590 S.frequency = 880 591 S.waveform = ["Triangle"] * 4 592 S.peak_to_peak = [2, 4, 6, 8] 593 ``` 594 595 In param sync mode, the device stores the settings for the next 596 rising edge on the param sync channel (see 597 `SynthPalDevice.trigger_mode`). 598 """ 599 return _common.batch(self) 600 601 def sync_from_device(self): 602 """Read every setting from the device into this object. 603 604 Use it after settings were changed with the device's joystick, for 605 example. The device holds one amplitude per channel: a Fixed 606 Voltage channel's goes to `SynthPalDevice.fixed_voltage`, and any 607 other channel's to `SynthPalDevice.peak_to_peak`. This object keeps 608 the other one, reduced if need be to suit the mean voltage read 609 back. In param sync mode, it reads the settings the device plays 610 now, not a set waiting for the next edge. Changes made with 611 `SynthPalDevice.auto_sync` off and not yet sent are discarded. 612 613 Raises: 614 PulsePalError: If the device does not reply. 615 """ 616 self._write_command(self._OP_GET_ALL_SETTINGS) 617 values = struct.unpack(self._SETTINGS_FORMAT, self._read_raw(struct.calcsize(self._SETTINGS_FORMAT))) 618 centihz = values[0] 619 waveforms = [WAVEFORMS[code] for code in values[1:5]] 620 amplitudes, means = list(values[5:9]), list(values[9:13]) 621 peak_to_peak, fixed_voltage = list(self._peak_to_peak)[1:], list(self._fixed_voltage)[1:] 622 for i, (name, amplitude, mean) in enumerate(zip(waveforms, amplitudes, means)): 623 if name == FIXED_VOLTAGE: 624 fixed_voltage[i] = amplitude / 1e6 625 # This object's own peak to peak voltage must still suit the mean voltage (see _check_levels()) 626 peak_to_peak[i] = min(peak_to_peak[i], (2 * _MAX_VOLTAGE_UV - 2 * abs(mean)) / 1e6) 627 else: 628 peak_to_peak[i] = amplitude / 1e6 629 self._frequency = centihz / 100 630 self._samples_per_cycle = _samples_per_cycle(centihz) 631 self._waveform._store(waveforms) 632 self._peak_to_peak._store(peak_to_peak) 633 self._fixed_voltage._store(fixed_voltage) 634 self._mean_voltage._store([mean / 1e6 for mean in means]) 635 self._resting_voltage._store([rest / 1e6 for rest in values[13:17]]) 636 self._play_duration._store([d / 1e6 for d in values[17:21]]) 637 self._on_ramp_duration._store([d / 1e6 for d in values[21:25]]) 638 self._off_ramp_duration._store([d / 1e6 for d in values[25:29]]) 639 self._link_trigger_channel1._store([bool(link) for link in values[29:33]]) 640 self._link_trigger_channel2._store([bool(link) for link in values[33:37]]) 641 self._trigger_mode._store([TRIGGER_MODES[code] for code in values[37:39]]) 642 self._device_waveform, self._device_amplitude_uv, self._device_mean_uv = waveforms, amplitudes, means 643 self._unsent = frozenset() # Every setting is read 644 645 def export_params(self): 646 """Return every setting, as a dict of plain values and lists. 647 648 Keyed by setting name: `"frequency"`, then the channel settings, 649 each with one value per channel and no unused index 0. It holds 650 only numbers, booleans and names, so it can be saved with `json` 651 and logged with your data, to record exactly what the device 652 played. 653 654 ```python 655 import json 656 with open("trial_settings.json", "w") as f: 657 json.dump(S.export_params(), f) 658 ``` 659 """ 660 params = {"frequency": self._frequency} 661 params.update({name: list(getattr(self, name))[1:] for name in _CHANNEL_SETTINGS}) 662 return params 663 664 def import_params(self, params): 665 """Program the device with settings exported by `SynthPalDevice.export_params`. 666 667 All of them are checked first, and then sent in one command, as 668 at the end of a `SynthPalDevice.batch` block: in param sync mode, 669 the device stores them for the next sync edge. The levels 670 (`waveform`, `peak_to_peak`, `fixed_voltage`, `mean_voltage`) are 671 checked together, so their order in `params` does not matter. 672 Settings missing from `params` keep their values. A name that is 673 not a setting is skipped with a warning, so that settings exported 674 by a newer version of this package, which may have more, can still 675 be imported. 676 677 ```python 678 import json 679 with open("trial_settings.json") as f: 680 S.import_params(json.load(f)) 681 ``` 682 683 Args: 684 params: A dict of setting name to its value: a number for 685 `"frequency"`, and one value per channel (a list, tuple or 686 NumPy array) for the others, as `export_params` returns. 687 688 Raises: 689 PulsePalError: If a value is invalid. Nothing is sent, and the 690 settings keep the values they had. 691 """ 692 names = {"frequency", *_CHANNEL_SETTINGS} 693 unknown = [name for name in params if name not in names] 694 if unknown: 695 warnings.warn(f"import_params(): skipped {', '.join(map(repr, unknown))}, which this version of " 696 "SynthPalDevice does not have.", stacklevel=2) 697 levels = ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage") 698 with self.batch(): 699 for name, values in params.items(): 700 if name in levels: 701 # Each is checked alone here, and the levels together when the block ends (_check_sync()): 702 # an assignment would check each against the others' old values 703 getattr(self, name)._store( 704 self._normalize(name, _common.one_value_per_channel(name, values, 4))) 705 elif name in names: 706 setattr(self, name, values) 707 708 def sync_to_device(self): 709 """Send every setting to the device in one command. 710 711 While a trigger channel is in param sync mode, the device stores 712 the set, replacing any set stored before, and that channel's next 713 rising edge loads it (see `SynthPalDevice.trigger_mode`). Otherwise 714 the device applies it at once. The settings sent are this object's: 715 `frequency`, and for each channel `waveform`, `peak_to_peak` or 716 `fixed_voltage`, `mean_voltage`, `resting_voltage`, 717 `play_duration`, `on_ramp_duration`, `off_ramp_duration`, 718 `link_trigger_channel1` and `link_trigger_channel2`, and 719 `trigger_mode`. They read as sent, including while a set waits for 720 its edge. 721 722 Raises: 723 PulsePalError: If the device rejects the set. 724 """ 725 self._check_sync() 726 self._send_sync() 727 728 def configure(self, channels, *, waveform=None, peak_to_peak=None, fixed_voltage=None, 729 mean_voltage=None, resting_voltage=None, play_duration=None, 730 on_ramp_duration=None, off_ramp_duration=None): 731 """Set several of the output channels' settings at once. 732 733 Settings left out keep their values. The new settings are checked 734 together, so their order does not matter: for example, a channel 735 whose sine wave swings 2 V around a mean voltage of 9 V can go 736 straight to 20 V peak to peak around 0 V. With 737 `SynthPalDevice.auto_sync` on, the class programs the device in an 738 order it accepts at every step. 739 740 ```python 741 S.configure(1, waveform="Sine", peak_to_peak=20, mean_voltage=0) 742 S.configure([2, 3], waveform="Fixed Voltage", fixed_voltage=-2.5, 743 play_duration=0.1) 744 ``` 745 746 Args: 747 channels: Output channel number 1-4, or several as a list, 748 tuple or NumPy array. 749 waveform, peak_to_peak, fixed_voltage, mean_voltage, 750 resting_voltage, play_duration, on_ramp_duration, 751 off_ramp_duration: New values, as for the settings of the 752 same names: one value for every channel given, or a list 753 with one value per channel given, in the same order. 754 755 Raises: 756 PulsePalError: If a value is out of range, or the settings do 757 not go together (nothing is sent), or the device rejects 758 them. 759 """ 760 channels = _common.channel_numbers(channels) 761 if len(set(channels)) != len(channels): 762 raise PulsePalError(f"configure(): channels must be distinct. Received {channels!r}.") 763 requested = {"waveform": waveform, "peak_to_peak": peak_to_peak, "fixed_voltage": fixed_voltage, 764 "mean_voltage": mean_voltage, "resting_voltage": resting_voltage, 765 "play_duration": play_duration, "on_ramp_duration": on_ramp_duration, 766 "off_ramp_duration": off_ramp_duration} 767 new_values = {} 768 for name, value in requested.items(): 769 if value is None: 770 continue 771 values = list(value) if isinstance(value, (list, tuple, np.ndarray)) else [value] 772 if len(values) == 1: 773 values = values * len(channels) 774 if len(values) != len(channels): 775 raise PulsePalError( 776 f"configure(): {name} has {len(values)} values for {len(channels)} channels. Give one " 777 "value, or one value per channel." 778 ) 779 settings = list(getattr(self, name))[1:] 780 for channel, item in zip(channels, values): 781 settings[channel - 1] = item 782 new_values[name] = self._normalize(name, settings) 783 levels = {name: new_values.get(name, list(getattr(self, name))[1:]) 784 for name in ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage")} 785 self._check_levels(levels["peak_to_peak"], levels["mean_voltage"], "configure") 786 if self._auto_sync: 787 self._send_levels(*self._device_levels(**levels)) 788 else: 789 self._unsent = self._unsent | set(new_values) # Until sync_to_device() sends them (see auto_sync) 790 for name in levels: 791 if name in new_values: 792 getattr(self, name)._store(new_values[name]) 793 ops = {"resting_voltage": self._OP_SET_RESTING_VOLTAGE, "play_duration": self._OP_SET_PLAY_DURATION, 794 "on_ramp_duration": self._OP_SET_ON_RAMP_DURATION, 795 "off_ramp_duration": self._OP_SET_OFF_RAMP_DURATION} 796 for name, op in ops.items(): 797 if name in new_values: 798 if self._auto_sync: 799 self._send_setting(name, op, new_values[name]) 800 getattr(self, name)._store(new_values[name]) 801 802 @property 803 def samples_per_cycle(self): 804 """Samples in one cycle of the waveform, at `frequency`. 805 806 The largest multiple of 4 whose sampling rate is at most 807 `DeviceInfo.max_sampling_rate` (100 kHz), so that a sample falls 808 on every edge, peak and trough of every waveform. For example, 809 332 at 300 Hz and 4 at 20 kHz. 810 """ 811 return self._samples_per_cycle 812 813 @property 814 def sampling_rate(self): 815 """The rate at which the device plays samples, in Hz. 816 817 `samples_per_cycle` times `frequency`: 100 kHz or just below it, 818 and at least 50 kHz. For example, 100 kHz at 100 Hz, 99.6 kHz at 819 300 Hz, and 80 kHz at 20 kHz. The device's timer cannot divide 820 every rate exactly, so sample periods differ by up to one tick of 821 its 24 MHz clock (42 ns), arranged so that the frequency is exact. 822 """ 823 return self._samples_per_cycle * self._frequency 824 825 @property 826 def waveform(self): 827 """The waveform of each output channel. 828 829 Indexed by channel number (see "Channel settings" above). Each 830 cycle starts at the trigger: 831 832 - `"Sine"` and `"Triangle"`: start at the mean voltage, rising. 833 - `"Square"`: high for the first half of each cycle, then low. 834 - `"Sawtooth"`: rises from its lowest voltage to its highest, then 835 falls back at the end of the cycle. 836 - `"Fixed Voltage"`: steps to the channel's 837 `SynthPalDevice.fixed_voltage` for the play duration, and 838 returns to the resting voltage. It is not periodic, so the 839 frequency does not change it. 840 - `"White Noise"`: a new random voltage on every sample, uniform 841 over the peak to peak voltage around the mean voltage. It is 842 rendered at the sampling rate the frequency sets 843 (`SynthPalDevice.sampling_rate`), so no claim is made about its 844 spectrum. Each channel has its own random sequence, which carries 845 on from one playback to the next: every playback is new noise. 846 The sequences start from the same point when the device powers 847 up. 848 849 The periodic waveforms and white noise swing 850 `SynthPalDevice.peak_to_peak` around `SynthPalDevice.mean_voltage`. 851 Names are not case sensitive. A change applies to playback in 852 progress. 853 """ 854 return self._waveform 855 856 @waveform.setter 857 def waveform(self, values): 858 self._waveform._assign(values) 859 860 @property 861 def peak_to_peak(self): 862 """The peak to peak voltage of each output channel's waveform, in volts. 863 864 Indexed by channel number (see "Channel settings" above). 0 to 865 20 V: the waveform swings `peak_to_peak / 2` above and below the 866 channel's `SynthPalDevice.mean_voltage`, so it must stay within 867 -10 V to 10 V: `abs(mean_voltage) + peak_to_peak / 2 <= 10`. To 868 raise it beyond what the mean voltage allows, change the mean 869 voltage first, or set both with `SynthPalDevice.configure`. 870 871 A sine wave of 4 V peak to peak swings from -2 V to 2 V around a 872 mean voltage of 0 V: it is 2 sin(2 pi f t). White noise of 4 V peak 873 to peak is uniform from -2 V to 2 V. A `"Fixed Voltage"` channel 874 keeps its peak to peak voltage for when it plays a periodic waveform 875 again. 876 877 Set to the nearest microvolt. A change applies to playback in 878 progress. 879 """ 880 return self._peak_to_peak 881 882 @peak_to_peak.setter 883 def peak_to_peak(self, values): 884 self._peak_to_peak._assign(values) 885 886 @property 887 def fixed_voltage(self): 888 """The voltage each output channel steps to when its waveform is 889 `"Fixed Voltage"`, in volts. 890 891 Indexed by channel number (see "Channel settings" above). -10 to 892 10 V, with any resting voltage and mean voltage. A channel playing 893 another waveform keeps its fixed voltage for when its waveform is 894 `"Fixed Voltage"` again. 895 896 Set to the nearest microvolt. A change applies to playback in 897 progress. 898 """ 899 return self._fixed_voltage 900 901 @fixed_voltage.setter 902 def fixed_voltage(self, values): 903 self._fixed_voltage._assign(values) 904 905 @property 906 def mean_voltage(self): 907 """The mean voltage of each output channel's waveform, in volts. 908 909 Indexed by channel number (see "Channel settings" above). -10 to 910 10 V. A periodic waveform, or white noise, swings around it at full 911 amplitude, and must stay within -10 V to 10 V with the 912 `peak_to_peak` voltage: `abs(mean_voltage) + peak_to_peak / 2 <= 10`. 913 It may differ from the `resting_voltage`, which the channel 914 outputs between playbacks: the ramps move the mean between the two 915 (see "Ramps" above). A `"Fixed Voltage"` ignores it. Set to the 916 nearest microvolt. A change applies to playback in progress. 917 """ 918 return self._mean_voltage 919 920 @mean_voltage.setter 921 def mean_voltage(self, values): 922 self._mean_voltage._assign(values) 923 924 @property 925 def resting_voltage(self): 926 """The resting voltage of each output channel, in volts. 927 928 Indexed by channel number (see "Channel settings" above). -10 to 929 10 V, with any waveform. The channel outputs it while idle, and 930 its ramps start and end there (see "Ramps" above). Set to the 931 nearest microvolt. A change applies to playback in progress. 932 """ 933 return self._resting_voltage 934 935 @resting_voltage.setter 936 def resting_voltage(self, values): 937 self._resting_voltage._assign(values) 938 939 @property 940 def play_duration(self): 941 """How long each output channel plays after a trigger, in seconds. 942 943 Indexed by channel number (see "Channel settings" above). Up to 944 `DeviceInfo.max_play_duration` (3600 s). `0` plays until the 945 channel is stopped. It counts at full amplitude, after the on 946 ramp. The channel stops when the duration has elapsed, part way 947 through a cycle if need be, and returns to its resting voltage, 948 over its off ramp. Durations are counted in samples, rounded to 949 the nearest one, and a nonzero duration lasts at least one sample. 950 A change applies to playback in progress. 951 """ 952 return self._play_duration 953 954 @play_duration.setter 955 def play_duration(self, values): 956 self._play_duration._assign(values) 957 958 @property 959 def on_ramp_duration(self): 960 """How long each output channel fades in after a trigger, in seconds. 961 962 Indexed by channel number (see "Channel settings" above). Up to 963 `DeviceInfo.max_play_duration` (3600 s); `0` (the default) for no 964 ramp. Over it, the peak to peak voltage rises linearly from 0 V to 965 `peak_to_peak`, and the mean from the `resting_voltage` to the 966 `mean_voltage`, before the play duration (see "Ramps" above). 967 Counted in samples, as `play_duration` is. A change applies to 968 playback in progress: a ramp under way keeps the level it has 969 reached, and goes on at the new rate. 970 """ 971 return self._on_ramp_duration 972 973 @on_ramp_duration.setter 974 def on_ramp_duration(self, values): 975 self._on_ramp_duration._assign(values) 976 977 @property 978 def off_ramp_duration(self): 979 """How long each output channel fades out when it stops, in seconds. 980 981 Indexed by channel number (see "Channel settings" above). Up to 982 `DeviceInfo.max_play_duration` (3600 s); `0` (the default) for no 983 ramp. Over it, the peak to peak voltage falls linearly to 0 V, and 984 the mean to the `resting_voltage`, after the play duration or a 985 stop (see "Ramps" above). Counted in samples, as `play_duration` 986 is. A change applies to playback in progress. 987 """ 988 return self._off_ramp_duration 989 990 @off_ramp_duration.setter 991 def off_ramp_duration(self, values): 992 self._off_ramp_duration._assign(values) 993 994 @property 995 def trigger_mode(self): 996 """How each trigger channel acts on the output channels linked to it. 997 998 Indexed by trigger channel number, 1 or 2 (index 0 is unused). 999 These are Pulse Pal's trigger modes: 1000 1001 - `"Normal"`: a rising edge starts the linked channels. Channels 1002 that are playing ignore it. 1003 - `"Toggle"`: a rising edge starts the linked channels, or stops 1004 those that are playing. 1005 - `"Gated"`: a rising edge starts the linked channels, and a 1006 falling edge stops them, unless the other trigger channel is 1007 also gated, linked to them, and still high. With a play 1008 duration of 0, a channel plays for exactly as long as the TTL is 1009 high. 1010 - `"Param Sync"`: a rising edge starts and stops nothing (the 1011 channel's links are ignored). It loads the settings most recently 1012 sent by `SynthPalDevice.sync_to_device` (or at the end of a 1013 `SynthPalDevice.batch` block), if any. This is how the next 1014 trial's settings are sent during the current trial and applied 1015 the instant it starts. 1016 1017 At a param sync edge, the frequency and both trigger modes change 1018 at once: they are shared by all channels. An output channel at its 1019 resting voltage takes its new settings at once. One that is playing 1020 (off ramp included) finishes on the settings it started with, at 1021 the new frequency, and takes the new ones the moment it reaches its 1022 resting voltage, so the next trigger plays them. A trigger mode 1023 sent by `SynthPalDevice.sync_to_device` applies from the next 1024 edge. 1025 1026 Only `SynthPalDevice.sync_to_device` is held back for the edge: 1027 with `SynthPalDevice.auto_sync` on, every assignment programs the 1028 device at once, also in param sync mode. So leaving param sync mode 1029 means assigning `trigger_mode` with `auto_sync` on. When no trigger 1030 channel is left in the mode, the device discards a stored set. 1031 1032 To start channels on the same edge, wire the TTL to the other 1033 trigger channel too: the settings load first, so the channels it 1034 starts play the new settings. Connecting, and `set_default_params`, 1035 take both trigger channels out of param sync mode. 1036 1037 Names are not case sensitive. 1038 """ 1039 return self._trigger_mode 1040 1041 @trigger_mode.setter 1042 def trigger_mode(self, values): 1043 self._trigger_mode._assign(values) 1044 1045 @property 1046 def link_trigger_channel1(self): 1047 """Whether each output channel is triggered by trigger channel 1. 1048 1049 Indexed by channel number (see "Channel settings" above). By 1050 default no output channel is linked to either trigger channel, so 1051 a TTL starts nothing until a script links it. 1052 """ 1053 return self._link_trigger_channel1 1054 1055 @link_trigger_channel1.setter 1056 def link_trigger_channel1(self, values): 1057 self._link_trigger_channel1._assign(values) 1058 1059 @property 1060 def link_trigger_channel2(self): 1061 """Whether each output channel is triggered by trigger channel 2. 1062 1063 Indexed by channel number (see "Channel settings" above). 1064 """ 1065 return self._link_trigger_channel2 1066 1067 @link_trigger_channel2.setter 1068 def link_trigger_channel2(self, values): 1069 self._link_trigger_channel2._assign(values) 1070 1071 def set_screen_saver(self, enabled, timeout=1800): 1072 """Switch the device's screen saver on or off, and set its timeout. 1073 1074 With the screen saver on, the device dims its screen once it has 1075 been left alone for `timeout` seconds: no command from a computer, 1076 no rising edge on a trigger channel, and no joystick click or 1077 push. The next of these brings the screen back. The device keeps 1078 both settings in its EEPROM, shared with Pulse Pal firmware, and 1079 saves them once no channel is playing. A new device starts with 1080 the screen saver on and 1800 s. 1081 1082 Args: 1083 enabled: `True` to switch the screen saver on, `False` to 1084 switch it off. 1085 timeout: Seconds without activity before the screen dims, 1 to 1086 65535. 1087 1088 Raises: 1089 PulsePalError: If a value is out of range. 1090 """ 1091 state = to_bool(enabled, "enabled") 1092 if isinstance(timeout, (bool, np.bool_)) \ 1093 or not isinstance(timeout, numbers.Integral) \ 1094 or not 1 <= timeout <= 65535: 1095 raise PulsePalError( 1096 "timeout must be a whole number of seconds from 1 to 65535. " 1097 f"Received {timeout!r}." 1098 ) 1099 self._write_command(self._OP_SET_SCREEN_SAVER, 1100 struct.pack("<BH", int(state), int(timeout))) 1101 self._read_ack("set_screen_saver()") 1102 1103 # ------------------------------------------------------------------ 1104 # Playback 1105 # ------------------------------------------------------------------ 1106 1107 def trigger(self, channels): 1108 """Trigger output channels in software. 1109 1110 Channels that are idle start from the beginning of their waveform 1111 cycle, together. Channels that are playing ignore it, as they 1112 ignore a trigger channel in normal mode. 1113 1114 ```python 1115 S.trigger(1) 1116 S.trigger([2, 4]) # a list, tuple or NumPy array 1117 ``` 1118 1119 Args: 1120 channels: Output channel number 1-4, or several as a list, 1121 tuple or NumPy array. 1122 """ 1123 self._write_command(self._OP_TRIGGER, bytes([_common.channel_bits(channels)])) 1124 1125 def stop(self, channels=None): 1126 """Stop playback. The stopped channels return to their resting 1127 voltage, over their off ramps. 1128 1129 Args: 1130 channels: Output channel number 1-4, or several as a list, 1131 tuple or NumPy array. `None` stops all channels. 1132 """ 1133 bits = self._ALL_CHANNELS if channels is None else _common.channel_bits(channels) 1134 self._write_command(self._OP_STOP, bytes([bits])) 1135 1136 def status(self): 1137 """Read the device's playback state. 1138 1139 Returns: 1140 A `DeviceStatus`, with the channels that are playing, the 1141 samples per cycle, each channel's output range, and timing 1142 figures. 1143 """ 1144 self._write_command(self._OP_GET_STATUS) 1145 values = struct.unpack( 1146 self._STATUS_FORMAT, 1147 self._read_raw(struct.calcsize(self._STATUS_FORMAT)), 1148 ) 1149 playing_bits = values[0] 1150 range_names = list(OUTPUT_RANGES) 1151 return DeviceStatus( 1152 playing=[ch for ch in range(1, 5) 1153 if playing_bits & (1 << (ch - 1))], 1154 samples_per_cycle=values[1], 1155 output_ranges=[None, *(range_names[i] if i < len(range_names) 1156 else None for i in values[2:6])], 1157 longest_interrupt_us=values[6] / 1000, 1158 late_updates=values[7], 1159 ) 1160 1161 def _playback_checksums(self): 1162 """For testing: what each channel played since it last started. 1163 1164 Returns two lists indexed by channel number, with index 0 unused: 1165 the number of samples played, and the sum of their DAC codes 1166 modulo 2**32. `tests/synthpal_hardware_test.py` compares them with 1167 the samples it expects, to check what the device played. 1168 """ 1169 self._write_command(self._OP_GET_PLAYBACK_CHECKSUMS) 1170 values = struct.unpack("<8I", self._read_raw(32)) 1171 return [None, *values[:4]], [None, *values[4:]] 1172 1173 # ------------------------------------------------------------------ 1174 # Connection 1175 # ------------------------------------------------------------------ 1176 1177 def close(self): 1178 """Close the connection to the device. 1179 1180 The device stops playback on all channels, each over its off ramp, 1181 and shows its own name on its screen again, in place of "PYTHON 1182 Connected". It keeps its settings, so TTL triggers still play the 1183 channels. Safe to call more than once. Called automatically when 1184 leaving a `with` block and when the object is garbage collected, 1185 so playback also stops when the last reference to the object 1186 goes. 1187 1188 """ 1189 self._close(send_disconnect=True) 1190 1191 def _close(self, send_disconnect): 1192 """Close the connection. send_disconnect: tell the device first (op 81). False when the device may not 1193 run this firmware: op 81 means something else to other devices.""" 1194 if getattr(self, "_closed", True): 1195 return 1196 self._closed = True 1197 if send_disconnect and self._unsent: 1198 _common.warn(f"SynthPalDevice closed with changes made with auto_sync off that were never sent to the " 1199 f"device: {self._unsent_names()}.") 1200 self._reply_pending = None # Nothing more is read, so a reply still to come needs no discarding 1201 try: 1202 if send_disconnect and self.port and self.port.is_open: 1203 self._write_command(self._OP_DISCONNECT) 1204 except Exception: 1205 pass # The port may already be gone, e.g. the cable was unplugged 1206 finally: 1207 if self.port and self.port.is_open: 1208 self.port.close() 1209 1210 def __enter__(self): 1211 """Enter a `with` block, returning the connected device.""" 1212 return self 1213 1214 def __exit__(self, exc_type, exc_value, traceback): 1215 """Close the port when leaving a `with` block. 1216 1217 Returns: 1218 `False`, so any exception raised in the block propagates. 1219 """ 1220 self.close() 1221 return False 1222 1223 def __del__(self): 1224 """Close the port when the object is collected.""" 1225 try: 1226 self.close() 1227 except Exception: 1228 # Destructors should not raise; the serial object may already be 1229 # gone during interpreter shutdown. 1230 pass 1231 1232 def __setattr__(self, name, value): 1233 """Refuse a public attribute that is not a setting, such as a misspelled one: it would program nothing (see 1234 _common.refuse_unknown_attribute()).""" 1235 if name[0] != "_": 1236 _common.refuse_unknown_attribute(self, name, _SETTING_NAMES) 1237 super().__setattr__(name, value) 1238 1239 def __repr__(self): 1240 """Describe the device and its settings, e.g. with `print(S)`.""" 1241 port = getattr(getattr(self, "port", None), "port", None) 1242 lines = [ 1243 f"SynthPalDevice on {port} (Synth Pal firmware v{self.info.firmware_version})", 1244 f"frequency: {self._frequency} Hz ({self._samples_per_cycle} samples per cycle)", 1245 f"auto_sync: {self._auto_sync}", 1246 ] 1247 lines += [f"{name}: {list(getattr(self, name))}" for name in _CHANNEL_SETTINGS] 1248 return "\n".join(lines) 1249 1250 # ------------------------------------------------------------------ 1251 # Internals: settings 1252 # ------------------------------------------------------------------ 1253 1254 def _handshake(self): 1255 """Check that the device runs a supported Synth Pal firmware.""" 1256 self._write_command(self._OP_HANDSHAKE) 1257 handshake = _common.read_handshake_reply(self.port) # Skips a reply to an earlier session's command 1258 if handshake is None: 1259 raise PulsePalError( 1260 f"No reply from the device on {self.port.port}. Is it a " 1261 "Pulse Pal 3 running Synth Pal firmware?" 1262 f" {_common.NO_REPLY_HINT}" 1263 ) 1264 reply, version = handshake 1265 if reply != self._HANDSHAKE_REPLY: 1266 if reply in _common.FIRMWARE_BY_HANDSHAKE_REPLY: 1267 raise _common.other_firmware_error(reply, version, self.port.port, "Synth Pal") 1268 raise PulsePalError( 1269 "Incorrect handshake returned. Expected " 1270 f"{self._HANDSHAKE_REPLY}, received {reply}." 1271 ) 1272 if version > self._CURRENT_FIRMWARE_VERSION: 1273 # New firmware only adds commands (see PROTOCOL.md), so this class still works with it 1274 warnings.warn( 1275 f"Synth Pal firmware v{version} is newer than this pulsepal package knows " 1276 f"(v{self._CURRENT_FIRMWARE_VERSION}). It works with it, but update the package to use what is " 1277 "new.", stacklevel=3) 1278 self.info.firmware_version = version 1279 1280 def _read_hardware_info(self): 1281 self._write_command(self._OP_HARDWARE_INFO) 1282 ( 1283 self.info.hardware_version, 1284 self.info.n_channels, 1285 min_centihz, 1286 max_centihz, 1287 self.info.max_sampling_rate, 1288 self.info.timer_clock_hz, 1289 max_duration_us, 1290 ) = struct.unpack( 1291 self._HARDWARE_INFO_FORMAT, 1292 self._read_raw(struct.calcsize(self._HARDWARE_INFO_FORMAT)), 1293 ) 1294 self.info.min_frequency = min_centihz / 100 1295 self.info.max_frequency = max_centihz / 100 1296 self.info.max_play_duration = max_duration_us / 1e6 1297 self._write_command(self._OP_HARDWARE_REVISION) 1298 self.info.circuit_board_revision = self._read_raw(1)[0] 1299 self._write_command(_common.OP_SERIAL_NUMBER) 1300 self.info.serial_number = _common.read_serial_number(self._read_raw) 1301 1302 def _normalize(self, name, values): 1303 """Check new values for all four channels of a setting, and return them as its list holds them.""" 1304 if name == "waveform": 1305 return [to_name(value, WAVEFORMS, "waveform") for value in values] 1306 if name == "peak_to_peak": 1307 return [self._to_volts(value, name, 0, 20, " peak to peak") for value in values] 1308 if name in ("fixed_voltage", "mean_voltage", "resting_voltage"): 1309 return [self._to_volts(value, name, -10, 10) for value in values] 1310 zero_meaning = "0 (play until stopped)" if name == "play_duration" else "0 (no ramp)" 1311 return [self._to_seconds(value, name, zero_meaning) for value in values] 1312 1313 def _check_levels(self, peak_to_peak, mean_voltage, setting): 1314 """Check that each channel's periodic waveform stays within -10 V to 10 V, as the device does. 1315 1316 Checked on every channel, also one playing a fixed voltage, so that a change of waveform always 1317 suits the levels it finds. `setting` is the one being changed, for the advice in the message. 1318 """ 1319 for channel, (pp, mean) in enumerate(zip(peak_to_peak, mean_voltage), start=1): 1320 pp_uv, mean_uv = _microvolts(pp), _microvolts(mean) 1321 if 2 * abs(mean_uv) + pp_uv > 2 * _MAX_VOLTAGE_UV: 1322 advice = { 1323 "peak_to_peak": "Change mean_voltage first, set both with configure(), or choose a " 1324 "smaller peak_to_peak.", 1325 "mean_voltage": "Change peak_to_peak first, set both with configure(), or choose a " 1326 "smaller mean_voltage.", 1327 }.get(setting, "Change peak_to_peak or mean_voltage.") 1328 raise PulsePalError( 1329 f"On channel {channel}, a mean voltage of {mean:g} V and a peak to peak voltage of " 1330 f"{pp:g} V would reach {(abs(mean_uv) + pp_uv / 2) / 1e6:g} V. The waveform must " 1331 f"stay within -10 V to 10 V. {advice}" 1332 ) 1333 1334 def _device_levels(self, waveform, peak_to_peak, fixed_voltage, mean_voltage): 1335 """What the device holds for these settings: waveforms, amplitudes in microvolts (a fixed voltage, or a 1336 periodic waveform's peak to peak voltage) and mean voltages in microvolts.""" 1337 amplitudes = [_microvolts(fixed if name == FIXED_VOLTAGE else pp) 1338 for name, pp, fixed in zip(waveform, peak_to_peak, fixed_voltage)] 1339 return list(waveform), amplitudes, [_microvolts(mean) for mean in mean_voltage] 1340 1341 def _apply_levels(self, name, values): 1342 """Apply new values of waveform, peak_to_peak, fixed_voltage or mean_voltage, for all four channels.""" 1343 values = self._normalize(name, values) 1344 levels = {level: list(getattr(self, level))[1:] 1345 for level in ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage")} 1346 levels[name] = values 1347 self._check_levels(levels["peak_to_peak"], levels["mean_voltage"], name) 1348 if self._auto_sync: 1349 self._send_levels(*self._device_levels(**levels)) 1350 else: 1351 self._unsent = self._unsent | {name} # Until sync_to_device() sends it (see auto_sync) 1352 return values 1353 1354 def _apply_waveform(self, values): 1355 return self._apply_levels("waveform", values) 1356 1357 def _apply_peak_to_peak(self, values): 1358 return self._apply_levels("peak_to_peak", values) 1359 1360 def _apply_fixed_voltage(self, values): 1361 return self._apply_levels("fixed_voltage", values) 1362 1363 def _apply_mean_voltage(self, values): 1364 return self._apply_levels("mean_voltage", values) 1365 1366 def _send_levels(self, waveforms, amplitudes_uv, means_uv): 1367 """Program the device's waveforms, amplitudes and mean voltages, in an order it accepts at every step. 1368 1369 The device checks each op against what it holds for the other two (see _is_valid_level()), and one 1370 order does not suit every change: a sine wave of 20 V peak to peak cannot become a fixed voltage of 1371 -5 V by either op first. So each channel first takes an amplitude that suits both its current and 1372 its new waveform and mean voltage: its current amplitude if it can, else its new one, else 0 V, which 1373 suits any. Then the waveforms, the mean voltages and the new amplitudes follow. If the device 1374 refuses a step, which means this object's record of what it holds is out of date (e.g. a param sync 1375 edge loaded a stored set), the change is made from 0 V, which works from any state. 1376 """ 1377 current = (self._device_waveform, self._device_amplitude_uv, self._device_mean_uv) 1378 if (waveforms, amplitudes_uv, means_uv) == tuple(map(list, current)): 1379 return 1380 first_amplitudes = [] 1381 for w0, a0, m0, w1, a1, m1 in zip(*current, waveforms, amplitudes_uv, means_uv): 1382 for candidate in (a0, a1, 0): 1383 if all(_is_valid_level(w, candidate, m) for w, m in ((w0, m0), (w1, m0), (w1, m1))): 1384 first_amplitudes.append(candidate) 1385 break 1386 steps = (("A", first_amplitudes), ("W", waveforms), ("M", means_uv), ("A", amplitudes_uv)) 1387 try: 1388 self._send_levels_in_order(steps) 1389 except _Rejected: 1390 self._send_levels_in_order((("A", [0] * 4), ("W", waveforms), ("M", means_uv), 1391 ("A", amplitudes_uv)), force=True) 1392 1393 def _send_levels_in_order(self, steps, force=False): 1394 """Send ops 'A' (amplitudes, uV), 'W' (waveform names) and 'M' (mean voltages, uV), in order, and keep 1395 the record of what the device holds. Unless `force`, an op that would not change what the record says 1396 the device holds is skipped.""" 1397 for op, values in steps: 1398 if op == "W": 1399 if values == self._device_waveform and not force: 1400 continue 1401 self._write_command(self._OP_SET_WAVEFORM, bytes(WAVEFORMS.index(name) for name in values)) 1402 self._read_ack("setting waveform") 1403 self._device_waveform = list(values) 1404 elif op == "A": 1405 if values == self._device_amplitude_uv and not force: 1406 continue 1407 self._write_command(self._OP_SET_AMPLITUDE, struct.pack("<4i", *values)) 1408 self._read_ack("setting the amplitude") 1409 self._device_amplitude_uv = list(values) 1410 else: 1411 if values == self._device_mean_uv and not force: 1412 continue 1413 self._write_command(self._OP_SET_MEAN_VOLTAGE, struct.pack("<4i", *values)) 1414 self._read_ack("setting mean_voltage") 1415 self._device_mean_uv = list(values) 1416 1417 def _apply_resting_voltage(self, values): 1418 # Any resting voltage within -10 V to 10 V goes with any waveform 1419 values = self._normalize("resting_voltage", values) 1420 if self._auto_sync: 1421 self._send_setting("resting_voltage", self._OP_SET_RESTING_VOLTAGE, values) 1422 else: 1423 self._unsent = self._unsent | {"resting_voltage"} 1424 return values 1425 1426 def _apply_play_duration(self, values): 1427 return self._apply_duration("play_duration", self._OP_SET_PLAY_DURATION, values) 1428 1429 def _apply_on_ramp_duration(self, values): 1430 return self._apply_duration("on_ramp_duration", self._OP_SET_ON_RAMP_DURATION, values) 1431 1432 def _apply_off_ramp_duration(self, values): 1433 return self._apply_duration("off_ramp_duration", self._OP_SET_OFF_RAMP_DURATION, values) 1434 1435 def _apply_duration(self, name, op, values): 1436 values = self._normalize(name, values) 1437 if self._auto_sync: 1438 self._send_setting(name, op, values) 1439 else: 1440 self._unsent = self._unsent | {name} 1441 return values 1442 1443 def _send_setting(self, name, op, values): 1444 """Send the resting voltages (int32 microvolts) or a duration (uint32 microseconds), for all four 1445 channels.""" 1446 if name == "resting_voltage": 1447 data = struct.pack("<4i", *(_microvolts(v) for v in values)) 1448 else: 1449 data = struct.pack("<4I", *(round(d * 1e6) for d in values)) 1450 self._write_command(op, data) 1451 self._read_ack(f"setting {name}") 1452 1453 def _apply_trigger_mode(self, values): 1454 names = [to_name(value, TRIGGER_MODES, "trigger mode") for value in values] 1455 if self._auto_sync: 1456 self._write_command( 1457 self._OP_SET_TRIGGER_MODE, 1458 bytes(TRIGGER_MODES.index(name) for name in names), 1459 ) 1460 self._read_ack("setting trigger_mode") 1461 else: 1462 self._unsent = self._unsent | {"trigger_mode"} 1463 return names 1464 1465 # One op programs both trigger channels' links, so each list is sent 1466 # with the other's current values 1467 def _apply_trigger_channel1_links(self, values): 1468 links1 = [to_bool(v, "link_trigger_channel1") for v in values] 1469 self._send_trigger_links(links1, self._link_trigger_channel2[1:]) 1470 if not self._auto_sync: 1471 self._unsent = self._unsent | {"link_trigger_channel1"} 1472 return links1 1473 1474 def _apply_trigger_channel2_links(self, values): 1475 links2 = [to_bool(v, "link_trigger_channel2") for v in values] 1476 self._send_trigger_links(self._link_trigger_channel1[1:], links2) 1477 if not self._auto_sync: 1478 self._unsent = self._unsent | {"link_trigger_channel2"} 1479 return links2 1480 1481 def _set_trigger_links(self, links1, links2): 1482 """Program both trigger channels' links with one command.""" 1483 self._send_trigger_links(links1, links2) 1484 self._link_trigger_channel1._store(links1) 1485 self._link_trigger_channel2._store(links2) 1486 1487 def _send_trigger_links(self, links1, links2): 1488 if self._auto_sync: 1489 self._write_command(self._OP_SET_TRIGGER_LINKS, 1490 bytes([*map(int, links1), *map(int, links2)])) 1491 self._read_ack("setting the trigger channel links") 1492 1493 def _snapshot(self): 1494 """All settings, and which are unsent, for batch() to restore if its block fails.""" 1495 settings = {name: list(getattr(self, name))[1:] for name in _CHANNEL_SETTINGS} 1496 return settings, self._frequency, self._samples_per_cycle, self._unsent 1497 1498 def _restore(self, snapshot): 1499 settings, self._frequency, self._samples_per_cycle, self._unsent = snapshot 1500 for name, values in settings.items(): 1501 getattr(self, name)._store(values) 1502 1503 def _unsent_names(self): 1504 """The settings assigned with auto_sync off and not yet sent, for messages, in the class's order.""" 1505 return ", ".join(name for name in _SETTING_NAMES if name in self._unsent) 1506 1507 def _check_sync(self): 1508 """Check the whole set before sync_to_device() sends it.""" 1509 self._check_levels(self._peak_to_peak[1:], self._mean_voltage[1:], "sync") 1510 1511 def _send_sync(self): 1512 """Send every setting with op 85 ('U'). The device checks the whole set, so no order matters.""" 1513 self._unsent = frozenset() # All are sent below 1514 waveforms, amplitudes, means = self._device_levels( 1515 self._waveform[1:], self._peak_to_peak[1:], self._fixed_voltage[1:], self._mean_voltage[1:]) 1516 links = [int(link) for link in (*self._link_trigger_channel1[1:], 1517 *self._link_trigger_channel2[1:])] 1518 payload = struct.pack( 1519 self._SETTINGS_FORMAT, 1520 round(self._frequency * 100), 1521 *(WAVEFORMS.index(name) for name in waveforms), 1522 *amplitudes, *means, *(_microvolts(v) for v in self._resting_voltage[1:]), 1523 *(round(d * 1e6) for d in self._play_duration[1:]), 1524 *(round(d * 1e6) for d in self._on_ramp_duration[1:]), 1525 *(round(d * 1e6) for d in self._off_ramp_duration[1:]), 1526 *links, 1527 *(TRIGGER_MODES.index(name) for name in self._trigger_mode[1:]), 1528 ) 1529 self._write_command(self._OP_SET_ALL_SETTINGS, payload) 1530 self._read_ack("sync_to_device()") 1531 # Applied now, or, in param sync mode, at the next edge. Either way these are the levels the next 1532 # assignments build on; if a stored set has not loaded yet, _send_levels() finds out and recovers. 1533 self._device_waveform, self._device_amplitude_uv, self._device_mean_uv = waveforms, amplitudes, means 1534 1535 @staticmethod 1536 def _to_volts(value, name, low, high, note=""): 1537 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real) \ 1538 or not math.isfinite(value) or not low <= value <= high: 1539 raise PulsePalError( 1540 f"{name} values must be numbers of volts from {low} to " 1541 f"{high}{note}. Received {value!r}." 1542 ) 1543 return float(value) 1544 1545 def _to_seconds(self, value, name, zero_meaning): 1546 if isinstance(value, (bool, np.bool_)) or not isinstance(value, numbers.Real): 1547 raise PulsePalError( 1548 f"{name} must be in seconds. Received {value!r}." 1549 ) 1550 duration = float(value) 1551 if not math.isfinite(duration) or not \ 1552 0 <= duration <= self.info.max_play_duration: 1553 raise PulsePalError( 1554 f"{name} must be {zero_meaning} or a positive number " 1555 f"of seconds up to {self.info.max_play_duration:g}. " 1556 f"Received {value!r}." 1557 ) 1558 return duration 1559 1560 # ------------------------------------------------------------------ 1561 # Internals: serial port 1562 # ------------------------------------------------------------------ 1563 1564 def _write_command(self, op_code, data=b""): 1565 """Send one command, with its framing byte, in a single write. 1566 1567 A command whose reply was not read in full, because the read timed out or was interrupted (Ctrl-C), leaves 1568 that reply, or the rest of it, to arrive later. Read as the reply to the next command, it would put every 1569 reply after it out of step. So it is discarded first (_discard_lost_reply()), and this command's reply, if it 1570 has one, is pending until _read_raw() has read it, as in PulsePalDevice. 1571 """ 1572 if self._reply_pending is not None: 1573 self._discard_lost_reply() 1574 if op_code not in _OPS_WITHOUT_TRACKED_REPLY: 1575 self._reply_pending = op_code # Before the write: a Ctrl-C can come as soon as it returns 1576 message = bytes([self._OP_MENU_BYTE, op_code]) + data 1577 bytes_written = self.port.write(message) 1578 if bytes_written != len(message): 1579 raise PulsePalError( 1580 f"Wrote {bytes_written} byte(s), expected to write " 1581 f"{len(message)} byte(s)." 1582 ) 1583 1584 def _discard_lost_reply(self): 1585 """Discard what arrives of a reply that was not read in full (see _write_command()), and bring this object 1586 back in step with the device. 1587 1588 The command may have changed settings, unknown to this object: an assignment interrupted after its write, 1589 for one, which the device took and this object did not. So after one of _SETTING_OPS, they are read back 1590 (op 'R'), outside param sync mode, where op 'R' returns the settings playing rather than a set stored for the 1591 next sync edge. 1592 """ 1593 op = self._reply_pending 1594 self._reply_pending = None 1595 n_late = len(_common.read_until_quiet(self.port)) 1596 read_back = op in self._SETTING_OPS and "Param Sync" not in list(self._trigger_mode)[1:] 1597 _common.warn_lost_reply(f"op {chr(op)!r}", n_late, 1598 ", and the settings were read back from the device" if read_back else "") 1599 if read_back: 1600 try: 1601 self.sync_from_device() 1602 except PulsePalError as exc: 1603 raise PulsePalError( 1604 f"Synth Pal did not send its settings, read back after the reply to an earlier command was lost. " 1605 f"{_common.NO_REPLY_HINT}" 1606 ) from exc 1607 1608 def _read_raw(self, n_bytes): 1609 """Read exactly n_bytes from the serial port: the whole reply to the command last sent, which is then no 1610 longer pending (see _write_command()). Only connecting reads a reply in more than one call.""" 1611 message = self.port.read(n_bytes) 1612 if len(message) < n_bytes: 1613 raise PulsePalError( 1614 f"Synth Pal sent {len(message)} of the {n_bytes} byte(s) of its reply within " 1615 f"{self.port.timeout} s. {_common.NO_REPLY_HINT}" 1616 ) 1617 self._reply_pending = None 1618 return message 1619 1620 def _read_ack(self, context): 1621 """Read a one-byte confirmation: 1 if the device executed the 1622 command, 0 if it rejected it.""" 1623 try: 1624 reply = self._read_raw(1)[0] 1625 except PulsePalError as exc: 1626 raise PulsePalError( 1627 f"Synth Pal did not confirm {context}: no reply arrived within {self.port.timeout} s. " 1628 f"{_common.NO_REPLY_HINT}" 1629 ) from exc 1630 if reply != 1: 1631 raise _Rejected( 1632 f"Synth Pal rejected {context}. A value was out of range." 1633 )
A class to control a Synth Pal on a USB serial port.
Creating an instance opens the serial port, checks that the device
runs Synth Pal firmware, reads its properties into
SynthPalDevice.info, shows "PYTHON Connected" on the device's
screen, stops any playback and programs the default settings (see
SynthPalDevice.set_default_params).
from pulsepal import SynthPalDevice
S = SynthPalDevice("COM3")
S.waveform[1] = "Triangle"
S.trigger(1)
S.close()
Replace "COM3" with the device's USB serial port name, which
SynthPalDevice.serialportlist lists. SynthPalDevice is also a
context manager, which closes the port on exit.
Closing the connection stops playback, as it does on Pulse Pal (each channel over its off ramp), and puts the device's own name back on its screen. The device keeps its settings, so TTL triggers still play the channels.
362 def __init__(self, port_name=None, *, serial_number=None, timeout=10): 363 """Open a connection to a Synth Pal. 364 365 Args: 366 port_name: USB serial port of the device, such as `COM3` on 367 Windows or `/dev/ttyACM0` on Linux. 368 serial_number: Instead of `port_name`, the device's USB serial 369 number (`DeviceInfo.serial_number`), which finds its port 370 wherever it is plugged in. 371 timeout: Serial read timeout, in seconds. 372 373 Firmware newer than this module knows is used with a warning: new 374 firmware only adds commands. Update the package to use what is new. 375 376 Raises: 377 PulsePalError: If the device does not reply to the handshake, or 378 runs other firmware. 379 serial.SerialException: If the serial port cannot be opened, 380 e.g. because another program has it open (on Linux and 381 macOS, another session of these classes, which lock it). 382 """ 383 self._closed = True 384 self._auto_sync = True 385 self.info = DeviceInfo() 386 self._frequency = None 387 self._samples_per_cycle = None 388 # What the device holds for each output channel's waveform, amplitude (in microvolts: a periodic 389 # waveform's peak to peak voltage, or a fixed voltage) and mean voltage (in microvolts), as far as this 390 # object has programmed them. _send_levels() changes them in an order the device accepts. 391 self._device_waveform = ["Sine"] * 4 392 self._device_amplitude_uv = [5_000_000] * 4 393 self._device_mean_uv = [0] * 4 394 self._waveform = ChannelSettings( 395 "waveform", ["Sine"] * 4, self, "_apply_waveform") 396 self._peak_to_peak = ChannelSettings( 397 "peak_to_peak", [5.0] * 4, self, "_apply_peak_to_peak") 398 self._fixed_voltage = ChannelSettings( 399 "fixed_voltage", [5.0] * 4, self, "_apply_fixed_voltage") 400 self._mean_voltage = ChannelSettings( 401 "mean_voltage", [0.0] * 4, self, "_apply_mean_voltage") 402 self._resting_voltage = ChannelSettings( 403 "resting_voltage", [0.0] * 4, self, "_apply_resting_voltage") 404 self._play_duration = ChannelSettings( 405 "play_duration", [1.0] * 4, self, "_apply_play_duration") 406 self._on_ramp_duration = ChannelSettings( 407 "on_ramp_duration", [0.0] * 4, self, "_apply_on_ramp_duration") 408 self._off_ramp_duration = ChannelSettings( 409 "off_ramp_duration", [0.0] * 4, self, "_apply_off_ramp_duration") 410 self._trigger_mode = ChannelSettings( 411 "trigger_mode", ["Normal"] * 2, self, "_apply_trigger_mode") 412 self._link_trigger_channel1 = ChannelSettings( 413 "link_trigger_channel1", [False] * 4, self, 414 "_apply_trigger_channel1_links") 415 self._link_trigger_channel2 = ChannelSettings( 416 "link_trigger_channel2", [False] * 4, self, 417 "_apply_trigger_channel2_links") 418 419 port_name = _common.port_name_from(port_name, serial_number, "SynthPalDevice") 420 self.port = serial.Serial( 421 port_name, 422 self._BAUD_RATE, 423 timeout=timeout, 424 rtscts=True, 425 # Windows opens a port for one program only. Linux and macOS let a second program open it too, and two 426 # sessions' bytes would then interleave; exclusive locks it (flock), so a second session that also locks 427 # it, such as another of these classes, gets an error instead 428 exclusive=True, 429 ) 430 self._closed = False 431 try: 432 # Discard anything left in the buffer by an earlier session 433 self.port.reset_input_buffer() 434 self._handshake() 435 self._read_hardware_info() 436 # Client name op + "PYTHON" in ASCII, shown as "PYTHON Connected" 437 self._write_command(self._OP_SET_CLIENT_NAME, b"PYTHON") 438 self.stop() 439 self.set_default_params() 440 except BaseException: 441 # Op 81 means something else to other devices, so it is sent 442 # only once the device has identified itself as a Synth Pal 443 self._close(send_disconnect=self.info.firmware_version is not None) 444 raise
Open a connection to a Synth Pal.
Arguments:
- port_name: USB serial port of the device, such as
COM3on Windows or/dev/ttyACM0on Linux. - serial_number: Instead of
port_name, the device's USB serial number (DeviceInfo.serial_number), which finds its port wherever it is plugged in. - timeout: Serial read timeout, in seconds.
Firmware newer than this module knows is used with a warning: new firmware only adds commands. Update the package to use what is new.
Raises:
- PulsePalError: If the device does not reply to the handshake, or runs other firmware.
- serial.SerialException: If the serial port cannot be opened, e.g. because another program has it open (on Linux and macOS, another session of these classes, which lock it).
446 @staticmethod 447 def serialportlist(ports_to_list="available"): 448 """Return the names of the USB serial ports on this computer. 449 450 Called on the class, without connecting to a device, to find the 451 port name to pass to `SynthPalDevice`. 452 453 Args: 454 ports_to_list: `available` to list only the ports that are 455 not already in use, or `all` to list every USB serial 456 port. Not case sensitive. 457 458 Returns: 459 Sorted list of port names, such as `["COM3", "COM7"]`. 460 461 Raises: 462 PulsePalError: If `ports_to_list` is not `available` or `all`. 463 """ 464 return _common.serialportlist(ports_to_list)
Return the names of the USB serial ports on this computer.
Called on the class, without connecting to a device, to find the
port name to pass to SynthPalDevice.
Arguments:
- ports_to_list:
availableto list only the ports that are not already in use, orallto list every USB serial port. Not case sensitive.
Returns:
Sorted list of port names, such as
["COM3", "COM7"].
Raises:
- PulsePalError: If
ports_to_listis notavailableorall.
470 def set_default_params(self): 471 """Program the default settings on the device. 472 473 The defaults are a frequency of 100 Hz, and on every output 474 channel a sine wave of 5 V peak to peak around a mean voltage of 475 0 V, resting at 0 V, played for 1 second with no ramps, and a 476 fixed voltage of 5 V for when the waveform is "Fixed Voltage". 477 Both trigger channels are in normal mode, and no output channel is 478 linked to a trigger channel, so a TTL starts nothing until a script 479 links it. They match the settings the device starts with. They are sent at once, 480 also while `SynthPalDevice.auto_sync` is off. 481 """ 482 auto_sync = self._auto_sync 483 self._auto_sync = True 484 try: 485 self.frequency = 100 486 # In this order, each is valid whatever the device holds: a mean of 0 V goes with any 487 # amplitude, 5 V is then a valid amplitude for any waveform, and a sine wave is then valid 488 self._send_levels_in_order((("M", [0] * 4), ("A", [5_000_000] * 4), ("W", ["Sine"] * 4)), 489 force=True) 490 for settings, value in ((self._waveform, "Sine"), (self._peak_to_peak, 5.0), 491 (self._fixed_voltage, 5.0), (self._mean_voltage, 0.0)): 492 settings._store([value] * 4) 493 self.resting_voltage = [0] * 4 494 self.play_duration = [1] * 4 495 self.on_ramp_duration = [0] * 4 496 self.off_ramp_duration = [0] * 4 497 self.trigger_mode = ["Normal"] * 2 498 self._set_trigger_links([False] * 4, [False] * 4) 499 self._unsent = frozenset() # Every setting has been sent 500 finally: 501 self._auto_sync = auto_sync
Program the default settings on the device.
The defaults are a frequency of 100 Hz, and on every output
channel a sine wave of 5 V peak to peak around a mean voltage of
0 V, resting at 0 V, played for 1 second with no ramps, and a
fixed voltage of 5 V for when the waveform is "Fixed Voltage".
Both trigger channels are in normal mode, and no output channel is
linked to a trigger channel, so a TTL starts nothing until a script
links it. They match the settings the device starts with. They are sent at once,
also while SynthPalDevice.auto_sync is off.
503 @property 504 def frequency(self): 505 """Frequency of all output channels, in Hz. 506 507 1 to 20000 Hz, rounded to 0.01 Hz. It can be changed during 508 playback: playing channels carry on from the same point in their 509 cycle, at the new frequency, and keep the time they have left to 510 play. The frequency played is exact: the device's sample clock is a 511 whole multiple of it (see `SynthPalDevice.sampling_rate`). For 512 `"White Noise"` it sets only the sampling rate. 513 """ 514 return self._frequency
Frequency of all output channels, in Hz.
1 to 20000 Hz, rounded to 0.01 Hz. It can be changed during
playback: playing channels carry on from the same point in their
cycle, at the new frequency, and keep the time they have left to
play. The frequency played is exact: the device's sample clock is a
whole multiple of it (see SynthPalDevice.sampling_rate). For
"White Noise" it sets only the sampling rate.
544 @property 545 def auto_sync(self): 546 """Whether assigning a setting programs the device at once. 547 548 `True` (the default): each assignment, such as 549 `S.peak_to_peak[1] = 2`, programs the device at once, as the 550 device's own op for that setting does, also while a trigger 551 channel is in param sync mode. 552 553 `False`: assignments change only this object's copy of the settings, 554 checking each value; `SynthPalDevice.sync_to_device` then sends all 555 of them at once. Use it to store the next trial's settings for a 556 param sync edge (see `SynthPalDevice.trigger_mode`), or to change 557 several settings in one command. Turning it back on does not send 558 changes made meanwhile, so it raises `pulsepal.PulsePalError` while 559 there are any: call `SynthPalDevice.sync_to_device` first to send 560 them, or `SynthPalDevice.sync_from_device` to discard them. A 561 `SynthPalDevice.batch` block does it all for you. `close()` warns of 562 changes never sent. `trigger`, `stop`, `status` and 563 `set_default_params` act at once either way. 564 """ 565 return self._auto_sync
Whether assigning a setting programs the device at once.
True (the default): each assignment, such as
S.peak_to_peak[1] = 2, programs the device at once, as the
device's own op for that setting does, also while a trigger
channel is in param sync mode.
False: assignments change only this object's copy of the settings,
checking each value; SynthPalDevice.sync_to_device then sends all
of them at once. Use it to store the next trial's settings for a
param sync edge (see SynthPalDevice.trigger_mode), or to change
several settings in one command. Turning it back on does not send
changes made meanwhile, so it raises pulsepal.PulsePalError while
there are any: call SynthPalDevice.sync_to_device first to send
them, or SynthPalDevice.sync_from_device to discard them. A
SynthPalDevice.batch block does it all for you. close() warns of
changes never sent. trigger, stop, status and
set_default_params act at once either way.
578 def batch(self): 579 """Change several settings, and send them in one command. 580 581 Returns a context manager. In its block, assignments change only 582 this object's copy of the settings (as with 583 `SynthPalDevice.auto_sync` off), and when the block ends, 584 `SynthPalDevice.sync_to_device` sends all of them at once. If the 585 block raises an error, nothing is sent, and the settings go back 586 to what they were. 587 588 ```python 589 with S.batch(): 590 S.frequency = 880 591 S.waveform = ["Triangle"] * 4 592 S.peak_to_peak = [2, 4, 6, 8] 593 ``` 594 595 In param sync mode, the device stores the settings for the next 596 rising edge on the param sync channel (see 597 `SynthPalDevice.trigger_mode`). 598 """ 599 return _common.batch(self)
Change several settings, and send them in one command.
Returns a context manager. In its block, assignments change only
this object's copy of the settings (as with
SynthPalDevice.auto_sync off), and when the block ends,
SynthPalDevice.sync_to_device sends all of them at once. If the
block raises an error, nothing is sent, and the settings go back
to what they were.
with S.batch():
S.frequency = 880
S.waveform = ["Triangle"] * 4
S.peak_to_peak = [2, 4, 6, 8]
In param sync mode, the device stores the settings for the next
rising edge on the param sync channel (see
SynthPalDevice.trigger_mode).
601 def sync_from_device(self): 602 """Read every setting from the device into this object. 603 604 Use it after settings were changed with the device's joystick, for 605 example. The device holds one amplitude per channel: a Fixed 606 Voltage channel's goes to `SynthPalDevice.fixed_voltage`, and any 607 other channel's to `SynthPalDevice.peak_to_peak`. This object keeps 608 the other one, reduced if need be to suit the mean voltage read 609 back. In param sync mode, it reads the settings the device plays 610 now, not a set waiting for the next edge. Changes made with 611 `SynthPalDevice.auto_sync` off and not yet sent are discarded. 612 613 Raises: 614 PulsePalError: If the device does not reply. 615 """ 616 self._write_command(self._OP_GET_ALL_SETTINGS) 617 values = struct.unpack(self._SETTINGS_FORMAT, self._read_raw(struct.calcsize(self._SETTINGS_FORMAT))) 618 centihz = values[0] 619 waveforms = [WAVEFORMS[code] for code in values[1:5]] 620 amplitudes, means = list(values[5:9]), list(values[9:13]) 621 peak_to_peak, fixed_voltage = list(self._peak_to_peak)[1:], list(self._fixed_voltage)[1:] 622 for i, (name, amplitude, mean) in enumerate(zip(waveforms, amplitudes, means)): 623 if name == FIXED_VOLTAGE: 624 fixed_voltage[i] = amplitude / 1e6 625 # This object's own peak to peak voltage must still suit the mean voltage (see _check_levels()) 626 peak_to_peak[i] = min(peak_to_peak[i], (2 * _MAX_VOLTAGE_UV - 2 * abs(mean)) / 1e6) 627 else: 628 peak_to_peak[i] = amplitude / 1e6 629 self._frequency = centihz / 100 630 self._samples_per_cycle = _samples_per_cycle(centihz) 631 self._waveform._store(waveforms) 632 self._peak_to_peak._store(peak_to_peak) 633 self._fixed_voltage._store(fixed_voltage) 634 self._mean_voltage._store([mean / 1e6 for mean in means]) 635 self._resting_voltage._store([rest / 1e6 for rest in values[13:17]]) 636 self._play_duration._store([d / 1e6 for d in values[17:21]]) 637 self._on_ramp_duration._store([d / 1e6 for d in values[21:25]]) 638 self._off_ramp_duration._store([d / 1e6 for d in values[25:29]]) 639 self._link_trigger_channel1._store([bool(link) for link in values[29:33]]) 640 self._link_trigger_channel2._store([bool(link) for link in values[33:37]]) 641 self._trigger_mode._store([TRIGGER_MODES[code] for code in values[37:39]]) 642 self._device_waveform, self._device_amplitude_uv, self._device_mean_uv = waveforms, amplitudes, means 643 self._unsent = frozenset() # Every setting is read
Read every setting from the device into this object.
Use it after settings were changed with the device's joystick, for
example. The device holds one amplitude per channel: a Fixed
Voltage channel's goes to SynthPalDevice.fixed_voltage, and any
other channel's to SynthPalDevice.peak_to_peak. This object keeps
the other one, reduced if need be to suit the mean voltage read
back. In param sync mode, it reads the settings the device plays
now, not a set waiting for the next edge. Changes made with
SynthPalDevice.auto_sync off and not yet sent are discarded.
Raises:
- PulsePalError: If the device does not reply.
645 def export_params(self): 646 """Return every setting, as a dict of plain values and lists. 647 648 Keyed by setting name: `"frequency"`, then the channel settings, 649 each with one value per channel and no unused index 0. It holds 650 only numbers, booleans and names, so it can be saved with `json` 651 and logged with your data, to record exactly what the device 652 played. 653 654 ```python 655 import json 656 with open("trial_settings.json", "w") as f: 657 json.dump(S.export_params(), f) 658 ``` 659 """ 660 params = {"frequency": self._frequency} 661 params.update({name: list(getattr(self, name))[1:] for name in _CHANNEL_SETTINGS}) 662 return params
Return every setting, as a dict of plain values and lists.
Keyed by setting name: "frequency", then the channel settings,
each with one value per channel and no unused index 0. It holds
only numbers, booleans and names, so it can be saved with json
and logged with your data, to record exactly what the device
played.
import json
with open("trial_settings.json", "w") as f:
json.dump(S.export_params(), f)
664 def import_params(self, params): 665 """Program the device with settings exported by `SynthPalDevice.export_params`. 666 667 All of them are checked first, and then sent in one command, as 668 at the end of a `SynthPalDevice.batch` block: in param sync mode, 669 the device stores them for the next sync edge. The levels 670 (`waveform`, `peak_to_peak`, `fixed_voltage`, `mean_voltage`) are 671 checked together, so their order in `params` does not matter. 672 Settings missing from `params` keep their values. A name that is 673 not a setting is skipped with a warning, so that settings exported 674 by a newer version of this package, which may have more, can still 675 be imported. 676 677 ```python 678 import json 679 with open("trial_settings.json") as f: 680 S.import_params(json.load(f)) 681 ``` 682 683 Args: 684 params: A dict of setting name to its value: a number for 685 `"frequency"`, and one value per channel (a list, tuple or 686 NumPy array) for the others, as `export_params` returns. 687 688 Raises: 689 PulsePalError: If a value is invalid. Nothing is sent, and the 690 settings keep the values they had. 691 """ 692 names = {"frequency", *_CHANNEL_SETTINGS} 693 unknown = [name for name in params if name not in names] 694 if unknown: 695 warnings.warn(f"import_params(): skipped {', '.join(map(repr, unknown))}, which this version of " 696 "SynthPalDevice does not have.", stacklevel=2) 697 levels = ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage") 698 with self.batch(): 699 for name, values in params.items(): 700 if name in levels: 701 # Each is checked alone here, and the levels together when the block ends (_check_sync()): 702 # an assignment would check each against the others' old values 703 getattr(self, name)._store( 704 self._normalize(name, _common.one_value_per_channel(name, values, 4))) 705 elif name in names: 706 setattr(self, name, values)
Program the device with settings exported by SynthPalDevice.export_params.
All of them are checked first, and then sent in one command, as
at the end of a SynthPalDevice.batch block: in param sync mode,
the device stores them for the next sync edge. The levels
(waveform, peak_to_peak, fixed_voltage, mean_voltage) are
checked together, so their order in params does not matter.
Settings missing from params keep their values. A name that is
not a setting is skipped with a warning, so that settings exported
by a newer version of this package, which may have more, can still
be imported.
import json
with open("trial_settings.json") as f:
S.import_params(json.load(f))
Arguments:
- params: A dict of setting name to its value: a number for
"frequency", and one value per channel (a list, tuple or NumPy array) for the others, asexport_paramsreturns.
Raises:
- PulsePalError: If a value is invalid. Nothing is sent, and the settings keep the values they had.
708 def sync_to_device(self): 709 """Send every setting to the device in one command. 710 711 While a trigger channel is in param sync mode, the device stores 712 the set, replacing any set stored before, and that channel's next 713 rising edge loads it (see `SynthPalDevice.trigger_mode`). Otherwise 714 the device applies it at once. The settings sent are this object's: 715 `frequency`, and for each channel `waveform`, `peak_to_peak` or 716 `fixed_voltage`, `mean_voltage`, `resting_voltage`, 717 `play_duration`, `on_ramp_duration`, `off_ramp_duration`, 718 `link_trigger_channel1` and `link_trigger_channel2`, and 719 `trigger_mode`. They read as sent, including while a set waits for 720 its edge. 721 722 Raises: 723 PulsePalError: If the device rejects the set. 724 """ 725 self._check_sync() 726 self._send_sync()
Send every setting to the device in one command.
While a trigger channel is in param sync mode, the device stores
the set, replacing any set stored before, and that channel's next
rising edge loads it (see SynthPalDevice.trigger_mode). Otherwise
the device applies it at once. The settings sent are this object's:
frequency, and for each channel waveform, peak_to_peak or
fixed_voltage, mean_voltage, resting_voltage,
play_duration, on_ramp_duration, off_ramp_duration,
link_trigger_channel1 and link_trigger_channel2, and
trigger_mode. They read as sent, including while a set waits for
its edge.
Raises:
- PulsePalError: If the device rejects the set.
728 def configure(self, channels, *, waveform=None, peak_to_peak=None, fixed_voltage=None, 729 mean_voltage=None, resting_voltage=None, play_duration=None, 730 on_ramp_duration=None, off_ramp_duration=None): 731 """Set several of the output channels' settings at once. 732 733 Settings left out keep their values. The new settings are checked 734 together, so their order does not matter: for example, a channel 735 whose sine wave swings 2 V around a mean voltage of 9 V can go 736 straight to 20 V peak to peak around 0 V. With 737 `SynthPalDevice.auto_sync` on, the class programs the device in an 738 order it accepts at every step. 739 740 ```python 741 S.configure(1, waveform="Sine", peak_to_peak=20, mean_voltage=0) 742 S.configure([2, 3], waveform="Fixed Voltage", fixed_voltage=-2.5, 743 play_duration=0.1) 744 ``` 745 746 Args: 747 channels: Output channel number 1-4, or several as a list, 748 tuple or NumPy array. 749 waveform, peak_to_peak, fixed_voltage, mean_voltage, 750 resting_voltage, play_duration, on_ramp_duration, 751 off_ramp_duration: New values, as for the settings of the 752 same names: one value for every channel given, or a list 753 with one value per channel given, in the same order. 754 755 Raises: 756 PulsePalError: If a value is out of range, or the settings do 757 not go together (nothing is sent), or the device rejects 758 them. 759 """ 760 channels = _common.channel_numbers(channels) 761 if len(set(channels)) != len(channels): 762 raise PulsePalError(f"configure(): channels must be distinct. Received {channels!r}.") 763 requested = {"waveform": waveform, "peak_to_peak": peak_to_peak, "fixed_voltage": fixed_voltage, 764 "mean_voltage": mean_voltage, "resting_voltage": resting_voltage, 765 "play_duration": play_duration, "on_ramp_duration": on_ramp_duration, 766 "off_ramp_duration": off_ramp_duration} 767 new_values = {} 768 for name, value in requested.items(): 769 if value is None: 770 continue 771 values = list(value) if isinstance(value, (list, tuple, np.ndarray)) else [value] 772 if len(values) == 1: 773 values = values * len(channels) 774 if len(values) != len(channels): 775 raise PulsePalError( 776 f"configure(): {name} has {len(values)} values for {len(channels)} channels. Give one " 777 "value, or one value per channel." 778 ) 779 settings = list(getattr(self, name))[1:] 780 for channel, item in zip(channels, values): 781 settings[channel - 1] = item 782 new_values[name] = self._normalize(name, settings) 783 levels = {name: new_values.get(name, list(getattr(self, name))[1:]) 784 for name in ("waveform", "peak_to_peak", "fixed_voltage", "mean_voltage")} 785 self._check_levels(levels["peak_to_peak"], levels["mean_voltage"], "configure") 786 if self._auto_sync: 787 self._send_levels(*self._device_levels(**levels)) 788 else: 789 self._unsent = self._unsent | set(new_values) # Until sync_to_device() sends them (see auto_sync) 790 for name in levels: 791 if name in new_values: 792 getattr(self, name)._store(new_values[name]) 793 ops = {"resting_voltage": self._OP_SET_RESTING_VOLTAGE, "play_duration": self._OP_SET_PLAY_DURATION, 794 "on_ramp_duration": self._OP_SET_ON_RAMP_DURATION, 795 "off_ramp_duration": self._OP_SET_OFF_RAMP_DURATION} 796 for name, op in ops.items(): 797 if name in new_values: 798 if self._auto_sync: 799 self._send_setting(name, op, new_values[name]) 800 getattr(self, name)._store(new_values[name])
Set several of the output channels' settings at once.
Settings left out keep their values. The new settings are checked
together, so their order does not matter: for example, a channel
whose sine wave swings 2 V around a mean voltage of 9 V can go
straight to 20 V peak to peak around 0 V. With
SynthPalDevice.auto_sync on, the class programs the device in an
order it accepts at every step.
S.configure(1, waveform="Sine", peak_to_peak=20, mean_voltage=0)
S.configure([2, 3], waveform="Fixed Voltage", fixed_voltage=-2.5,
play_duration=0.1)
Arguments:
- channels: Output channel number 1-4, or several as a list, tuple or NumPy array.
- waveform, peak_to_peak, fixed_voltage, mean_voltage, resting_voltage, play_duration, on_ramp_duration, off_ramp_duration: New values, as for the settings of the same names: one value for every channel given, or a list with one value per channel given, in the same order.
Raises:
- PulsePalError: If a value is out of range, or the settings do not go together (nothing is sent), or the device rejects them.
802 @property 803 def samples_per_cycle(self): 804 """Samples in one cycle of the waveform, at `frequency`. 805 806 The largest multiple of 4 whose sampling rate is at most 807 `DeviceInfo.max_sampling_rate` (100 kHz), so that a sample falls 808 on every edge, peak and trough of every waveform. For example, 809 332 at 300 Hz and 4 at 20 kHz. 810 """ 811 return self._samples_per_cycle
Samples in one cycle of the waveform, at frequency.
The largest multiple of 4 whose sampling rate is at most
DeviceInfo.max_sampling_rate (100 kHz), so that a sample falls
on every edge, peak and trough of every waveform. For example,
332 at 300 Hz and 4 at 20 kHz.
813 @property 814 def sampling_rate(self): 815 """The rate at which the device plays samples, in Hz. 816 817 `samples_per_cycle` times `frequency`: 100 kHz or just below it, 818 and at least 50 kHz. For example, 100 kHz at 100 Hz, 99.6 kHz at 819 300 Hz, and 80 kHz at 20 kHz. The device's timer cannot divide 820 every rate exactly, so sample periods differ by up to one tick of 821 its 24 MHz clock (42 ns), arranged so that the frequency is exact. 822 """ 823 return self._samples_per_cycle * self._frequency
The rate at which the device plays samples, in Hz.
samples_per_cycle times frequency: 100 kHz or just below it,
and at least 50 kHz. For example, 100 kHz at 100 Hz, 99.6 kHz at
300 Hz, and 80 kHz at 20 kHz. The device's timer cannot divide
every rate exactly, so sample periods differ by up to one tick of
its 24 MHz clock (42 ns), arranged so that the frequency is exact.
825 @property 826 def waveform(self): 827 """The waveform of each output channel. 828 829 Indexed by channel number (see "Channel settings" above). Each 830 cycle starts at the trigger: 831 832 - `"Sine"` and `"Triangle"`: start at the mean voltage, rising. 833 - `"Square"`: high for the first half of each cycle, then low. 834 - `"Sawtooth"`: rises from its lowest voltage to its highest, then 835 falls back at the end of the cycle. 836 - `"Fixed Voltage"`: steps to the channel's 837 `SynthPalDevice.fixed_voltage` for the play duration, and 838 returns to the resting voltage. It is not periodic, so the 839 frequency does not change it. 840 - `"White Noise"`: a new random voltage on every sample, uniform 841 over the peak to peak voltage around the mean voltage. It is 842 rendered at the sampling rate the frequency sets 843 (`SynthPalDevice.sampling_rate`), so no claim is made about its 844 spectrum. Each channel has its own random sequence, which carries 845 on from one playback to the next: every playback is new noise. 846 The sequences start from the same point when the device powers 847 up. 848 849 The periodic waveforms and white noise swing 850 `SynthPalDevice.peak_to_peak` around `SynthPalDevice.mean_voltage`. 851 Names are not case sensitive. A change applies to playback in 852 progress. 853 """ 854 return self._waveform
The waveform of each output channel.
Indexed by channel number (see "Channel settings" above). Each cycle starts at the trigger:
"Sine"and"Triangle": start at the mean voltage, rising."Square": high for the first half of each cycle, then low."Sawtooth": rises from its lowest voltage to its highest, then falls back at the end of the cycle."Fixed Voltage": steps to the channel'sSynthPalDevice.fixed_voltagefor the play duration, and returns to the resting voltage. It is not periodic, so the frequency does not change it."White Noise": a new random voltage on every sample, uniform over the peak to peak voltage around the mean voltage. It is rendered at the sampling rate the frequency sets (SynthPalDevice.sampling_rate), so no claim is made about its spectrum. Each channel has its own random sequence, which carries on from one playback to the next: every playback is new noise. The sequences start from the same point when the device powers up.
The periodic waveforms and white noise swing
SynthPalDevice.peak_to_peak around SynthPalDevice.mean_voltage.
Names are not case sensitive. A change applies to playback in
progress.
860 @property 861 def peak_to_peak(self): 862 """The peak to peak voltage of each output channel's waveform, in volts. 863 864 Indexed by channel number (see "Channel settings" above). 0 to 865 20 V: the waveform swings `peak_to_peak / 2` above and below the 866 channel's `SynthPalDevice.mean_voltage`, so it must stay within 867 -10 V to 10 V: `abs(mean_voltage) + peak_to_peak / 2 <= 10`. To 868 raise it beyond what the mean voltage allows, change the mean 869 voltage first, or set both with `SynthPalDevice.configure`. 870 871 A sine wave of 4 V peak to peak swings from -2 V to 2 V around a 872 mean voltage of 0 V: it is 2 sin(2 pi f t). White noise of 4 V peak 873 to peak is uniform from -2 V to 2 V. A `"Fixed Voltage"` channel 874 keeps its peak to peak voltage for when it plays a periodic waveform 875 again. 876 877 Set to the nearest microvolt. A change applies to playback in 878 progress. 879 """ 880 return self._peak_to_peak
The peak to peak voltage of each output channel's waveform, in volts.
Indexed by channel number (see "Channel settings" above). 0 to
20 V: the waveform swings peak_to_peak / 2 above and below the
channel's SynthPalDevice.mean_voltage, so it must stay within
-10 V to 10 V: abs(mean_voltage) + peak_to_peak / 2 <= 10. To
raise it beyond what the mean voltage allows, change the mean
voltage first, or set both with SynthPalDevice.configure.
A sine wave of 4 V peak to peak swings from -2 V to 2 V around a
mean voltage of 0 V: it is 2 sin(2 pi f t). White noise of 4 V peak
to peak is uniform from -2 V to 2 V. A "Fixed Voltage" channel
keeps its peak to peak voltage for when it plays a periodic waveform
again.
Set to the nearest microvolt. A change applies to playback in progress.
886 @property 887 def fixed_voltage(self): 888 """The voltage each output channel steps to when its waveform is 889 `"Fixed Voltage"`, in volts. 890 891 Indexed by channel number (see "Channel settings" above). -10 to 892 10 V, with any resting voltage and mean voltage. A channel playing 893 another waveform keeps its fixed voltage for when its waveform is 894 `"Fixed Voltage"` again. 895 896 Set to the nearest microvolt. A change applies to playback in 897 progress. 898 """ 899 return self._fixed_voltage
The voltage each output channel steps to when its waveform is
"Fixed Voltage", in volts.
Indexed by channel number (see "Channel settings" above). -10 to
10 V, with any resting voltage and mean voltage. A channel playing
another waveform keeps its fixed voltage for when its waveform is
"Fixed Voltage" again.
Set to the nearest microvolt. A change applies to playback in progress.
905 @property 906 def mean_voltage(self): 907 """The mean voltage of each output channel's waveform, in volts. 908 909 Indexed by channel number (see "Channel settings" above). -10 to 910 10 V. A periodic waveform, or white noise, swings around it at full 911 amplitude, and must stay within -10 V to 10 V with the 912 `peak_to_peak` voltage: `abs(mean_voltage) + peak_to_peak / 2 <= 10`. 913 It may differ from the `resting_voltage`, which the channel 914 outputs between playbacks: the ramps move the mean between the two 915 (see "Ramps" above). A `"Fixed Voltage"` ignores it. Set to the 916 nearest microvolt. A change applies to playback in progress. 917 """ 918 return self._mean_voltage
The mean voltage of each output channel's waveform, in volts.
Indexed by channel number (see "Channel settings" above). -10 to
10 V. A periodic waveform, or white noise, swings around it at full
amplitude, and must stay within -10 V to 10 V with the
peak_to_peak voltage: abs(mean_voltage) + peak_to_peak / 2 <= 10.
It may differ from the resting_voltage, which the channel
outputs between playbacks: the ramps move the mean between the two
(see "Ramps" above). A "Fixed Voltage" ignores it. Set to the
nearest microvolt. A change applies to playback in progress.
924 @property 925 def resting_voltage(self): 926 """The resting voltage of each output channel, in volts. 927 928 Indexed by channel number (see "Channel settings" above). -10 to 929 10 V, with any waveform. The channel outputs it while idle, and 930 its ramps start and end there (see "Ramps" above). Set to the 931 nearest microvolt. A change applies to playback in progress. 932 """ 933 return self._resting_voltage
The resting voltage of each output channel, in volts.
Indexed by channel number (see "Channel settings" above). -10 to 10 V, with any waveform. The channel outputs it while idle, and its ramps start and end there (see "Ramps" above). Set to the nearest microvolt. A change applies to playback in progress.
939 @property 940 def play_duration(self): 941 """How long each output channel plays after a trigger, in seconds. 942 943 Indexed by channel number (see "Channel settings" above). Up to 944 `DeviceInfo.max_play_duration` (3600 s). `0` plays until the 945 channel is stopped. It counts at full amplitude, after the on 946 ramp. The channel stops when the duration has elapsed, part way 947 through a cycle if need be, and returns to its resting voltage, 948 over its off ramp. Durations are counted in samples, rounded to 949 the nearest one, and a nonzero duration lasts at least one sample. 950 A change applies to playback in progress. 951 """ 952 return self._play_duration
How long each output channel plays after a trigger, in seconds.
Indexed by channel number (see "Channel settings" above). Up to
DeviceInfo.max_play_duration (3600 s). 0 plays until the
channel is stopped. It counts at full amplitude, after the on
ramp. The channel stops when the duration has elapsed, part way
through a cycle if need be, and returns to its resting voltage,
over its off ramp. Durations are counted in samples, rounded to
the nearest one, and a nonzero duration lasts at least one sample.
A change applies to playback in progress.
958 @property 959 def on_ramp_duration(self): 960 """How long each output channel fades in after a trigger, in seconds. 961 962 Indexed by channel number (see "Channel settings" above). Up to 963 `DeviceInfo.max_play_duration` (3600 s); `0` (the default) for no 964 ramp. Over it, the peak to peak voltage rises linearly from 0 V to 965 `peak_to_peak`, and the mean from the `resting_voltage` to the 966 `mean_voltage`, before the play duration (see "Ramps" above). 967 Counted in samples, as `play_duration` is. A change applies to 968 playback in progress: a ramp under way keeps the level it has 969 reached, and goes on at the new rate. 970 """ 971 return self._on_ramp_duration
How long each output channel fades in after a trigger, in seconds.
Indexed by channel number (see "Channel settings" above). Up to
DeviceInfo.max_play_duration (3600 s); 0 (the default) for no
ramp. Over it, the peak to peak voltage rises linearly from 0 V to
peak_to_peak, and the mean from the resting_voltage to the
mean_voltage, before the play duration (see "Ramps" above).
Counted in samples, as play_duration is. A change applies to
playback in progress: a ramp under way keeps the level it has
reached, and goes on at the new rate.
977 @property 978 def off_ramp_duration(self): 979 """How long each output channel fades out when it stops, in seconds. 980 981 Indexed by channel number (see "Channel settings" above). Up to 982 `DeviceInfo.max_play_duration` (3600 s); `0` (the default) for no 983 ramp. Over it, the peak to peak voltage falls linearly to 0 V, and 984 the mean to the `resting_voltage`, after the play duration or a 985 stop (see "Ramps" above). Counted in samples, as `play_duration` 986 is. A change applies to playback in progress. 987 """ 988 return self._off_ramp_duration
How long each output channel fades out when it stops, in seconds.
Indexed by channel number (see "Channel settings" above). Up to
DeviceInfo.max_play_duration (3600 s); 0 (the default) for no
ramp. Over it, the peak to peak voltage falls linearly to 0 V, and
the mean to the resting_voltage, after the play duration or a
stop (see "Ramps" above). Counted in samples, as play_duration
is. A change applies to playback in progress.
994 @property 995 def trigger_mode(self): 996 """How each trigger channel acts on the output channels linked to it. 997 998 Indexed by trigger channel number, 1 or 2 (index 0 is unused). 999 These are Pulse Pal's trigger modes: 1000 1001 - `"Normal"`: a rising edge starts the linked channels. Channels 1002 that are playing ignore it. 1003 - `"Toggle"`: a rising edge starts the linked channels, or stops 1004 those that are playing. 1005 - `"Gated"`: a rising edge starts the linked channels, and a 1006 falling edge stops them, unless the other trigger channel is 1007 also gated, linked to them, and still high. With a play 1008 duration of 0, a channel plays for exactly as long as the TTL is 1009 high. 1010 - `"Param Sync"`: a rising edge starts and stops nothing (the 1011 channel's links are ignored). It loads the settings most recently 1012 sent by `SynthPalDevice.sync_to_device` (or at the end of a 1013 `SynthPalDevice.batch` block), if any. This is how the next 1014 trial's settings are sent during the current trial and applied 1015 the instant it starts. 1016 1017 At a param sync edge, the frequency and both trigger modes change 1018 at once: they are shared by all channels. An output channel at its 1019 resting voltage takes its new settings at once. One that is playing 1020 (off ramp included) finishes on the settings it started with, at 1021 the new frequency, and takes the new ones the moment it reaches its 1022 resting voltage, so the next trigger plays them. A trigger mode 1023 sent by `SynthPalDevice.sync_to_device` applies from the next 1024 edge. 1025 1026 Only `SynthPalDevice.sync_to_device` is held back for the edge: 1027 with `SynthPalDevice.auto_sync` on, every assignment programs the 1028 device at once, also in param sync mode. So leaving param sync mode 1029 means assigning `trigger_mode` with `auto_sync` on. When no trigger 1030 channel is left in the mode, the device discards a stored set. 1031 1032 To start channels on the same edge, wire the TTL to the other 1033 trigger channel too: the settings load first, so the channels it 1034 starts play the new settings. Connecting, and `set_default_params`, 1035 take both trigger channels out of param sync mode. 1036 1037 Names are not case sensitive. 1038 """ 1039 return self._trigger_mode
How each trigger channel acts on the output channels linked to it.
Indexed by trigger channel number, 1 or 2 (index 0 is unused). These are Pulse Pal's trigger modes:
"Normal": a rising edge starts the linked channels. Channels that are playing ignore it."Toggle": a rising edge starts the linked channels, or stops those that are playing."Gated": a rising edge starts the linked channels, and a falling edge stops them, unless the other trigger channel is also gated, linked to them, and still high. With a play duration of 0, a channel plays for exactly as long as the TTL is high."Param Sync": a rising edge starts and stops nothing (the channel's links are ignored). It loads the settings most recently sent bySynthPalDevice.sync_to_device(or at the end of aSynthPalDevice.batchblock), if any. This is how the next trial's settings are sent during the current trial and applied the instant it starts.
At a param sync edge, the frequency and both trigger modes change
at once: they are shared by all channels. An output channel at its
resting voltage takes its new settings at once. One that is playing
(off ramp included) finishes on the settings it started with, at
the new frequency, and takes the new ones the moment it reaches its
resting voltage, so the next trigger plays them. A trigger mode
sent by SynthPalDevice.sync_to_device applies from the next
edge.
Only SynthPalDevice.sync_to_device is held back for the edge:
with SynthPalDevice.auto_sync on, every assignment programs the
device at once, also in param sync mode. So leaving param sync mode
means assigning trigger_mode with auto_sync on. When no trigger
channel is left in the mode, the device discards a stored set.
To start channels on the same edge, wire the TTL to the other
trigger channel too: the settings load first, so the channels it
starts play the new settings. Connecting, and set_default_params,
take both trigger channels out of param sync mode.
Names are not case sensitive.
1045 @property 1046 def link_trigger_channel1(self): 1047 """Whether each output channel is triggered by trigger channel 1. 1048 1049 Indexed by channel number (see "Channel settings" above). By 1050 default no output channel is linked to either trigger channel, so 1051 a TTL starts nothing until a script links it. 1052 """ 1053 return self._link_trigger_channel1
Whether each output channel is triggered by trigger channel 1.
Indexed by channel number (see "Channel settings" above). By default no output channel is linked to either trigger channel, so a TTL starts nothing until a script links it.
1059 @property 1060 def link_trigger_channel2(self): 1061 """Whether each output channel is triggered by trigger channel 2. 1062 1063 Indexed by channel number (see "Channel settings" above). 1064 """ 1065 return self._link_trigger_channel2
Whether each output channel is triggered by trigger channel 2.
Indexed by channel number (see "Channel settings" above).
1071 def set_screen_saver(self, enabled, timeout=1800): 1072 """Switch the device's screen saver on or off, and set its timeout. 1073 1074 With the screen saver on, the device dims its screen once it has 1075 been left alone for `timeout` seconds: no command from a computer, 1076 no rising edge on a trigger channel, and no joystick click or 1077 push. The next of these brings the screen back. The device keeps 1078 both settings in its EEPROM, shared with Pulse Pal firmware, and 1079 saves them once no channel is playing. A new device starts with 1080 the screen saver on and 1800 s. 1081 1082 Args: 1083 enabled: `True` to switch the screen saver on, `False` to 1084 switch it off. 1085 timeout: Seconds without activity before the screen dims, 1 to 1086 65535. 1087 1088 Raises: 1089 PulsePalError: If a value is out of range. 1090 """ 1091 state = to_bool(enabled, "enabled") 1092 if isinstance(timeout, (bool, np.bool_)) \ 1093 or not isinstance(timeout, numbers.Integral) \ 1094 or not 1 <= timeout <= 65535: 1095 raise PulsePalError( 1096 "timeout must be a whole number of seconds from 1 to 65535. " 1097 f"Received {timeout!r}." 1098 ) 1099 self._write_command(self._OP_SET_SCREEN_SAVER, 1100 struct.pack("<BH", int(state), int(timeout))) 1101 self._read_ack("set_screen_saver()")
Switch the device's screen saver on or off, and set its timeout.
With the screen saver on, the device dims its screen once it has
been left alone for timeout seconds: no command from a computer,
no rising edge on a trigger channel, and no joystick click or
push. The next of these brings the screen back. The device keeps
both settings in its EEPROM, shared with Pulse Pal firmware, and
saves them once no channel is playing. A new device starts with
the screen saver on and 1800 s.
Arguments:
- enabled:
Trueto switch the screen saver on,Falseto switch it off. - timeout: Seconds without activity before the screen dims, 1 to 65535.
Raises:
- PulsePalError: If a value is out of range.
1107 def trigger(self, channels): 1108 """Trigger output channels in software. 1109 1110 Channels that are idle start from the beginning of their waveform 1111 cycle, together. Channels that are playing ignore it, as they 1112 ignore a trigger channel in normal mode. 1113 1114 ```python 1115 S.trigger(1) 1116 S.trigger([2, 4]) # a list, tuple or NumPy array 1117 ``` 1118 1119 Args: 1120 channels: Output channel number 1-4, or several as a list, 1121 tuple or NumPy array. 1122 """ 1123 self._write_command(self._OP_TRIGGER, bytes([_common.channel_bits(channels)]))
Trigger output channels in software.
Channels that are idle start from the beginning of their waveform cycle, together. Channels that are playing ignore it, as they ignore a trigger channel in normal mode.
S.trigger(1)
S.trigger([2, 4]) # a list, tuple or NumPy array
Arguments:
- channels: Output channel number 1-4, or several as a list, tuple or NumPy array.
1125 def stop(self, channels=None): 1126 """Stop playback. The stopped channels return to their resting 1127 voltage, over their off ramps. 1128 1129 Args: 1130 channels: Output channel number 1-4, or several as a list, 1131 tuple or NumPy array. `None` stops all channels. 1132 """ 1133 bits = self._ALL_CHANNELS if channels is None else _common.channel_bits(channels) 1134 self._write_command(self._OP_STOP, bytes([bits]))
Stop playback. The stopped channels return to their resting voltage, over their off ramps.
Arguments:
- channels: Output channel number 1-4, or several as a list,
tuple or NumPy array.
Nonestops all channels.
1136 def status(self): 1137 """Read the device's playback state. 1138 1139 Returns: 1140 A `DeviceStatus`, with the channels that are playing, the 1141 samples per cycle, each channel's output range, and timing 1142 figures. 1143 """ 1144 self._write_command(self._OP_GET_STATUS) 1145 values = struct.unpack( 1146 self._STATUS_FORMAT, 1147 self._read_raw(struct.calcsize(self._STATUS_FORMAT)), 1148 ) 1149 playing_bits = values[0] 1150 range_names = list(OUTPUT_RANGES) 1151 return DeviceStatus( 1152 playing=[ch for ch in range(1, 5) 1153 if playing_bits & (1 << (ch - 1))], 1154 samples_per_cycle=values[1], 1155 output_ranges=[None, *(range_names[i] if i < len(range_names) 1156 else None for i in values[2:6])], 1157 longest_interrupt_us=values[6] / 1000, 1158 late_updates=values[7], 1159 )
Read the device's playback state.
Returns:
A
DeviceStatus, with the channels that are playing, the samples per cycle, each channel's output range, and timing figures.
1177 def close(self): 1178 """Close the connection to the device. 1179 1180 The device stops playback on all channels, each over its off ramp, 1181 and shows its own name on its screen again, in place of "PYTHON 1182 Connected". It keeps its settings, so TTL triggers still play the 1183 channels. Safe to call more than once. Called automatically when 1184 leaving a `with` block and when the object is garbage collected, 1185 so playback also stops when the last reference to the object 1186 goes. 1187 1188 """ 1189 self._close(send_disconnect=True)
Close the connection to the device.
The device stops playback on all channels, each over its off ramp,
and shows its own name on its screen again, in place of "PYTHON
Connected". It keeps its settings, so TTL triggers still play the
channels. Safe to call more than once. Called automatically when
leaving a with block and when the object is garbage collected,
so playback also stops when the last reference to the object
goes.
52class PulsePalError(Exception): 53 """Raised when communication with the device, or a setting, fails. 54 55 All three device classes (`pulsepal.PulsePalDevice`, 56 `pulsepal.WavePalDevice` and `pulsepal.SynthPalDevice`) raise it, for 57 serial reads that time out, short serial writes, commands the device 58 rejects, and values it cannot play, which are refused before anything 59 is sent. 60 """
Raised when communication with the device, or a setting, fails.
All three device classes (pulsepal.PulsePalDevice,
pulsepal.WavePalDevice and pulsepal.SynthPalDevice) raise it, for
serial reads that time out, short serial writes, commands the device
rejects, and values it cannot play, which are refused before anything
is sent.
Version of this package.