Metadata-Version: 2.4
Name: pyTriggerSync
Version: 0.3
Summary: A python library/GUI.
Home-page: https://github.com/MicheleCotrufo/
Author: Michele Cotrufo
Author-email: michele.cotrufo@gmail.com
License: MIT
Description-Content-Type: text/markdown
Requires-Dist: pyserial
Dynamic: author
Dynamic: author-email
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: requires-dist
Dynamic: summary

# pyTriggerSync

`pyTriggerSync` is a Python library/GUI interface for a Teensy microcontroller running the `Send_Trigger_Synced_With_External_Trigger_OnlyRemoteControl` firmware. The Teensy generates an output trigger pulse synchronized to an external trigger signal received on its input pin, either continuously or in user-controlled bursts. The package is composed of two parts: a low-level driver for serial communication, and a high-level GUI written with PyQt5 that can be easily embedded into other GUIs.

The interface can work either as a stand-alone application, or as a module of [ergastirio](https://github.com/MicheleCotrufo/ergastirio).

## Table of Contents
 - [Installation](#installation)
 - [Usage via the low-level driver](#usage-via-the-low-level-driver)
   * [Creating a driver instance](#creating-a-driver-instance)
   * [Properties](#properties)
   * [Other attributes](#other-attributes)
   * [Methods](#methods)
   * [Examples](#examples)
 - [Usage as a stand-alone GUI interface](#usage-as-a-stand-alone-gui-interface)
 - [Embed the GUI within another GUI](#embed-the-gui-within-another-gui)


## Installation

Use the package manager pip to install,

```bash
pip install pyTriggerSync
```

This will install `pyTriggerSync` together with `pyserial`, which is required by the low-level driver. In order to use the GUI, it is also necessary to install

```bash
pip install abstract_instrument_interface>=0.10
pip install "PyQt5>=5.15.6"
```

## Usage via the low-level driver

`pyTriggerSync` provides a low-level driver to communicate with the Teensy device over a USB serial connection.

```python
from pyTriggerSync.driver import pyTriggerSync
device = pyTriggerSync()
available_devices = device.list_devices()
print(available_devices)
device.connect_device(port=available_devices[0].split(':')[0])
print(device.mode)
device.disconnect_device()
```

The method `list_devices()` scans all serial ports and returns a list of descriptor strings for compatible devices. Each string is in the form `"<port>: <description> [<hwid>]"`, e.g. `"COM4: USB Serial [USB VID:PID=16C0:0483 ...]"`. The port can be extracted from the first element by splitting on `':'`.

The class `pyTriggerSync` exposes several properties and methods to communicate with the device and read or change its settings. All properties and methods that communicate with the device require a connection to be active; they raise `RuntimeError` if called while disconnected.

### Creating a driver instance

```python
pyTriggerSync(port='COM4', baudrate=115200, timeout=0.1)
```

| Parameter | Type | Description |
| --- | --- | --- |
| `port` | str, optional | Default serial port to connect to. Used by `connect_device()` when no port is explicitly specified. Default is `'COM4'`. |
| `baudrate` | int, optional | Baud rate for the serial connection. Must match the firmware (115200). Default is `115200`. |
| `timeout` | float, optional | Read timeout in seconds for the serial connection. Default is `0.1`. |

### Properties

The following are implemented as Python `@property`, i.e. they are accessed without parentheses (e.g. `device.mode`) and, when settable, assigned with `=` (e.g. `device.mode = 1`). Reading or setting any of these requires a device to be connected, otherwise a `RuntimeError` is raised.

| Property | Type | Description | Can be set? |
| --- | --- | --- | --- |
| `identity` | str | The device's identity string (its response to `idn?`). | No |
| `mode` | int | Operating mode: `0` = Continuous Trigger (every qualifying input edge fires an output pulse), `1` = Trigger Controlled by User (output pulses only fire in response to `sendtrigger()`). | Yes â must be `0` or `1`; raises `ValueError` otherwise. |
| `polarity` | int | Output pulse polarity: `0` = Negative (output idles high, pulses low), `1` = Positive (output idles low, pulses high). | Yes â must be `0` or `1`; raises `ValueError` otherwise. |
| `delay` | int | Delay in nanoseconds between an input trigger edge and the generated output pulse. | Yes â must be a non-negative integer; raises `ValueError` otherwise. |
| `triggerduration` | int | Duration of the generated output pulse in nanoseconds. | Yes â must be a positive integer; raises `ValueError` otherwise. |
| `divider` | int | Sub-sampling divider: in Continuous Trigger mode, one output pulse is emitted for every `divider` input edges. `divider=1` passes every trigger through (no sub-sampling). | Yes â must be a positive integer; raises `ValueError` otherwise. |
| `number_of_triggers` | int | Number of output pulses produced by a single `sendtrigger()` call. Only meaningful in mode `1`. | Yes â must be a positive integer; raises `ValueError` otherwise. |

After setting a property, the driver reads the value back from the device to confirm it took effect. If the device does not acknowledge the command, or if the read-back value does not match the requested value, a `RuntimeError` is raised.

### Other attributes

These are plain instance attributes (not `@property`) that are useful to inspect directly.

| Attribute | Type | Description |
| --- | --- | --- |
| `connected` | bool | `True` if a device is currently connected, `False` otherwise. |
| `port` | str | Serial port used for the current (or most recent) connection. |
| `baudrate` | int | Baud rate configured for the connection. |
| `timeout` | float | Read timeout (in seconds) configured for the connection. |
| `identifier` | str | Substring that must appear at the start of a device's identity string for it to be recognised as compatible. Default is `'Send_Trigger_Synced_With_External_Trigger'`. |

### Methods

| Method | Returns | Description |
| --- | --- | --- |
| `list_devices()` | list of str | Scans all serial ports and returns a list of descriptor strings for compatible devices. Each string is in the form `"<port>: <description> [<hwid>]"`. Raises `RuntimeError` if a device is already connected. |
| `connect_device(port=None, baudrate=None, timeout=None)` | (str, int) | Attempts to connect to the device on the specified serial port. If any parameter is `None`, the corresponding instance attribute is used. On success, queries the device for all current settings and caches them. Returns `(message, 1)` on success or `(error, 0)` on failure. |
| `disconnect_device()` | (str, int) | Closes the serial connection to the currently connected device. Returns `(message, 1)` on success or `(error, 0)` on failure. |
| `check_valid_connection()` | None | Raises `RuntimeError` if no device is currently connected. Called internally by all properties and `sendtrigger()`. |
| `query(q)` | str | Sends the command string `q` to the device and returns its reply as a stripped string. Clears any stale data from the input buffer before writing. |
| `sendtrigger()` | bool | Sends a `trg` command to start a burst of output pulses. Only valid in mode `1`; raises `RuntimeError` in mode `0`. Returns `True` if the device acknowledged the command, `False` otherwise. Note: the acknowledgement confirms the burst was *armed*, not that it has completed â completion depends on input trigger edges arriving at the device and is reported asynchronously by the device itself (it sends `"01"` over serial when done). |

### Examples

```python
from pyTriggerSync.driver import pyTriggerSync

device = pyTriggerSync()

# Scan for available devices
available_devices = device.list_devices()
print(available_devices)
# e.g. ['COM4: USB Serial [USB VID:PID=16C0:0483 SER=123456 LOCATION=1-1]']

# Connect to the first available device
port = available_devices[0].split(':')[0]
device.connect_device(port=port)

# Read device identity
print(device.identity)

# Set mode to "Trigger Controlled by User"
device.mode = 1

# Set output polarity to Negative (idles high, pulses low)
device.polarity = 0

# Set a 500 ns delay and a 200 ns pulse duration
device.delay = 500
device.triggerduration = 200

# Configure a burst of 5 pulses and fire
device.number_of_triggers = 5
success = device.sendtrigger()
print(f"Trigger sent: {success}")

# Switch to Continuous Trigger mode with a divider of 2
# (fires one output pulse for every 2 input edges)
device.mode = 0
device.divider = 2

# Disconnect
device.disconnect_device()
```

## Usage as a stand-alone GUI interface

The installation sets up an entry point for the GUI. Just type

```bash
pyTriggerSync
```

in the command prompt to start the GUI.

## Embed the GUI within another GUI

The GUI can also be easily integrated within a larger graphical interface:

```python
import PyQt5.QtWidgets as Qt
import pyTriggerSync

app = Qt.QApplication([])
window = Qt.QWidget()

# The GUI must be contained inside a widget object
widget_containing_interface_GUI = Qt.QWidget()
widget_containing_interface_GUI.setStyleSheet(
    ".QWidget {
"
    "border: 1px solid black;
"
    "border-radius: 4px;
"
    "}"
)

# Create the interface (model) object
Interface = pyTriggerSync.interface(app=app)
Interface.verbose = False

# Signals emitted by the interface can be connected to external functions, e.g.:
#
#   Interface.sig_mode_changed.connect(my_function)
#
# my_function will be called with the new mode (0 or 1) every time the mode changes.
# Similarly, Interface.sig_external_trigger_changed, sig_polarity_changed, etc.
# can be used to react to any setting change.
#
# To fire a trigger from external code (e.g. in response to another instrument):
#
#   Interface.fire_trigger()

# Create the GUI (view + controller) and bind it to the interface
view = pyTriggerSync.gui(interface=Interface, parent=widget_containing_interface_GUI)

# Add additional GUI elements alongside the pyTriggerSync panel
gridlayoutwidget = Qt.QWidget()
gridlayout = Qt.QGridLayout()
gridlayout.addWidget(Qt.QLabel("Additional GUI 1"), 0, 0)
gridlayout.addWidget(Qt.QLabel("Additional GUI 2"), 1, 0)
gridlayoutwidget.setLayout(gridlayout)

layout = Qt.QVBoxLayout()
layout.addWidget(widget_containing_interface_GUI)
layout.addWidget(gridlayoutwidget)
layout.addStretch(1)
window.setLayout(layout)

app.aboutToQuit.connect(Interface.close)
window.show()
app.exec()
```
