Metadata-Version: 2.4
Name: trgenpy
Version: 1.0.4
Summary: ## A Python library for Trgen Device
Home-page: https://gitlab.com/b00leant/trgenpy
Author: Stefano Latini
Author-email: Stefano Latini <stefanoelatini@hotmail.it>
License: MIT License
        
        Copyright (c) 2025 Stefano Latini, CoSANLab
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
Project-URL: Homepage, https://gitlab.com/b00leant/trgenpy
Project-URL: Repository, https://gitlab.com/b00leant/trgenpy
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: author
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-python

# `trgenpy` 🐍🧠

## A Python library for Trgen Device

[![PyPI version](https://img.shields.io/pypi/v/trgenpy.svg)](https://pypi.org/project/trgenpy/)
[![Python Version](https://img.shields.io/pypi/pyversions/trgenpy?style=flat&logo=python&logoColor=white)](https://pypi.org/project/trgenpy/)
[![License](https://img.shields.io/pypi/l/trgenpy.svg)](https://pypi.org/project/trgenpy/)

The full documentation is available [here](https://b00leant.gitlab.io/trgenpy/).

---

## Index

- [📚 Getting started](#getting-started)
  - [How to install](#how-to-install)
  - [Architecture](#architecture)
  - [Client](#client)
  - [Sending Default Single Trigger](#sending-default-single-trigger)
  - [Custom Trigger(s)](#custom-triggers)
  - [Sending Markers](#sending-markers)
  - [BNC I/O Event Listening](#bnc-io-event-listening)
  - [GPIO Event Listening](#gpio-event-listening)
  - [Custom Event Listening](#custom-event-listening)
  - [Programming Custom Trigger Signals]
    - [TrgenPin List](#trgenpin-list)
    - [TrgenPort](#trgenport)
    - [Custom Instruction Set](#custom-instruction-set)
    - [Concatenate Instructions](#concatenate-instructions)
    - [Instruction Helpers](#instruction-helpers)
    - [Custom Event Listening](#custom-event-listening)
  - [Native Commands](#native-commands)
    - [Program TrgenPort](#program-trgenport)
    - [Start Trigger](#start-trigger)
    - [Set GPIO I/O Direction](#set-gpio-io-direction)
    - [Request Implementation](#request-implementation)
    - [Request TrgenPort Status](#request-trgenport-status)
    - [Set The Trigger Level](#set-the-trigger-level)
    - [Get The Trigger Level](#get-the-trigger-level)
    - [Get the GPIO I/O Direction](#get-the-gpio-io-direction)
    - [Stop the TrgenPort](#stop-the-trgenport)
- [🧠 E-Prime integration](#e-prime-integration)
- [🚀 How To build](#how-to-build)
- [💪 Examples](#examples)

---

## Getting started

### How to install

To install this package use is required `Python >= 3.9`

```shell
pip install trgenpy
```

### Architecture

As explained in this [Mermaid](https://github.com/mermaid-js/mermaid) diagram, it is possible to program n (0-25) TrgenPorts to the `TrgenClient`

```mermaid
graph TD
    A[TrgenClient]
    A --> Impl[TrgenImplementation]
    A --> B0[TrgenPort 0]
    A --> B1[TrgenPort 1]
    A --> BN[TrgenPort n]
    B0 --> P0[TrgenPin]
    B1 --> P1[TrgenPin]
    BN --> PN[TrgenPin]
    B0 --> M0[Memory]
    B1 --> M1[Memory]
    BN --> MN[Memory]
    M0 --> I0[Instruction 0]
    M0 --> I1[Instruction 1]
    M0 --> I2[Instruction n]
    M1 --> J0[Instruction 0]
    M1 --> J1[Instruction 1]
    M1 --> J2[Instruction n]
    MN --> K0[Instruction 0]
    MN --> K1[Instruction 1]
    MN --> K2[Instruction n]
```

The `client` variable can be used to launch each command inside TRGen device 

```python
client = TrgenClient() # now I can use this client for any purpose.
```

Each `TrgenPort`can be programmed with a bunch of instructions.
We can use the function `set_instruction()` in many memory slots.
Finally, we can use `set_trgen_memory` to a specific `TrgenPort` and execute the `TrgenClient` with `client.start()`

#### NOTE
Each TrgenPort can support a list of N instructions, where N = 2^MTML (Max TrgenPort Memory Length).
the Memory Length is a "magic number" hardcoded inside the triggerbox firmware. In the current versions is *`5`* 

You can access to the `mtml` value by

```python
# Add this in try block to manage errors
try:
    impl = client.get_implementation()
    mtml = impml.mtml
except InvalidAckError as e:
    print(f"⚠️ ACK sbagliato: {e}")
except AckFormatError as e:
    print(f"⚠️ ACK malformato: {e}")
except TimeoutError as e:
    print(f"⏱️ Timeout: {e}")
```


#### Import the library

```python
# you can import 
# library like this:
import trgenpy as tp
client = tp.TrgenClient()

# or like this:
from from trgenpy import TrgenClient,TrgenPin,active_for_us,unactive_for_us,end,wait_ne,wait_pe
client = TrgenClient()
```

### Client

The `TrgenClient` object stores all the information about the socket connection between the user PC and the TrGEN Device.  
The `TrgenClient` object may be istantiated like this:

```python
client = TrgenClient() 
```

Once the client object is created, connection between user PC and TrGEN Device is achieved with:

```python
client.connect()
```

It is also possible to check the availability of the device (useful to check if properly connected)

```python
isAavailable = client.is_available() # true / false
```

### Sending Default Single Trigger

To send a defaul trigger signal (positive square with lasting 20µs) the `sendTrigger()` function can be used:

```python
client.sendTrigger()
```

### Custom Trigger(s)

To send trigger to a customa single or multiple custom triggers at the same time (i.e., with nanosecond time resolution), the `sendCustomTrigger()` function can be used    :

```python
client.sendCustomTrigger([TrgenPin.NS0,TrgenPin.GPIO0])
```

In this example, `sendCustomTrigger` has one array as argument.
The array represent the list of the desired PINs (PIN 0 of the parallel port and PIN 0 of the GPIO, TrgenPin.NS0 and TrgenPin.GPIO0, respectively) to send simultaneous triggers.
This function can also be used to test whether signals on PINs are properly functioning.

### Sending Markers

To send a marker to bioamplifiers equipped with parallel ports used for triggering and register events, the function `sendMarker()` function can be used:

```python
client.sendMarker(13)
```

`sendMarker(n)` will send the appropriate triggers to generate the desired marker `n` on the electrophysiological signal from ALL the equipped output ports (Parallel port, GPIO) on the TRGen device.

### BNC I/O Event Listening 
Configures the BNC output to automatically respond to signals received on the BNC input connector (BNCI).

```python
from trgenpy import TrgenClient

client = TrgenClient()
client.connect()

# Respond to positive edge (default)
client.input_bnc_output()

# Respond to negative edge
client.input_bnc_output(ne=True)

# Start listening
client.start()
```

### GPIO Event Listening
Configures the GPIO connector (in "input direction" mode) to respond to signals received on a specific GPIO pin.

```python
# Configure GPIO2 as input before using it
gpio_config = DirectionConfig().input(TrgenPin.GPIO2).build()
client.set_gpio_direction(gpio_config)

# Respond to GPIO0 on positive edge (default)
client.input_gpio_trigger_tmso_behaviour()

# Respond to GPIO2 on negative edge
client.input_gpio_trigger_tmso_behaviour(ne=True, gpio_id=TrgenPin.GPIO2)

# Start listening
client.start()
```


#### Pin Binary Mapping

Both `sendMarker()` and `sendCustomTrigger()` use a binary mapping system where each pin corresponds to a specific bit position. This allows for efficient encoding of multiple pin states in a single value:

| Pin | Binary Position | Decimal Value | Binary Representation |
|-----|----------------|---------------|---------------------|
| NS0 | Bit 0 (2^0) | 1 | 00000001 |
| NS1 | Bit 1 (2^1) | 2 | 00000010 |
| NS2 | Bit 2 (2^2) | 4 | 00000100 |
| NS3 | Bit 3 (2^3) | 8 | 00001000 |
| NS4 | Bit 4 (2^4) | 16 | 00010000 |
| NS5 | Bit 5 (2^5) | 32 | 00100000 |
| NS6 | Bit 6 (2^6) | 64 | 01000000 |
| NS7 | Bit 7 (2^7) | 128 | 10000000 |

This suggests the pins might be mapped differently than the sequential naming suggests. Please verify the complete mapping to ensure accuracy.

**Examples based on your observation:**
- `sendMarker(1)` → activates only NS0
- `sendMarker(128)` → activates only NS6
- `sendMarker(129)` → activates NS0 + NS6 (binary: 10000001)

This same mapping applies to Synamps (SA0-SA7) and GPIO (GPIO0-GPIO7) pins when using their respective marker parameters.

If the marker number sent does not match with the one observed on the physiological signal, this is may due to an inverted mapping of the bioamplifier’s [*PINOUT*](https://en.wikipedia.org/wiki/Pinout). To fix the issue, also it is possible to invert the bit order, just flag the `LSB` argument (`True` by default)

```python
client.sendMarker(13,LSB=False)
```

To send different markers simultaneously (with nanosecond temporal precision) n different output ports, the following arguments may be used:

- `markerNS`
- `markerSA`
- `markerGPIO`

like this:

```python
client.sendMarker(markerNS=8,markerSA=2,markerGPIO=15)
```


In this way, a marker value of 8 will be generated from the parallel port, 2 on the second parallel port and 15 from the [*GPIO*](https://it.wikipedia.org/wiki/General_Purpose_Input/Output) port.


## Programming Custom Trigger Signals


### TrgenPin List

Each Pin on the TRGen Device has an unique ID.
This is the classification of each Port grouped by connector Type

- Neuroscan IDs
    The Neuroscan pinout (only for used pins) goes from 0 to 7  
    - `[NS0,NS1,NS2,NS3,NS4,NS5,NS6,NS7]`
- Synamps IDs
     The Neuroscan pinout (only for used pins) goes from 0 to 7  
    - `[SA0,SA1,SA2,SA3,SA4,SA5,SA6,SA7]`
- BNC I/O IDs
    - `BNCO`
    - `BNCI`
- GPIO IDs
    The GPIO pinout goes from 0 to 7  
    - `[GPIO0,GPIO01,GPIO02,GPIO03,GPIO04,GPIO05,GPIO06,GPIO07]`

The definition of a specific port is done by calling an enumeration through the `TrgenPin` class by doing `TrgenPin.$PIN_ID`, like this:

```python
# defining pin
neuroscan_third_pin = TrgenPin.NS3
```

### TrgenPort

The `TrgenPort` object defines a single trigger behaviours through its id. This is the list of supported  [`TrgenPin`](#trgenpin-list) values for the TRGrgen:

Instantiate the `TrgenPort` object with:

```python
neuroScan = TrgenClient.create_trgen(TrgenPin.NS3)
```

passing as only argument the enum from `TrgenPin` corresponding to the real used pin. 

### Custom Instruction Set

The `TrgenPort` object has a `memory` property, a list that can contain 32 slot.
Each slot can contain a single istruction that will be executed in order from the first to the last.

The supported instruction set for TRGen Device is:

- Unactive For N µs
- Active For N µs
- Wait Positive Edge
- Wait Negative Edge
- Repeat from N, for X times
- End
- Not Ammissible

### Concatenate Instructions

The memory list can be build by adding some istructions to it.

The static function `set_instruction(i, x)` can be used to add a specific instruction (`x`) in the desired program sequence position  (`i`)
```
def set_instruction(self, index, instruction):
# index: 0-32 value
# instruction: instruction_code
```

### Instruction Helpers

Since for TRGen Device all instruction are "bitmap chunks" this library offers some helper functions that do the bitmap encoding.
They can be used to define easily any instruction:

| Instruction | 1° Param | 2° Param | Description |
| ----------- | ----------- | ------------ | ----------- |
| `unactive_for_us(us)` | µ seconds duration | | Set the unactivation time for µ seconds |
| `active_for_us(us)` | µ seconds duration | | Set the activation time for µ seconds |
| `wait_pe(tr)` | TrgenPin | | Wait the positive edge for a specific [TrgenPin](#trgenpin-list) |
| `wait_ne(tr)` | TrgenPin | | Wait the negative edge for a specific [TrgenPin](#trgenpin-list) |
| `repeat(addr,time)` | Instruction address | Time of repeat | Set the activation time for µ seconds |
| `end()` | | | End the behaviour |
| `not_admissible()` | | | Empty instruction, it has to be placed after the `end()`|


#### Example

Here, and example of custom trigger on the previously defined PIN 4 of the parallel port, which will be active for 5 microseconds (HIGH status -1-) and unactive for 3 microseconds (LOW status -0-).


```python
# call the function directly on the same TrgenPort object
neuroScan.set_instruction(0, active_for_us(5))
neuroScan.set_instruction(1, unactive_for_us(3))
# ...
```

### Custom Event Listening
Configures a custom output pin to respond to a specific input pin choseon between GPIO 0-7 and BNCI

```python
# Basic configuration: NS0 responds to BNCI
client.input_trigger_custom_pin(TrgenPin.BNCI, TrgenPin.NS0)

# Advanced configuration: SA1 responds to GPIO3 on negative edge
# First configure GPIO3 as input
gpio_config = DirectionConfig().input(TrgenPin.GPIO3).build()
client.set_gpio_direction(gpio_config)

client.input_trigger_custom_pin(
    input_port_id=TrgenPin.GPIO3,
    output_port_id=TrgenPin.SA1,
    ne=True
)

# Configuration with custom instructions
custom_instructions = [
    wait_pe(TrgenPin.BNCI),      # Wait for positive edge on BNCI
    active_for_us(50),           # Active for 50µs
    unactive_for_us(10),         # Inactive for 10µs
    end()                        # End program
]

client.input_trigger_custom_pin(
    input_port_id=TrgenPin.BNCI,
    output_port_id=TrgenPin.NS0,
    instructions=custom_instructions
)

# Start listening
client.start()
```

#### NOTE
**Programming custom trigger does not include the automatic start.**
**It's always recommended to invoke the [*`start()`*](#start-trigger) function once your custom istruction set is written** 

---

## Native Commands 

TRGen devices can receive some commands, the full instruction set includes:

- Program TrgenPort
- Start TrgenPort
- Set GPIO I/O Direction
- Request Implementation parameters
- Request TrgenPort Status
- Set the TrgenPort level
- Get the TrgenPort Level
- Get GPIO I/O Direction
- Stop the TrgenPort

### Program TrgenPort

You can program a trigger behaviour in two ways:

1. **Default trigger** using `program_default_trigger()`  
   This sets a predefined impulse of duration (default 20µs).

```python
tr = client.create_trgen(TrgenPin.GPIO0)
client.program_default_trigger(tr, us=50)  # impulse of 50µs
```

In this way, all the successive triggers sent from GPIO0 PIN will have a duration of 50 microseconds

2. **Advanced programming** by manually setting instructions with `set_instruction()`

```python
tr = client.create_trgen(TrgenPin.NS1)
tr.set_instruction(0, active_for_us(10))
tr.set_instruction(1, unactive_for_us(5))
tr.set_instruction(2, repeat(0, 3))
tr.set_instruction(3, end())
client.set_trgen_memory(tr)
```

### Start Trigger

After programming triggers, you can start them with:

```python
client.start()
```

This command makes the Trgen execute all programmed triggers.  
Remember: you must have previously sent at least one trigger memory.

### Set GPIO I/O Direction

You can use this helper to build the complete or partial pinout map to set.

```python
# we choose to set just GPIO0 -> High and the GPIO1 -> Low
mask = DirectionConfig().out(TrgenPin.GPIO0).in(TrgenPin.GPIO1).build()
# once you have it, you can use it as argument in the set_level function
client.set_gpio_direction(mask)
```

### Request Implementation

You can get information about the hardware configuration (number of channels, memory length, etc.) with:

```python
impl = client.get_implementation()
print(impl.memory_length)  # max number of instructions per trigger
```

### Request TrgenPort Status

To check current status of triggers (active/inactive state):

```python
status = client.get_status()
print(status)
```

This returns an integer bitmask where each bit represents the state of a trigger pin.

### Set the Trigger level

It is possible to change the polarity (active-high or active-low) for any `TrgenPort`:
Use the helper funcion `set_level`like this in order to build the complete or partial pinout map to set.

```python
# we choose to set just GPIO0 -> High and the GPIO1 -> Low
mask = LevelConfig().high(TrgenPin.GPIO0, TrgenPin.BNCO).low(TrgenPin.GPIO1).build()
# once you have it, you can use it as argument in the set_level function
client.set_level(mask)
```

### Get the Trigger Level

To check trigger polarity:

```python
level = client.get_level()
print(bin(level))
```

### Get the GPIO I/O Direction

To retrieve the current GPIO configuration:

```python
gpio_state = client.get_gpio_direction()
print(bin(gpio_state))
```

### Stop the TrgenPort

To stop the TRGen device use the `stop()` function 

```python
gpio_state = client.stop()
```
---

## E-Prime integration

trgenpy offers a wrapper version to integrate its behaviour inside the [E-Prime](https://pstnet.com/products/e-prime/) software via Python COM-visible.

Install pywin32

```bash
pip install pywin32
```

Register the COM wrapper

```
python trgen_com.py --register
```

So that you can use in E-Prime like this

```vb
Dim tb
Set tb = CreateObject("Trgen.COM")

If tb.IsAvailable() Then
    tb.Connect
    tb.SendTrigger
    tb.SendMarker 13
Else
    MsgBox "Trgen not available!"
End If
```

### E-Prime example

See the `examples` directory for the complete examples
---

## How to build

trgenpy uses [setuptools](https://pypi.org/project/setuptools/) to have a much clear and minimal build process.

### Step 1 - install twine
`pip install --upgrade build twine`

### Step 2 - build
`python -m build`

### Step 3 - use it anywhere!
In the same directory (pwd) of this project, run:
`pip install .`

### Deploy
`twine upload --repository testpypi dist/*`


## Examples

These are some example extracted from the `example` directory in this repo.


### Default Single Trigger (ONLY BNCO)

```python
from trgenpy import TrgenClient,TrgenPin,active_for_us,unactive_for_us,end,wait_ne,wait_pe

# create the Client for the Trgen
client = TrgenClient() # eventually TrgenClient(ip="192.168.123.2")

client.connect()
client.sendTrigger()
```

### Custom Trigger
```python
from trgenpy import TrgenClient,TrgenPin,active_for_us,unactive_for_us,end,wait_ne,wait_pe

# create the Client for the Trgen
client = TrgenClient() # eventually TrgenClient(ip="192.168.123.2")

client.connect()
client.sendCustomTrigger([TrgenPin.NS0,TrgenPin.GPIO0])
```

### Custom Trigger
```python
from trgenpy import TrgenClient,TrgenPin,active_for_us,unactive_for_us,end,wait_ne,wait_pe
# create the Client for the Trgen
client = TrgenClient() # eventually TrgenClient(ip="192.168.123.2")

client.connect()

def set_tmso_up():
    # check if the device is online
    if client.is_available():
        print("Trgen is connected")

        # build a trigger
        bnco = client.create_trgen(TrgenPin.BNCO)
        bnco.set_instruction(0, active_for_us(5))
        bnco.set_instruction(1, unactive_for_us(3))
        bnco.set_instruction(2, end())
        # send the trigger
        client.set_trgen_memory(bnco)

        # start the sequence
        client.start()

        # stop the sequence
        client.stop()
        
set_tmso_up()
```
