# Quick Start: Python

The minimum working program is three lines:

```python
import synergy_logic

base = synergy_logic.Base()
base.print_modules()
```

```text
$ python3 modules.py
Slot 1 - SLM-DCI-16NP
Slot 2 - SLM-DCO-15P
Slot 3 - SLM-AI-8-V
```

If this lists the modules on the rail correctly, the library is working.

## Printing base modules

Calling the `Base` constructor initializes the base controller and begins the module sign-on process. Every configured module responds and is added to the base's object tree.

`print_modules()` prints each slot and its module name. See the [API reference](api-reference.md) for the constructor's optional arguments (including zero-based indexing).

## The Base Class

The `Base` object exposes the backplane as a two-level index:

```python
module = base[1]       # IO_Module in slot 1
channel = base[1][2]   # channel 2 of slot 1
```

Slots and channels are numbered from 1 by default, matching the editor's I/O Mapping table. See the [API reference](api-reference.md) for the complete class.

## Read a channel

```python
sensor = base[1][1]
print(sensor.value)
```

`.value` returns the value of the channel. For analog channels, `.real` returns the same reading converted to engineering units:

```python
analog = base[3][1]
print(f"{analog.value} counts = {analog.real:.2f} V")
```

## Write a channel

```python
output = base[2][1]
output.value = True       # discrete
output.real = 5.0         # analog (volts, milliamps, °C — depends on the module)
```

## Combo modules

Combo modules carry both inputs and outputs. Plain `[]` indexing is ambiguous, so inputs and outputs are addressed through separate properties:

```python
combo = base[1]
sensor = combo.inputs[1]
relay  = combo.outputs[1]
```

See the [API reference](api-reference.md) for the full property list.

## IO_Module class object

Whole-module operations and configuration go through the module object:

```python
module = base[2]

module.values                      # list of every channel's value
module.do_bitmapped(0b00001111)    # write all outputs in one SPI transaction
mask = module.di_bitmapped()       # read all inputs as one integer
```

Bitmapped calls use a single bus transaction for the whole module and are substantially faster than iterating over channels when driving many at once.

To configure a module:

```python
module = base[3]                       # SLM-AI-8-V
module.configure_module((0x40, 0x03))  # enable channels 1-4 only
```

Configuration byte values and the list of configurable modules are in the [API reference](api-reference.md).

## Verify modules sign-on

`rollCall` sends an array of expected modules and compares them against those signed on. Raises `ValueError` on a mismatch, naming the expected module, the module found, and its position in the list.

```python
base.rollCall([
    "SLM-DCI-16NP",
    "SLM-DCO-15P",
    "SLM-AI-8-V",
])
```

Call this at the start of any program that assumes a particular rail.

## Example program

A complete program that reads an analog input, drives an output proportionally, and shuts down cleanly on exit.

Clean shutdown uses the standard `signal` library to catch `SIGINT` (Ctrl-C from a terminal) and `SIGTERM` (sent by a service manager stopping the program). Both signals set a flag that breaks the main loop; the `finally` block then closes the output before the program exits.

For manual testing, the toggle switch can be read in the same way to trigger a simulated failure or stop. See [Reading the toggle switch](examples/toggle-switch.md) for the switch-reading pattern.

```python
#!/usr/bin/env python3
"""Proportional valve control from a level transmitter."""

import signal
import time

import synergy_logic

base = synergy_logic.Base()
base.rollCall(["SLM-AI-8-V", "SLM-AO-8-V"])

level = base[1][1]
valve = base[2][1]

running = True

def shutdown(signum, frame):
    global running
    running = False

signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)

try:
    while running:
        volts = level.real
        valve.real = min(max(volts, 0.0), 10.0)
        time.sleep(0.1)
finally:
    valve.real = 0.0
    print("stopped, valve closed")
```

```{admonition} The finally block is the important part
:class: warning

Without it, killing the program leaves the valve in whatever state it was last commanded. Nothing in Linux or in the library closes it automatically.

Every program that drives an output needs a shutdown path that leaves the plant in a safe state, and that path must run on `SIGTERM` as well as `Ctrl-C`.
```

## Next

- {doc}`examples/index`, for more complete programs.
- {doc}`api-reference`, for the full library reference.
- {doc}`../running-on-boot`, for running a program as a service.
