fbs-interlock-gateway is a small Go service that lets an FBS interlock server control networked interlocks through a local gateway.
The gateway sits between FBS and each physical network interlock. FBS talks to the gateway over HTTP. The gateway translates those requests into Shelly-style HTTP RPC calls and returns the simple JSON state response that FBS expects.
Real deployment configuration must stay out of Git. Do not commit config.yaml or config.yaml.bak.
+------------+ HTTP +-------------------------+ HTTP RPC +----------------------+
| | /status /on /off | | Switch.GetStatus | |
| FBS Server | <-----------------> | fbs-interlock-gateway | <-------------------> | Network Interlock |
| | | | Switch.Set | Relay / Control Box |
+------------+ +-------------------------+ +----------------------+
|
v
Tool enable / monitor
circuit changes state
Local-only admin UI
http://127.0.0.1:18090
|
v
config.yaml editor / status view
The gateway exposes one HTTP listener per configured tool/interlock. Each listener port represents one interlock target.
Example layout using placeholders only:
FBS Server
-> http://<gateway-host>:<tool-1-port>/status
-> fbs-interlock-gateway
-> http://<interlock-host-1>/rpc/Switch.GetStatus?id=<switch-id>
FBS Server
-> http://<gateway-host>:<tool-2-port>/on
-> fbs-interlock-gateway
-> http://<interlock-host-2>/rpc/Switch.Set?id=<switch-id>&on=true
The gateway is the only service FBS needs to know about. The individual interlock hostnames/IPs live only in the local config.yaml on the deployment machine.
FBS sends HTTP requests to the gateway:
http://<gateway-host>:<port>/status
http://<gateway-host>:<port>/on
http://<gateway-host>:<port>/off
The gateway accepts several common request formats for on/off commands, including path-based and query-based values such as:
/on
/off
?turn=on
?turn=off
?state=1
?state=0
?value=1
?value=0
The gateway responds to FBS with:
{"Success":1,"State":1}or:
{"Success":1,"State":0}State: 1 means the interlock output is on.
State: 0 means the interlock output is off.
For status requests, the gateway asks the interlock for its current output state:
http://<interlock-host>/rpc/Switch.GetStatus?id=<switch-id>
For command requests, the gateway sets the output state:
http://<interlock-host>/rpc/Switch.Set?id=<switch-id>&on=true
http://<interlock-host>/rpc/Switch.Set?id=<switch-id>&on=false
The network interlock changes the state of the configured relay/control output. The physical wiring determines what the relay controls, such as a monitor circuit, enable circuit, or another non-destructive control line.
The software path is:
FBS event
-> FBS HTTP request
-> gateway listener for that tool
-> interlock HTTP RPC command
-> relay output changes state
-> tool control circuit changes state
The gateway includes a built-in web admin UI.
By default, it listens on:
http://127.0.0.1:18090
The admin UI is embedded into the Go binary using Go's embed package. The deployed Linux machine does not need a separate web/ directory at runtime.
The admin UI currently provides:
- live status table for configured tools
- Shelly connection/output status
- editable config table
- add-tool support
- config save support
- automatic restart request after saving
- styled interface with a fixed footer
The admin UI is intentionally local-only by default. Do not expose it directly to the network unless access is restricted with firewall rules or another access-control layer.
The admin UI address can be changed with:
./fbs-interlock-gateway -config config.yaml -admin 127.0.0.1:18090To disable the admin UI:
./fbs-interlock-gateway -config config.yaml -admin ""For a remote Debian/Linux gateway, keep the admin UI bound to 127.0.0.1 and use an SSH tunnel from your workstation:
ssh -L 18090:127.0.0.1:18090 fbs-gateway@<gateway-host>Then open this on your workstation:
http://127.0.0.1:18090
The admin UI uses these local API endpoints:
GET /api/config
PUT /api/config
GET /api/status
POST /api/restart
Returns the currently loaded config as JSON.
Accepts the edited config as JSON, validates it, writes it back to config.yaml, and creates config.yaml.bak from the previous file when possible.
The save operation writes through a temporary file before renaming it into place.
Returns live status information for configured tools:
[
{
"interlock_name": "EQU-EXAMPLE-TOOL-01",
"ip": "interlock-01.example.local",
"port": 8081,
"switch_id": 0,
"enabled": true,
"connected": true,
"output": false
}
]If an interlock cannot be reached, connected is false and the error message is included.
Requests a clean gateway restart after a config save.
In production, the gateway exits and systemd is expected to restart it according to the service restart policy. When testing locally with go run, the process will exit unless you run it inside a restart loop.
The service loads config.yaml from the path provided with the -config flag.
If no -config flag is provided, the gateway looks for config.yaml next to the executable.
In the systemd deployment, the service starts with:
-config /opt/fbs-interlock-gateway/config.yaml
config.yaml is intentionally not tracked by Git because it contains deployment-specific information such as interlock hostnames, addresses, ports, and future credentials.
Create a starter config.yaml with:
make init-configThis target does not overwrite an existing config.yaml.
Generated starter config:
bind: 0.0.0.0
defaults:
timeout_ms: 800
safe_state_on_error: "off"
tools:
- interlock_name:
ip:
port:
switch_id:
username:
password:
enabled:Fill in real values before running the gateway.
Safe example:
bind: "0.0.0.0"
defaults:
timeout_ms: 800
safe_state_on_error: "off"
tools:
- interlock_name: "EQU-EXAMPLE-TOOL-01"
ip: "interlock-01.example.local"
port: 8081
switch_id: 0
username: null
password: null
enabled: true
- interlock_name: "EQU-EXAMPLE-TOOL-02"
ip: "interlock-02.example.local"
port: 8082
switch_id: 0
username: null
password: null
enabled: true| Field | Purpose |
|---|---|
bind |
Address the FBS-facing gateway listeners bind to. Use 0.0.0.0 to listen on all interfaces. |
defaults.timeout_ms |
HTTP timeout for interlock requests. |
defaults.safe_state_on_error |
State reported back to FBS if the interlock cannot be reached. Usually off. |
tools[].interlock_name |
Human-readable tool/interlock name used in logs and the admin UI. |
tools[].ip |
Hostname or IP address of the network interlock. Keep real values out of Git. |
tools[].port |
Gateway listener port for that FBS tool/interlock. |
tools[].switch_id |
Interlock switch/relay ID. For Shelly 1 Mini Gen3 this is usually 0. |
tools[].username |
Reserved for future authenticated interlocks. |
tools[].password |
Reserved for future authenticated interlocks. Do not commit real passwords. |
tools[].enabled |
Whether this gateway listener should start. |
The admin API validates edited config before saving.
Validation checks include:
- missing
interlock_name - missing
ip - invalid ports
- duplicate ports
- invalid
switch_id
If validation fails, the config is not written.
Format the code:
make fmtBuild for macOS Apple Silicon:
make build-macBuild for Linux ARM64:
make build-linux-arm64Build for Linux AMD64:
make build-linux-amd64Build release binaries and checksums:
make releaseBuild only the Linux AMD64 release asset:
make release-linux-amd64Build only the Linux ARM64 release asset:
make release-linux-arm64Clean build outputs:
make cleanUse the exact targets available in the included Makefile.
Linux build targets place the binary, local config copy, generated install script, generated update script, and generated systemd files under:
build/linux/
Expected Linux build output:
build/linux/
fbs-interlock-gateway
config.yaml
fbs-interlock-gateway.service
install.sh
update.sh
fbs-interlock-gateway-update.service
fbs-interlock-gateway-update.timer
Release targets place release binaries and SHA-256 checksum files under:
build/release/
Expected release output:
build/release/
fbs-interlock-gateway-linux-amd64
fbs-interlock-gateway-linux-amd64.sha256
fbs-interlock-gateway-linux-arm64
fbs-interlock-gateway-linux-arm64.sha256
Systemd service and update files are generated from templates in services/.
Templates:
services/app.service.in
services/install-linux.sh.in
services/update-linux.sh.in
services/update.service.in
services/update.timer.in
Generated files:
build/linux/fbs-interlock-gateway.service
build/linux/install.sh
build/linux/update.sh
build/linux/fbs-interlock-gateway-update.service
build/linux/fbs-interlock-gateway-update.timer
Do not manually edit generated files. Edit the templates or Makefile variables instead.
Current app name:
APP := fbs-interlock-gatewayDefault install directory:
/opt/fbs-interlock-gateway
Default service user:
fbs-gateway
Create a local config if needed:
make init-configRun locally:
go run . -config config.yamlOr specify the admin UI address explicitly:
go run . -config config.yaml -admin 127.0.0.1:18090Open the admin UI:
http://127.0.0.1:18090
Test the admin API:
curl -s "http://127.0.0.1:18090/api/config"
curl -s "http://127.0.0.1:18090/api/status"Test the interlock directly first:
curl "http://<interlock-host>/rpc/Switch.GetStatus?id=<switch-id>"
curl "http://<interlock-host>/rpc/Switch.Set?id=<switch-id>&on=true"
curl "http://<interlock-host>/rpc/Switch.Set?id=<switch-id>&on=false"Then test through the gateway:
curl "http://<gateway-host>:<port>/status"
curl "http://<gateway-host>:<port>/on"
curl "http://<gateway-host>:<port>/off"Expected gateway response:
{"Success":1,"State":1}or:
{"Success":1,"State":0}The admin UI can request a restart after saving config. On Linux, systemd should restart the service. On macOS, go run will simply exit.
To simulate production restart behavior locally:
while true; do
go run . -config ./config.yaml
echo "gateway exited; restarting in 2 seconds..."
sleep 2
doneBuild the Linux target for the deployment machine:
make build-linux-amd64or:
make build-linux-arm64Copy the generated Linux build folder to the deployment machine, then run the generated installer:
cd build/linux
./install.shIf installing manually, create the install directory:
sudo mkdir -p /opt/fbs-interlock-gatewayCopy the binary and local config:
sudo cp build/linux/fbs-interlock-gateway /opt/fbs-interlock-gateway/fbs-interlock-gateway
sudo cp config.yaml /opt/fbs-interlock-gateway/config.yaml
sudo chmod +x /opt/fbs-interlock-gateway/fbs-interlock-gatewayInstall the generated systemd service:
sudo cp build/linux/fbs-interlock-gateway.service /etc/systemd/system/fbs-interlock-gateway.service
sudo systemctl daemon-reload
sudo systemctl enable fbs-interlock-gateway.service
sudo systemctl restart fbs-interlock-gateway.serviceCheck status:
systemctl status fbs-interlock-gateway.service
journalctl -u fbs-interlock-gateway.service -fOn startup, the gateway:
- Loads
config.yamlfrom the path provided with-config. - Applies default values when fields are omitted.
- Starts the local admin UI unless disabled.
- Starts one HTTP server per enabled tool.
- Maps each gateway port to one configured interlock.
- Logs inbound FBS requests.
- Logs outbound FBS responses.
- Shuts down cleanly on interrupt, systemd stop, or admin restart request.
If a tool is disabled, the gateway skips that listener.
If an interlock cannot be reached, the gateway logs the error and reports the configured safe state back to FBS.
Incoming FBS requests are logged with:
FBS_IN
Outgoing FBS responses are logged with:
FBS_OUT
When running under systemd:
journalctl -u fbs-interlock-gateway.service -fThese files must stay ignored:
build
config.yaml
config.yaml.bakDo not commit:
- real hostnames
- IP addresses
- usernames
- passwords
- internal network names
- deployment-specific interlock mappings
- generated config backups
Only commit safe source files, examples, templates, and documentation.
The current implementation uses Shelly-style HTTP RPC. Keep the gateway and interlocks on the intended internal network.
The admin UI can edit config.yaml and request a restart. Keep it bound to 127.0.0.1 unless you have a specific reason to expose it and have applied firewall or access-control protections.
FBS-facing tool ports should be restricted so only the FBS server can reach them.
- One gateway process can manage multiple interlocks.
- Each interlock gets its own gateway listener port.
- FBS communicates only with the gateway.
- The gateway communicates with each interlock using HTTP RPC.
- Real deployment mappings live only in local
config.yaml. - The web UI is embedded into the binary.
- The admin UI defaults to
127.0.0.1:18090. - The systemd service and update files are generated from templates.
- The generated service name and executable path are based on the Makefile
APPvalue.