# Running on Boot

A control program that runs only when started by hand is of limited use. To
operate as a controller it must start at power-up, restart if it fails, and log
where the record can be retrieved afterwards. On the SLM-RP4 this is handled by
`systemd`.

```{admonition} The OpenPLC Runtime provides this already
:class: note

A PLC program persists across a power cycle, restarts automatically, is
supervised, and logs to a stream the editor can read, with nothing to configure.

This page describes providing the equivalent for your own program. See
{doc}`overview`.
```

## Releasing the backplane at boot

A program that drives the backplane cannot share it with the OpenPLC Runtime.
Stopping the runtime by hand is enough for a single session, but it starts
again at the next power-up, so a program set to run at boot would find the bus
already taken. See {doc}`shared-hardware-rules`.

The runtime is supplied as two containers. Docker starts the **bootloader** at
power-up, and the bootloader then starts the **runtime**. Preventing the
bootloader from starting therefore keeps both stopped.

Disable them:

```bash
sudo docker update --restart=no openplc-bootloader
sudo docker stop openplc-bootloader openplc-runtime
```

The first command is what survives a reboot. The `stop` command ends the
current session, and the backplane is free from that point on.

To return the device to normal PLC operation:

```bash
sudo docker update --restart=unless-stopped openplc-bootloader
sudo docker start openplc-bootloader
```

Starting the bootloader is sufficient. It starts the runtime itself, and the
device is reachable from the editor again after about twenty seconds.

```{admonition} The device is no longer a PLC while this is disabled
:class: warning

With the runtime stopped, the device does not answer the editor's **Search**,
does not accept program uploads, and does not run any PLC program that was
previously loaded. Ports 8443, 8445 and 33333 are closed.

The stored project is not deleted. Re-enabling the runtime restores it.
```

Confirm the state before relying on it:

```bash
sudo docker inspect -f "{{.Name}} restart={{.HostConfig.RestartPolicy.Name}} running={{.State.Running}}" \
  openplc-runtime openplc-bootloader
```

`openplc-bootloader` should report `restart=no`, and both containers should report `running=false`. `openplc-runtime` has no restart policy of its own; the bootloader starts and supervises it.

## A service unit

Create the unit file:

```bash
sudo nano /etc/systemd/system/my-controller.service
```

```ini
[Unit]
Description=SLM-RP4 control program
After=network.target

[Service]
Type=simple
User=slm-rp4
WorkingDirectory=/home/slm-rp4/my-controller
Environment=PYTHONPATH=/home/slm-rp4/Desktop/synergy_python
Environment=PYTHONUNBUFFERED=1
ExecStart=/usr/bin/python3 /home/slm-rp4/my-controller/main.py
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=my-controller

[Install]
WantedBy=multi-user.target
```

Enable and start it:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now my-controller
```

Check it:

```bash
systemctl status my-controller
journalctl -u my-controller -f
```

## What each line is doing

`User=slm-rp4`
: Run unprivileged. The `slm-rp4` user already has backplane access. Use `root`
  only where it is genuinely required, such as driving the RGB indicator.

`WorkingDirectory=`
: Relative paths in your program resolve from here. Set it explicitly rather
  than relying on the default.

`Environment=PYTHONPATH=...`
: Makes `synergy_logic` and `slm_rp4_helpers` importable from a program that
  lives outside the supplied library directory. Omit this line if your program
  runs from `~/Desktop/synergy_python`. See {doc}`python/setup`.

`Environment=PYTHONUNBUFFERED=1`
: Python buffers standard output when it is not a terminal, which delays log
  entries and can discard them if the process is killed. This setting disables
  that buffering.

`Restart=on-failure` / `RestartSec=5`
: Restart on a crash or a non-zero exit, waiting five seconds between attempts.

  `Restart=always` also restarts after a clean exit. Use it for a program that
  should never stop, but not for one that exits deliberately when its work is
  complete.

`StandardOutput=journal`
: Send `print()` and stderr to the journal. Read them with `journalctl`.

## Startup ordering

A program that opens the backplane at startup may run before the SPI device is
ready. Retry the connection instead of assuming the first attempt succeeds:

```python
import sys
import time

import synergy_logic

base = None
for attempt in range(10):
    try:
        base = synergy_logic.Base()
        break
    except Exception as exc:
        print(f"backplane not ready ({exc}), retry {attempt + 1}/10")
        time.sleep(2)

if base is None:
    print("giving up")
    sys.exit(1)
```

The same code then handles a boot race, a slow power ramp, and a service
restarted while the hardware is still settling.

## Shutting down safely

`systemctl stop` sends `SIGTERM`, waits, then sends `SIGKILL`. Handle the first
one and put your outputs in a safe state.

```python
import signal
import sys

running = True


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


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

try:
    while running:
        control_loop()
finally:
    all_outputs_off()
```

Give yourself enough time to do it:

```ini
TimeoutStopSec=30
```

```{admonition} Without this, outputs stay where they were
:class: danger

A process killed without a handler leaves every output in its last commanded
state. A valve that was open stays open. Nothing in Linux, in systemd or in the
libraries will close it for you.

Handling `SIGTERM` is not optional for a program that drives real equipment.
```

## Watching a C++ program instead

Identical, with the binary in `ExecStart`:

```ini
[Service]
Type=simple
User=slm-rp4
WorkingDirectory=/home/slm-rp4/my-controller
ExecStart=/home/slm-rp4/my-controller/my-controller
Restart=on-failure
RestartSec=5
```

C++ has no output buffering problem in the Python sense, but `printf` to a pipe
is still block-buffered. Call `setvbuf(stdout, NULL, _IOLBF, 0)` at startup, or
`fflush(stdout)` after anything you need to see promptly.

## Real-time scheduling

For a control loop with tight timing:

```ini
[Service]
CPUSchedulingPolicy=fifo
CPUSchedulingPriority=50
```

```{admonition} Be careful with this
:class: warning

A `SCHED_FIFO` process at high priority that stops yielding can lock up the
system. It will not be preempted by anything at lower priority, including the
shell you would use to stop it.

Use it only for a loop that sleeps every iteration, and keep the priority below
that of any other real-time process on the device. Test on a bench unit first, with a
means of power-cycling it.
```

The absolute-deadline pattern in {doc}`cpp/examples` matters more than the
scheduling policy for most applications. Get the loop structure right before
reaching for real-time priorities.

## Log rotation

The journal rotates by itself, but a program logging to a file of its own will
fill the disk eventually.

```bash
sudo nano /etc/logrotate.d/my-controller
```

```text
/home/slm-rp4/my-controller/logs/*.log {
    daily
    rotate 14
    compress
    missingok
    notifempty
    copytruncate
}
```

Or cap the journal instead, in `/etc/systemd/journald.conf`:

```text
SystemMaxUse=500M
```

A controller that has been running for two years with unrotated logs is a
controller that stops when the filesystem fills.

## Useful commands

```bash
sudo systemctl start my-controller
sudo systemctl stop my-controller
sudo systemctl restart my-controller
sudo systemctl enable my-controller      # start at boot
sudo systemctl disable my-controller

systemctl status my-controller
journalctl -u my-controller -f           # follow
journalctl -u my-controller -n 100       # last 100 lines
journalctl -u my-controller --since "1 hour ago"

sudo systemctl daemon-reload             # after editing the unit file
```

`daemon-reload` after every edit. Forgetting it means systemd keeps running the
old definition, which makes the resulting behaviour difficult to diagnose.

For the OpenPLC Runtime containers:

```bash
sudo docker update --restart=no openplc-bootloader             # do not start at boot
sudo docker update --restart=unless-stopped openplc-bootloader # start at boot (factory setting)

sudo docker stop openplc-bootloader openplc-runtime
sudo docker start openplc-bootloader                           # starts both

sudo docker ps                                                 # what is running
```

## Next

- {doc}`../integrations/fuxa-hmi`, for adding a dashboard to a program running
  this way.
- {doc}`../troubleshooting`.
