ligo-softioc 0.1.7
Support library for writing EPICS SoftIOCs in Python.
Requires Python: >=3.6

# softioc
A python  library supporting simple creation of soft EPICS iocs.

## Features

- generates customizable IOC status channels
- handles timing of channel updates
- handles all interaction with pcaspy
- optional customizable alarm system
- supports active servers that generate their own channel values.
- supports passive servers that reflects values from external inputs.
- can generate an INI file listing the channels of the IOC

## Quick Start

This example IOC, also in `examples/active.py`, read an input dchannel and squares it, writing the result to an output channel.

It also reports the number of times the IOC process function has been run.

```
from ligo_softioc import SoftIOC
import sys

process_count = 0

def process(ioc: SoftIOC, gps_time_sec: int) -> None:
    """
    Update the custom channels.

    Auto-generated channels are auto-updated.
    :param ioc:
    :param gps_time_sec:
    :return:
    """
    global process_count
    process_count += 1

    # Read the input - change this value with caput
    in_val = ioc.getParam("INPUT")

    # square INPUT and write it to SQUARED_OUTPUT
    ioc.setParam("SQUARED_OUTPUT", in_val ** 2)

    # set to how many times process() has run
    ioc.setParam("PROCESS_COUNT", process_count)


def build_ioc() -> SoftIOC:
    """
    Build a new SoftIOC that's completely ready to run

    Separate the ioc build from 'if __name__ == "__main__"'
    so that halper scripts can load the IOC and create the object
    to interrogate it.
    :return:
    """

    # setup the ioc with a channel prefix.
    # the ioc will by default run process() about 10 times per second
    ioc = SoftIOC(
        prefix="X1:IOC-",
        process_func=process)

    # add in some channels

    channels = {
        "INPUT": {'prec': 3},
        "SQUARED_OUTPUT": {'prec': 3},
    }

    ioc.add_channels(channels)

    ioc.add_channel("PROCESS_COUNT", {'type': 'int'})

    # call this when all channels are added.
    # and before you try to set the value of any channel
    ioc.finalize_channels()

    # setup the input varible so we know how it'll start
    ioc.setParam("INPUT", 0)

    return ioc


if __name__ == "__main__":

    ioc = build_ioc()

    ioc.start()

```

## build_ioc function

The SoftIOC object should be built in a function called 'build_ioc'.  
This function should return the SoftIOC object.

The 'build_ioc' function is necessary to run helper scripts like 'softioc_print_ini'.
Such scripts will call the build_ioc function to get the SoftIOC object for introspection.

Any arguments to the 'build_ioc' function should be type annoted.

Pass build_ioc arguments on the command line to the helper scripts after a '--' argument.

For example, if build_ioc is defined as 

```
def build_ioc(foo: int, bar: str):
    ...
```

Then 'print_ini' is called as

```
print_ini ioc.py -- 123 abc
```

Then the command line arguments `['123', 'abc']` will be converted to `[123, 'abc']`
and passed to build_ioc().


## Auto-generated channels
The following channels are autogenerated. The values are also set automatically.

These channels use the normal prefix, but the prefix can be lengthened with
an optional parameter to SoftIOC.

The IOC declared with
```
SoftIOC(
    prefix="X1-SYS_EXAMPLE_",
    ioc_chan_prefix="IOC_"
)
```

will use the prefix "X1-SYS_EXAMPLE_IOC_" for the channels mentioned in this section.
All other channels will use the prefix "X1-SYS_EXAMPLE_".

GPS
: The current time in seconds from the GPS epoch (see below).

TIMESTAMP
: The current time formatted as YYYY-MM-DD HH:mm:SS TZ

START_GPS
: The GPS second recorded when the IOC started.

START_TIMESTAMP
: The start time formatted the same as TIMESTAMP

UPTIME_SEC
: Seconds since the start of the IOC.

UPTIME_STR
: The uptime of the IOC as an easy to understand string, e.g. "3 weeks".

HOSTNAME
: The name of the host the IOC is running on

PROCESS
: Some info about the process controller, e.g. "systemd"

ERROR_MESSAGE
: The latest error message generated by the IOC, or an empty string if there haven't been any errors.

ERROR_GPS
: The time of the last error in GPS seconds.

ERROR_TIMESTAMP
: The time of the last error in TIMESTAMP format.

### The GPS Epoch
GPS times are given in seconds counted from 00:00:00 Jan 6, 1980 UTC.


## Passive IOC

A passive IOC, also known as a 'separate server' IOC serves custom channels but does not process them.
Some external process is responsible for setting the channel value.  This external process is sometimes called the 'server'.
Some channels that reference it have 'SERVER' in the name.

An example can be found in `examples/passive.py`.

A passive IOC does not need a process function.

A passive IOC gets several more auto-generated channels.  These channels can help monitor the external process.

Some of the channels should be set directly by the external process, and some are calculated by the SoftIOC.
The server is responsible for setting these channels: SERVER_START_GPS, SERVER_GPS, LAST_PROCESS_GPS,

SERVER_START_GPS
: The start time of the server in GPS seconds.  Set by the server.

SERVER_GPS
: The current time of the server in GPS seconds.  Set by the server.

SERVER_START_TIMESTAMP
: The start time of the server as a string.  Set automatically by SoftIOC.

SERVER_UPTIME_SEC
: Uptime of the server.  Set automatically by SoftIOC.

SERVER_UPTIME_STR
: Server uptime as a reasonable string, e.g. '3 months'.  Set automatically by SoftIOC.

LAST_PROCESS_GPS
: Timestamp of the last time the server set channel values. Set by the server.

LAST_PROCESS_TIMESTAMP
: The last porcess time of the server as a string.

SINCE_LAST_PROCESS_SEC
: Number of seconds since LAST_PROCESS_GPS was updated by the server. Automatically set by SoftIOC.

SINCE_LAST_PROCESS_STR
: Time since LAST_PROCESS_GPS was updated by the server in a readable format.  Automatically set by SoftIOC.

SERVER_RUNNING
: A value that indicates if the server has run recnetly
0 - Invalid state.  SINCE_LAST_PROCESS_SEC is negative.
1 - Running.  SINCE_LAST_PROCESS_SEC is less than or equal to the maximum allowed, default 60 seconds.
2 - Stalled.  SINCE_LAST_PROCESS_SEC is greater than the maximum allowed.
Set automatically by SoftIOC.

## Alarms

Alarms can be optionally added to the SoftIOC.

Create an alarm object, then add it to the soft IOC before calling `finalize_channels()`.

```
from ligo_softioc import SoftIOC, Alarm

...

ioc = SoftIOC(...)

...

alarm = Alarm(
    name = 'chan_to_big`,
    text = 'the value of an example chan is greater than the allowed maximum`,
    predicate = lambda: ioc.getParam("X1:SYS-EXAMPLE_CHAN") > 100
)
ioc.add(alarm)

...

ioc.finalize_channels()
```

### Basic alarm channels

When an alarm is added to a SoftIOC, these additional channels are automatically created and updated.
Where a lowercase 'n' appears at the end of a channel name, n takes on the values from 1..10, so each listing represents
10 channels:


ALARM_GPS_TIME
: GPS time of the last alarm

ALARM_MESSAGE
: Message of the last alarm.  This is the 'text' parameter of the alarm.

ALARM_TIMESTAMP
: Time of the last alarm as a string.

ALARM_DUMP
: An EPICS input.  Set to 1 to send up to 100 currently-set alarms to the standard out of the IOC.

ALARM_DUMP_HISTORY
: An EPICS input.  Set to 1 to send a history of up to the last 100 alarms set to standar out of the IOC.

ALARM_HISTORY_GPS_TIME_n
: The GPS time of the 'n'th alarm that has been set in order from the most recent alarm.

ALARM_HISTORY_TIMESTAMP_n
: A formatted timestamp of the 'n'th alarm that has been set in order from the most recent alarm.

ALARM_HISTORY_MESSAGE_n
: The alarm message of the 'n'th alarm that has been set in order from the most recent alarm.

ALARM_GPS_TIME_n
: The GPS time of the 'n'th alarm that is currently set.

ALARM_TIMESTAMP_n
: The formatted timestamp of the 'n'th alarm that is currently set.

ALARM_MESSAGE_n
: The alarm message of the 'n'th alarm that is currently set.

The "ALARM" prefix of these channels can be changed using alarm families.  See below.

### Alarm Groups

Instead of adding alarms directly to an IOC, they can be added to an AlarmGroup object.
Then the AlarmGroup object can be added to the IOC.

An alarm group is a set of alarms that produces only a single alarm at one time, even when multiple
alarms are set.

The alarm group will always produce the most recent alarm set within the hour.  After an hour has elapsed,
The alarm group will cycle through older alarms and produce them one at a time, a new one each hour, eventually cycling
back around the most recently set alarm.

If alarms are added to a SoftIOC directly, they are added to a default alarm group named "alarm".


```
from ligo_softioc import SoftIOC, Alarm, AlarmGroup

...

ioc = SoftIOC(...)

...

zoneB_bad_temp = Alarm(...)
zoneB_bad_humidity = Alarm(...)
zoneB_alarm_group = AlarmGroup('zoneB')
zoneB_alarm_group.add(zoneB_bad_temp)
zoneB_alarm_group.add(zoneB_bad_humidity)
ioc.add(zoneB_alarm_broup)
```

### Alarm Families

Instead of adding alarm groups directly to an IOC, they can be added to an AlarmFamily.

An alarm family is a logical set of AlarmGroups.  Alarm families create the alarm epics channels.

If Alarm or AlarmGroup objects are added directly to the IOC, then they will be put into tha default
alarm family named 'alarm'.  This is the reason for the 'ALARM_' prefix on the basic alarm channels
described above.

```
from ligo_softioc import SoftIOC, Alarm, AlarmGroup, AlarmFamily

...

lvea_zoneB_group = AlarmGroup(...)
lvea_zoneC_group = AlarmGroup(...)
cer_alarm_group = AlarmGroup(...)

...

lvea_alarm_family = AlarmFamily('lvea_alarm')
cer_alarm_family = AlarmFamily('cer_alarm')
lvea_alarm_family.add_alarm_group(lvea_zoneB_group)
lvea_alarm_family.add_alarm_group(lvea_zoneC_group)
cer_alarm_family.add_alarm_group(cer_alarm_group)

...

```

This example will create a set of alarm channels for each alarm family, base on its name.

There will be both LVEA_ALARM_GPS_TIME and CER_ALARM_GPS_TIME.  Both LVEA_ALARM_MESSAGE and CER_ALARM_MESSAGE etc.