# FUXA: A Local HMI

FUXA is a free, open-source SCADA/HMI package that runs on the SLM-RP4 itself.
A browser pointed at the device presents a dashboard with gauges, bar graphs,
sliders and trend charts, without a separate HMI panel or a cloud service.

FUXA communicates over Modbus TCP, which is how it reaches your data whichever
way the device is programmed.

![A FUXA dashboard driving an output from a slider](../../images/fuxa/FUXAdemo.gif)

## Which architecture

FUXA is a Modbus TCP **client**. It connects to a Modbus server running on the
SLM-RP4, whichever software provides it:

::::{grid} 1 1 2 2
:gutter: 3

:::{grid-item-card} OpenPLC Runtime

The runtime's built-in Modbus server, enabled in the PLC project. See
[Modbus server](https://edge.autonomylogic.com/docs/openplc-editor/communication/modbus/server)
in the OpenPLC Editor documentation.
:::

:::{grid-item-card} Python or C++ program

A Modbus server in your own program. See
{ref}`fuxa-python-server` below.
:::

::::

```text
┌─────────────────┐    Modbus TCP     ┌──────────────────┐     SPI     ┌───────────┐
│   FUXA server   │ ◄── localhost ──► │  Modbus server   │ ◄────────►  │ Backplane │
│   (port 1881)   │                   │  (runtime or app)│             │           │
└────────┬────────┘                   └──────────────────┘             └───────────┘
         │ HTTP
   ┌─────▼──────┐
   │  Browser   │  on your workstation, or a panel PC
   └────────────┘
```

Both sides run on the same device, so they talk over the loopback interface.

## Installing FUXA

FUXA requires Node.js. The FUXA project recommends Node.js 18 LTS; Node.js 14 and older are not supported since FUXA 1.2.7.

```bash
# Install nvm, the Node version manager
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash

# Reopen the shell, then
nvm install 18
node -v && npm -v
```

Install FUXA itself. This takes a while on a Raspberry Pi:

```bash
npm install -g --unsafe-perm @frangoteam/fuxa-min
```

```{note}
`fuxa-min` is the smaller build, without the `node-snap7` dependency used for
direct Siemens S7 connections. It is the appropriate choice here, as it builds
cleanly on current Node versions and nothing in this guide requires S7. If you
need S7 support, install the full `@frangoteam/fuxa` package instead.
```

Run it:

```bash
fuxa
```

```text
2026-08-11T13:04:41.945Z [INF]  FUXA V.1.1.19
2026-08-11T13:04:46.808Z [INF]  FUXA started!
2026-08-11T13:04:46.826Z [INF]  WebServer is running http://127.0.0.1:1881/
```

Open it from your workstation at `http://<device-ip>:1881/`, or locally at
`http://localhost:1881/`.

To start FUXA at boot, see {doc}`../advanced/running-on-boot`. The unit file
takes the same form, with `ExecStart` pointing at the `fuxa` binary. Locate it
with `which fuxa`; npm installs under `~/.nvm/versions/node/`.

## Connecting FUXA to your data

Open the hamburger menu at the bottom left ![hamburger](../../images/fuxa/FUXAburger.png)
and choose **Editor**. Click the settings gear
![gear](../../images/fuxa/FUXAgear.png) and open **Connections**.

Add a device with ![plus](../../images/fuxa/FUXAplus.png) and configure it as a
Modbus TCP client.

![The FUXA device property dialog](../../images/fuxa/FUXAdevice.png)

| Field | OpenPLC Runtime | Python or C++ server |
|---|---|---|
| Address | `127.0.0.1` | `127.0.0.1` |
| Port | The port set for the Modbus server in the PLC project (default `502`) | The port your program binds (`1502` in the example below) |
| Protocol | Modbus TCP | Modbus TCP |

## Adding tags

Tags are the individual values FUXA reads and writes. In the Connections view
you can switch between a list and a node-based layout; click the tag icon
![tag](../../images/fuxa/FUXAclip.png) under your device to add one.

![The node view of devices and tags](../../images/fuxa/FUXAnode.png)

![The tag property dialog](../../images/fuxa/FUXAtag.png)

The address you enter uses traditional Modbus notation:

![Modbus address ranges](../../images/fuxa/modbusRange.png)

| Range | Type | Access |
|---|---|---|
| 0xxxx | Coils | Read/write bit |
| 1xxxx | Discrete inputs | Read-only bit |
| 3xxxx | Input registers | Read-only word |
| 4xxxx | Holding registers | Read/write word |

With OpenPLC Runtime, the mapping of PLC variables to these ranges is documented
in [Modbus Addressing](https://edge.autonomylogic.com/docs/openplc-editor/communication/modbus/addressing).
With a Python or C++ server, your program defines the mapping.

With data flowing, the tag's value updates live in the editor:

![A tag showing a live value](../../images/fuxa/FUXAvalue.png)

```{admonition} If a tag never updates
:class: tip

Check the **data type** first. A tag configured with the wrong type, whether
signed against unsigned or word against double word, usually shows a plausible
but frozen value rather than an obvious error.

A 32-bit value requires two consecutive registers and an agreed word order,
which is a common cause of incorrect readings. Publishing analog values as raw
counts in a single `UINT` register avoids the problem.
```

## Building a dashboard

Back in the editor, add a view and start placing widgets.

### Displaying values

Drop a label and bind it to a tag:

![Binding a label to a tag](../../images/fuxa/FUXAOutputValue.gif)

The play button ![play](../../images/fuxa/FUXAplay.png) at the top left gives a
live preview:

![A live preview of the dashboard](../../images/fuxa/FUXApreview.png)

### Bar graphs

A bar is more legible than a number for anything with a range. Bind it to the
tag and set the minimum and maximum to match:

![Configuring a bar graph](../../images/fuxa/FUXAbar.gif)

![The bar graph in preview](../../images/fuxa/FUXAbarprev.png)

### Writing values back

Sliders, buttons and input fields write to a tag, which means an operator can
change a setpoint from the dashboard.

![Adding a slider bound to a tag](../../images/fuxa/FUXAslider.gif)

Set the slider's scale and limits to match the real range of the value.

```{admonition} Validate what comes back from the HMI
:class: warning

An HMI writing directly into a register that your control logic acts on
provides a path from a touchscreen into your process. The limits configured in
FUXA are a convenience rather than a guarantee, because anything that can reach
that Modbus port can write any value.

Clamp and sanity-check every incoming setpoint in the PLC program or in your
control loop:

    IF setpoint > MAX_SAFE THEN setpoint := MAX_SAFE; END_IF;
    IF setpoint < MIN_SAFE THEN setpoint := MIN_SAFE; END_IF;

Do not rely on the HMI to be the only thing that ever writes.
```

(fuxa-python-server)=
## A Python Modbus server

A minimal server that publishes an analog input and takes a setpoint for an
analog output. It listens on port `1502`. To use the standard Modbus port `502`,
run the program as root; any port above 1024 works without root.

```python
#!/usr/bin/env python3
"""Expose backplane data to FUXA over Modbus TCP."""

import signal
import time

import synergy_logic
from pyModbusTCP.server import DataBank, ModbusServer

running = True

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

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

base = synergy_logic.Base()
level = base[1][1]      # SLM-AI-8-V
valve = base[2][1]      # SLM-AO-8-V

bank = DataBank()
server = ModbusServer(host="127.0.0.1", port=1502, no_block=True, data_bank=bank)
server.start()

try:
    while running:
        # Publish the reading -- FUXA reads this as 300002
        bank.set_input_registers(1, [level.value])

        # Take the setpoint -- FUXA writes this as 400002
        setpoint = bank.get_holding_registers(1, 1)[0]
        valve.value = min(setpoint, 4095)

        time.sleep(0.1)
finally:
    valve.value = 0
    server.stop()
```

```bash
pip3 install --user --break-system-packages pyModbusTCP
```

This installs the package for the `slm-rp4` user only. A program run as root needs it installed with `sudo`.

```{admonition} Register numbering in pyModbusTCP
:class: important

`pyModbusTCP` takes an **offset within the block**, without the leading digit of
the traditional notation. Holding register `400001` is offset `0`, `400002` is
offset `1`, `423000` is offset `22999`.

`set_holding_registers(address, word_list)` takes a **list**, even for a single
value: `[value]`, not `value`.

Both are common causes of a tag that exists but never updates.
```

This program accesses the backplane directly and must not run while a PLC
program is running. See {doc}`../advanced/shared-hardware-rules`.

## Running both at boot

FUXA runs as its own service; see {doc}`../advanced/running-on-boot`. When it
connects to a Python or C++ server that also runs as a service, order FUXA after
it:

```ini
[Unit]
Description=FUXA HMI server
After=network.target my-controller.service
Wants=my-controller.service
```

FUXA reconnects on its own if the server is not up yet, so this is a
politeness rather than a requirement.

## Security

FUXA serves an unauthenticated web application on port 1881 by default, and the
Modbus port it talks to has no authentication at all.

- Keep both on the control network. Do not forward 1881 to the internet.
- Enable FUXA's own authentication if it will be reachable by anyone who should
  not be changing setpoints.
- Treat any writable tag as something an attacker on that network can set.

## Further reading

- [FUXA wiki](https://github.com/frangoteam/FUXA/wiki), the project's own
  documentation
- [FUXA on GitHub](https://github.com/frangoteam/FUXA)
- [pyModbusTCP ModbusServer](https://pymodbustcp.readthedocs.io/en/latest/package/class_ModbusServer.html)
