Metadata-Version: 2.4
Name: trgenpy
Version: 1.1.3
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.7
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

TRGEn Device is the ultimate triggerbox with the easiest API ever.

[![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/).

---

## Features

<!-- start features -->

- **Nanosecond powered** --- every trgenpy features is tested with signals that perform into the nanosecond
- **Easy coding** --- code is clean and well documented for a simple use.
- **Open Source** --- feel free to read and spread the code, do your forks and share it! 
- **Customize** --- with carefully-designed sidebar navigation and inter-page links.
- **Configurazion Export/import** --- your TriggerBox session can be imported and exported on file where you want.

<!-- end features -->

---

## Index

- [📚 **Getting started**](#-getting-started)
    - [How to install](#how-to-install)
    - [Architecture](#architecture)
    - [Import the library](#import-the-library)
    - [Client](#client)
- [🏁 **How to use**](#-how-to-use)  
  - [Sending Default Single Trigger to BNCO](#sending-default-single-trigger-to-bnco)
  - [Send Trigger(s) to Multiple Ports](#send-triggers-to-multiple-ports)
  - [Sending Markers](#sending-markers)
- [🚀 **Programming Custom Trigger Signals**](#-programming-custom-trigger-signals)
  - [TrgenPin](#trgenpin)
  - [TrgenPort](#trgenport)
  - [Custom Instruction Set](#custom-instruction-set)
  - [Concatenate Instructions](#concatenate-instructions)
  - [Instruction Helpers](#instruction-helpers)
  - [Write Instruction on TRGenPort](#write-instruction-on-trgenport)
  - [Event Listening](#event-listening)
    - [Listen for Default Simple to BNCO](#listen-for-default-simple-to-bnco)
    - [Listen for Markers](#listen-for-markers)
    - [Listen for Custom Triggers](#listen-for-custom-triggers)
- [🧩 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)
- [🛟 Helpers](#helpers)
  - [Reset TrgenPort](#reset-trgenport)
  - [Get Default Duration](#get-default-duration)
  - [Set Default Duration](#set-default-duration)
  - [Get Implementation](#get-implementation)
- [⚙️ **Export/Import configuration**](#️-exportimport-configuration)
  - [Configuration Manager](#configuration-manager)
  - [Create Configuration](#create-configuration)
  - [Export Configuration](#export-configuration)
  - [Import Configuration](#import-configuration)
  - [Configuration file format](#configuration-file-format)
- [🧠 **E-Prime integration**](#-e-prime-integration)
- [💪 **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]
```

### 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,istrActiveFor,istrUnactiveFor,istrEnd,istrWaitNE,istrWaitPE
client = TrgenClient()
```

### Client

The [`TrgenClient`](https://trgenpy-90ed8a.gitlab.io/#trgenpy.connection.TrgenClient) object stores all the information about the socket connection between the user PC and the TrGEN Device. It may be istantiated like this:

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

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
```
---

## 🏁 How to use

Here's some basic features that let the use send signals out of the TRGen Device.
These functions are used to send a defaul trigger signal (positive square with lasting 20µs)

### Sending Default Single Trigger to BNCO

To send a default trigger to the BNCO the `sendTrigger()` function can be used:

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

### Send Trigger(s) to Multiple Ports

To send trigger to a customa single or multiple custom triggers at the same time (i.e., with nanosecond time resolution), the same `sendTrigger()` function can be used but with **one extra argument**: A list of type [`TrgenPin`](#trgenpin)

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

It send on TRGen Device two signals like these

```mermaid
gantt
    title Synchronized Triggers - Multiple Pins
    dateFormat X
    axisFormat %s µs
    
    section NS0 (NeuroScan)
    High (PE)    :active, 0, 20
    Low  (NE)     :20, 50
    
    section GPIO0
    High (PE)   :active, 0, 20
    Low  (NE)    :20, 50
    
```

In this example, `sendTrigger` 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.
See the [`TrgenPin`](https://trgenpy-90ed8a.gitlab.io/advanced/index.html#trgenpin) documentation.

### 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 with the following arguments:

- `markerNS` (default None)
- `markerSA` (default None)
- `markerGPIO` (default None)
- `LSB` (default False)
- `stop` (default True)

like this:

```python
client.sendMarker(markerNS=12,markerSA=139,markerGPIO=42)
```

`sendMarker(markerNS,markerSA,markerGPIO)` will send the appropriate triggers to generate the desired marker `n` on the electrophysiological signal from ALL the equipped output ports (NeuroScan, SynAmps, GPIO) on the TRGen device.

#### Pin Binary Mapping

`sendMarker(markerNS,markerSA,markerGPIO)` 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 |

**Example of mapping marker values:**
- `sendMarker(markerNS=1)` → activates only NS0 (binary: 00000001)
- `sendMarker(markerSA=128, markerNS=2)` → activates SA7 (binary: 10000000) and NS1 (binary: 00000010)
- `sendMarker(markerGPIO=131)` → activates GPIO0 + GPIO1 + GPIO7 (binary: 10000011)

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

#### LSB

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 (`False` by default)

```python
client.sendMarker(markerNS=13,LSB=True)
```

**Example of mapping marker values with LSB:**
- `sendMarker(markerNS=1,LSB=True)` → activates only NS0 (binary: 10000000)
- `sendMarker(markerSA=12,markerNS=2,LSB=True)` → activates SA0 (binary: 00000001) and NS6 (binary: 01000000)
- `sendMarker(markerGPIO=131,LSB=True)` → activates GPIO0 + GPIO6 + GPIO7 (binary: 11000001)


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.

#### Auto-Stop

If setted to False, the TRGen Device will continue the execution.

```python
client.sendMarker(markerSA=100,LSB=True,stop=False)
# it's the same of calling client.stop() right after.
```

## 🚀 Programming Custom Trigger Signals

[`trgenpy`](#trgenpy-) offers some advanced functions and features for users need to customize the output/input signal's behaviour.

### TrgenPin

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

This is the complete list of the TRGen Device's pinout:
- 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,GPIO1,GPIO2,GPIO3,GPIO4,GPIO5,GPIO6,GPIO7]`

The definition of a specific port is done by calling an enumeration through the [`TrgenPin`](https://trgenpy-90ed8a.gitlab.io/api.html#trgenpy.trgen_pin.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
# a NeuroScan port is defined passing 
# the TrgenPin.NS3 enum in the constructor argument
client = TrgenClient()
neuro_scan = client.createTrgenPort(TrgenPin.NS3)

# but it's also possible to store the pin variable
bnco_pin = TrgenPin.BNCO
bnco_port = client.createTrgenPort(bnco_pin)
```

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

### Custom Instruction Set

The `TrgenPort` object has a `memory` property.
`memory` has to be filled with a list of `2^mtml` slots, default cardinality is 32.
Each slot can contain a single instruction that will be executed in order from the first to the last.

[see the "**Concatenate Instructions**"" section](#concatenate-instructions) for further informations.

The supported instruction set for TRGen Device is:

- ##### Unactive For N µs
    The TrgenPort stays *unactive* for N µs

- ##### Active For N µs
    The TrgenPort stays *active* for N µs

- ##### Wait Positive Edge
    The TrgenPort waits until there is a positive edge

- ##### Wait Negative Edge
    The TrgenPort waits until there is a negative edge

- ##### Repeat from N, for X times

    The TrgenPort istrRepeat the instruction sequence from the index X to the current one, for X times 

- ##### End

    The TrgenPort istrEnd its behaviour
- ##### Not Admissible

    This instruction is ignored, but it is *necessary* to fill the empty instruction list.
    **NOTE: if the space is empty, it will be filled automatically by `trgenpy`** 

### Concatenate Instructions

Each `TrgenPort`can be programmed with a bunch of instructions.
the function `setInstruction()` can be use to store instructuons into memory slots.

Instructions can be added in memory slots using the static function `setInstruction(i, x)`, that adds a specific instruction (`x`) in the desired memory index  (`i`).

Index position is important since the program counter of TRGen Device execute each TrgenPort instructions from 0 to 2^mtml.

```
def setInstruction(self, index, instruction):
# index: 0-32 value
# instruction: instruction_code
```

Remember, the `setInstruction(index,instruction)` function **does not program** the `TrgenPort`, it just stores the instructon, that will be written using `writeTrgenMemory()`.

### Instruction Helpers

Since for TRGen Device all instruction are "[bitmask](https://en.wikipedia.org/wiki/Mask_(computing)) chunks" this library offers some helper functions that do the bitmask encoding.
They can be used to define easily any instruction:

| Instruction | 1° Param | 2° Param | Description |
| ----------- | ----------- | ------------ | ----------- |
| [`istrUnactiveFor(us)`](#unactive-for-n-µs) | µ seconds duration | | Set the unactivation time for µ seconds |
| [`istrActiveFor(us)`](#active-for-n-µs) | µ seconds duration | | Set the activation time for µ seconds |
| [`istrWaitPE(tr)`](#wait-positive-edge) | TrgenPin | | Wait the positive edge for a specific [TrgenPin](#trgenpin-list) |
| [`istrWaitNE(tr)`](#wait-negative-edge) | TrgenPin | | Wait the negative edge for a specific [TrgenPin](#trgenpin-list) |
| [`istrRepeat(addr,time)`](#istrRepeat-from-n-for-x-times) | Instruction address | Time of istrRepeat | Set the activation time for µ seconds |
| [`istrEnd()`](#istrEnd) | | | End the behaviour |
| [`istrNotAdmissible()`](#not-admissible) | | | Empty instruction, it has to be placed after the [`istrEnd()`](#istrEnd)|

### Write Instruction on TRGenPort

Once the desired instruction set has be setted, the `memory` can be written using  the function `writeTrgenMemory(trgenport=TrgenPin.BNCO)` to a specific `TrgenPort` by its `TrgenPin`.
After this, the program can be executed invoking the static method `start()`

The following script use the whole instruction set in order to program a single `TrgenPort`

#### custom instructions examples

```python
client = TrgenClient()
bnco = client.createTrgenPort(TrgenPin.BNCO)
impl = client.getImplementation()

# helper function used to clear the current TrgenPort memory
# it program the desired TrgenPort with a blank program
# made of one istrEnd() and all istrNotAdmissible() instructions
client.resetTrgenPort(bnco) # it is explained in the "Helpers" paragraph

bnco.setInstruction(0, istrActiveFor(5))
bnco.setInstruction(1, istrUnactiveFor(3))
bnco.setInstruction(2, istrRepeat(0,2))
bnco.setInstruction(3, istrWaitPE(TrgenPin.NS4))
bnco.setInstruction(4, istrUnactiveFor(3))
bnco.setInstruction(5, istrWaitNE(TrgenPin.NS0))
bnco.setInstruction(6, istrEnd()) # istrEnd is mandatory!
for i in range(7,impl.memory_length): # from 7 to 31!
    bnco.setInstruction(i, istrNotAdmissible()) # this also is mandatory

# write the program
# every instruction in bnco.memory
# is flashed into TRGen Device
client.writeTrgenMemory(bnco)

# execute the TRGen Device
client.start()
```

`writeTrgenMemory()` write only specific `TrgenPort`. This means that many custom programs can be written

```python
bnco = client.createTrgenPort(TrgenPin.BNCO)
ns7 = client.createTrgenPort(TrgenPin.NS7)

# you can also use this helper in order to reset All TrgenPort
client.resetAllTrgenPorts()

bnco.setInstruction(0, istrActiveFor(5))
bnco.setInstruction(1, istrUnactiveFor(3))
bnco.setInstruction(2, istrEnd())
for i in range(3,31):
    bnco.setInstruction(i, istrNotAdmissible())

ns7.setInstruction(0, istrWaitPE(TrgenPin.BNCO))
ns7.setInstruction(1, istrActiveFor(5))
ns7.setInstruction(2, istrUnactiveFor(3))
ns7.setInstruction(3, istrEnd())
for i in range(4,31):
    ns7.setInstruction(i, istrNotAdmissible())

client.writeTrgenMemory(ns7)
client.writeTrgenMemory(bnco)

# execute the TRGen Device
client.start()
```
---

## Event Listening

These are functions that helps on programming simple and customized behaviour on a simple trigger event on a specified `inputPin`.

### Listen for Default Simple to BNCO
Configures the BNC output to automatically respond to signals received on a custom inputPin

```python
from trgenpy import TrgenClient

client = TrgenClient()
client.connect()

# Respond to positive edge (default)
# default input inputPin is BNCI
# send default Trigger to BNCO
client.callbackCustomTrigger(inputPin=TrgenPin.BNCI)

# Program a behaviour that send default trigger when BNCI (default) is on negative edge
client.callbackCustomTrigger(ne=True,inputPin=TrgenPin.BNCI)

# Program a behaviour that send default trigger when NS6 is on negative edge
client.callbackCustomTrigger(ne=True,inputPin=TrgenPin.NS6)

# Program a behaviour that send default trigger when GPIO2 is on positive edge (default True)
# it automatically set the global GPIO direction to "input" 
client.callbackCustomTrigger(ne=True,inputPin=TrgenPin.GPIO2)

# Remember to call .start(), because this is just Port programming
# Start listening
client.start()
```


### Listen for Markers

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

#### Marker arguments

Same parametrization of [standard `sendMarkers`](#sending-markers):
- markerNS: for NeuroScan ports
- markerSA: for Synamps ports
- markerGPIO: for GPIO ports

```python
# Program a behaviour that send default trigger when GPIO2 is on negative edge
# Also set the Global GPIO direction to "input"
client.callbackMarker(ne=True, markerNS=50,inputPin=TrgenPin.GPIO2)

# Start listening
client.start()
```

#### NOTE
It's not allowed to send GPIO markers when the `inputPin` is GPIO

```python
# THIS WILL GENERATE ERRORS
client.callbackMarker(ne=True, markerGPIO=13,inputPin=TrgenPin.GPIO2)

# Like:
# ❌ GPIO cannot be used as both input and output simultaneously
```

### Listen for Custom Triggers
Configures a custom output pin to respond to a specific `inputPin` of type `TrgenPin`

```python
# Program a default trigger to Neuroscan 7 and Synamps 8 pins on a negative front edge from the BNCI port
client.callbackCustomTrigger(ne=True, trgenPinList=[TrgenPin.NS7,TrgenPin.SA8], inputPin=TrgenPin.BNCI))

# Start listening
client.start()

# Advanced configuration: use custom instruction set
# Configuration with custom instructions
instruction_set = [
    istrWaitPE(TrgenPin.BNCI),      # Wait for positive edge on BNCI
    istrActiveFor(50),           # Active for 50µs
    istrUnactiveFor(10),         # Inactive for 10µs
    istrActiveFor(30),           # Active for 30µs
    istrUnactiveFor(20),         # Inactive for 20µs
    istrActiveFor(20),           # Active for 20µs
    istrUnactiveFor(30),         # Inactive for 30µs
    istrEnd()                        # End program
]

# Program a custom trigger on GPIO8 and BNCO on a positive edge front (default when not specified) with a custom instruction set
client.callbackCustomTrigger(trgenPinList=[TrgenPin.GPIO8,TrgenPin.BNCO], inputPin=TrgenPin.BNCI,customInstructions=instruction_set)

# 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 instruction 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 `programDefaultTrigger()`  
```python
tr = client.createTrgenPort(TrgenPin.GPIO0)
# This sends an impulse of duration (default 20µs)
client.programDefaultTrigger(tr)  # impulse of default 20µs
```

```python
tr = client.createTrgenPort(TrgenPin.GPIO0)
# This override the standard duration and sends an impulse.
client.programDefaultTrigger(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 `setInstruction()`

```python
tr = client.createTrgenPort(TrgenPin.NS1)
tr.setInstruction(0, istrActiveFor(10))
tr.setInstruction(1, istrUnactiveFor(5))
tr.setInstruction(2, istrRepeat(0, 3))
tr.setInstruction(3, istrEnd())
client.writeTrgenMemory(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 written at least one `TrgenPort` program into its `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.getImplementation()
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()
```
---

## Helpers

Here's some utility functions used to simplify the TRGen Device interaction

### Reset TrgenPort

In order to reset a specific `TrgenPort` program, the following static functions can be used:

- `resetTrgenPort(trgenport)`
- `resetAllTrgenPorts()`

```python
bnco = TrgenPort(TrgenPin.BNCO)
client.resetTrgenPort(bnco) # reset just bnco
# it basically does
# bnco.setInstruction(0, istrEnd())
# for i in range(1, 31):
#    bnco.setInstruction(i,istrNotAdmissible())

client.resetAllTrgenPorts() # reset all TrgenPorts
```

### Set Default Duration

In [this](#sending-default-single-trigger-to-bnco) and [this](#custom-triggers) section is explained how to send a default trigger to BNCO or multiple TrgenPorts 

Default duration is 20µs, but it can be changed using `setDefaultDuration(duration_us)` like this:

```python
client.setDefaultDuration(100)
client.sendTrigger() # now this default trigger lasts 100µs
```

### Get Default Duration

To get standard new duration use `getDefaultDuration()` like this:

```python
new_duration = client.getDefaultDuration() # returns an int value
```

### Get Implementation

The TRGenDevice has its own "settings" stored into the `impl` object defined by class [`TrgenImplementation`](./trgenpy/implementation.py).
`impl` is a `TrgenClient` property and can be reached invoking the static function `getImplementation()` like this:

```python
client = TrgenClient()
impl = client.getImplementation()
```

`TrgenImplementation` contains informations about:

- [TRGen cardinality](#trgen-cardinality)
- [`mtml`](#max-trgen-memory-length-mtml)
- [`memory_length`](#memory-length)

#### TRGen Cardinality

Its a bunch of properties describing the maximum number of TrgenPort by type:

- `ns_num` (8)
- `sa_num` (8)
- `bnco_num` (1)
- `bnci_num` (1)
- `gpio_num` (8)

#### Max TRGen Memory Length (`mtml`)
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.getImplementation()
    mtml = impl.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}")
```

#### Memory Length

`memory_length` is the max number of instructions per `TrgenPort`, that is 2^mtml.

---

## ⚙️ Export/Import configuration

You can export the current device configuration to a file and import it back when needed. This is useful for backup, sharing, or restoring device settings.
You basically save the  [`TrgenClient`](https://trgenpy-90ed8a.gitlab.io/#trgenpy.connection.TrgenClient) state.

### Configuration Manager

The class `ConfigurationManager` offers function for the entire lifecycle of a configuration. creation, import and export

### Create Configuration

You can create the current configuration to a JSON file using the `export_config()` method:


```python
config = TrgenConfiguration()

# Set metadata
config.metadata.project_name = "Advanced EEG Experiment"
config.metadata.description = "Configuration with custom timing for multi-modal experiment"
config.metadata.author = "Research Team"

# Set custom default duration
config.defaults.default_trigger_duration_us = 50  # 50 microseconds
config.defaults.auto_reset_enabled = True
config.defaults.auto_reset_delay_us = 200

# Configure NS0 with custom instructions (100µs trigger)
ns0_config = TriggerPortConfig(32)
ns0_config.id = TrgenPin.NS0
ns0_config.name = "EEG Start Marker"
ns0_config.type = "NeuroScan"
ns0_config.enabled = True
ns0_config.custom_duration_us = 100
ns0_config.notes = "Used for experiment start markers"

# Program custom instructions for NS0
ns0_config.memory_instructions = [0] * 32
ns0_config.memory_instructions[0] = istrActiveFor(100)   # Active for 100µs
ns0_config.memory_instructions[1] = istrUnactiveFor(500) # Inactive for 500µs
ns0_config.memory_instructions[2] = istrEnd()               # End sequence
ns0_config.last_instruction_index = 2
ns0_config.programming_state = "Programmed"

config.trigger_ports["NS0"] = ns0_config

# Configure SA0 with different timing
sa0_config = TriggerPortConfig(32)
sa0_config.id = TrgenPin.SA0
sa0_config.name = "Stimulus Trigger"
sa0_config.type = "Synamps"
sa0_config.enabled = True
sa0_config.custom_duration_us = 200
sa0_config.notes = "Used for stimulus presentation markers"

# Program custom instructions for SA0
sa0_config.memory_instructions = [0] * 32
sa0_config.memory_instructions[0] = istrActiveFor(200)   # Active for 200µs
sa0_config.memory_instructions[1] = istrEnd()               # End sequence
sa0_config.last_instruction_index = 1
sa0_config.programming_state = "Programmed"

config.trigger_ports["SA0"] = sa0_config

# Configure BNCO for TMS applications
bnco_config = TriggerPortConfig(32)
bnco_config.id = TrgenPin.BNCO
bnco_config.name = "TMS Output Control"
bnco_config.type = "BNCO"
bnco_config.enabled = True
bnco_config.custom_duration_us = 1000  # 1ms for TMS
bnco_config.notes = "Controls TMS device output"

# Program TMS-specific instructions
bnco_config.memory_instructions = [0] * 32
bnco_config.memory_instructions[0] = istrActiveFor(1000) # Active for 1ms
bnco_config.memory_instructions[1] = istrUnactiveFor(5000) # Inactive for 5ms safety margin
bnco_config.memory_instructions[2] = istrEnd()              # End sequence
bnco_config.last_instruction_index = 2
bnco_config.programming_state = "Programmed"

config.trigger_ports["BNCO"] = bnco_config

# Add some standard ports without custom programming (will use defaults)
for i in range(1, 4):  # NS1, NS2, NS3
    port_config = TriggerPortConfig(32)
    port_config.id = getattr(TrgenPin, f'NS{i}')
    port_config.name = f"NeuroScan {i}"
    port_config.type = "NeuroScan"
    port_config.enabled = True
    config.trigger_ports[f"NS{i}"] = port_config

```

The exported configuration includes:
- All programmed TrgenPort instructions
- GPIO direction settings
- Trigger level configurations
- Default duration settings

### Export Configuration

Export is easy:

```python
# Export current client state (if you have an existing setup)
print("\n4. Exporting current client configuration...")
export_file = TrgenConfigurationManager.export_configuration(
    client, # is the current TrgenClient used
    "config_examples/current_setup.trgen", # destination path
    project_name="Current Device Setup", # project name
    description="Exported configuration from current client state", # description
    author="TrGEN User" # author
)
```

### Import Configuration

To load a previously saved configuration, use the `import_config()` method:

```python
from trgenpy import TrgenClient

client = TrgenClient()
client.connect()

# Save the configuration to file
config_file = "config_examples/advanced_experiment.trgen"
imported_config = TrgenConfigurationManager.import_configuration(client, config_file)

client.start()
```

### Configuration File Format

The configuration is stored in JSON format with the following structure:

```json
{
    "metadata": {
        "version": "1.0",
        "created_at": "2025-01-15T10:30:00.123456",
        "modified_at": "2025-01-15T10:30:00.123456",
        "description": "Configuration for EEG experiment with custom triggers",
        "project_name": "Multi-modal Experiment",
        "author": "Research Team"
    },
    "defaults": {
        "default_trigger_duration_us": 50,
        "default_log_level": "info",
        "default_timeout_ms": 2000,
        "auto_reset_enabled": true,
        "auto_reset_delay_us": 200
    },
    "trigger_ports": {
        "NS0": {
            "id": 0,
            "name": "EEG Start Marker",
            "type": "NeuroScan",
            "enabled": true,
            "custom_duration_us": 100,
            "memory_instructions": [268435712, 536871424, 1, 0, 0, ...],
            "memory_length": 32,
            "last_instruction_index": 2,
            "programming_state": "programmed",
            "last_programmed_at": "2025-01-15T10:30:00",
            "notes": "Used for experiment start markers"
        },
        "BNCO": {
            "id": 16,
            "name": "TMS Output Control",
            "type": "BNCO",
            "enabled": true,
            "custom_duration_us": 1000,
            "memory_instructions": [805306880, 1342177792, 1, 0, 0, ...],
            "memory_length": 32,
            "last_instruction_index": 2,
            "programming_state": "programmed",
            "notes": "Controls TMS device output"
        }
    },
    "network": {
        "ip_address": "192.168.123.1",
        "port": 4242,
        "timeout_ms": 2000
    }
}
```

This feature is particularly useful for:
- Sharing configurations across different experiments
- Version control of trigger setups
- Quick restoration of complex configurations
- Backup of working setups

---

## 🧠 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
```


## 💪 Examples

See the `examples` directory for the complete examples

### Send Trigger BNCO

```python
from trgenpy import TrgenClient,TrgenPin,istrActiveFor,istrUnactiveFor,istrEnd,istrWaitNE,istrWaitPE

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

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

### Send Trigger 
```python
from trgenpy import TrgenClient,TrgenPin,istrActiveFor,istrUnactiveFor,istrEnd,istrWaitNE,istrWaitPE

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

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

### Custom Trigger
```python
from trgenpy import TrgenClient,TrgenPin,istrActiveFor,istrUnactiveFor,istrEnd,istrWaitNE,istrWaitPE
# create the Client for the Trgen
client = TrgenClient() # eventually TrgenClient(ip="192.168.123.2")

client.connect()

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

        # build a trigger
        bnco = client.createTrgenPort(TrgenPin.BNCO)
        bnco.setInstruction(0, istrActiveFor(5))
        bnco.setInstruction(1, istrUnactiveFor(3))
        bnco.setInstruction(2, istrEnd())
        # send the trigger
        client.writeTrgenMemory(bnco)

        # start the sequence
        client.start()

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