Hi
I was working on an old idea of a “Soft PLC” written on top of Elixir. It was too much work but with the advent of better AI I thought maybe it could be possible to explore the idea cheaply.
My library is called Xignal and is not yet public. I am looking for other people into industry/automation who may be interested in discussing this before I make the project public. To give a taster, the code could look like this:
# A sump pump station: a level transmitter and three contacts on a Modbus TCP I/O module, a pump tha
t
# starts at a high level and stops at a low one, three alarms, and an OPC UA server for the HMI with
the
# alarms as conditions.
#
# elixir pump_station.exs
#
# then browse opc.tcp://localhost:4840 with an OPC UA client such as UaExpert.
Mix.install([{:xignal, github: "tallakt/xignal"}, {:yaopcua, "~> 0.2"}])
defmodule Station do
use Xignal
xignal pump(raw, high_switch, overload, running, auto, start_at, stop_at, ack, alarms) do
level = raw / 100
run = auto and not overload and sr(level > start_at, level < stop_at)
{_q, starts} = ctu(run, false, 1_000_000)
high = Xignal.Alarm.check(alarms.high, ton(high_switch or level > 95.0, ~t(5s)), ack)
trip = Xignal.Alarm.check(alarms.overload, overload, ack)
stuck = Xignal.Alarm.check(alarms.no_start, ton(run and not running, ~t(5s)), ack)
[level: level, run: run, starts: starts, alarm: high[:on] or trip[:on] or stuck[:on]]
end
end
io = Xignal.Modbus.new("127.0.0.1", port: 1502, scan: Xignal.t("100ms"))
level = Xignal.Modbus.input(io, input_register: 0, desc: "Level, 0.01 %")
high_switch = Xignal.Modbus.input(io, discrete_input: 0, desc: "High level switch")
overload = Xignal.Modbus.input(io, discrete_input: 1, desc: "Pump overload")
running = Xignal.Modbus.input(io, discrete_input: 2, desc: "Pump running")
alarms = Xignal.Alarm.new(:station)
points = %{
high: Xignal.Alarm.define(alarms, :level_high, desc: "Sump level high", severity: :high),
overload: Xignal.Alarm.define(alarms, :pump_overload, desc: "Pump overload", severity: :critical),
no_start: Xignal.Alarm.define(alar"Pump did not start")
}
hmi = Xignal.OPCUA.Server.new(port: 4840, alarms: :station)
percent = [range: {0.0, 100.0}, unit
auto = Xignal.OPCUA.Server.input(hmi, "Station/Auto", default: true)
start_at = Xignal.OPCUA.Server.input, [default: 80.0] ++ percent)
stop_at = Xignal.OPCUA.Server.input(hmi, "Station/StopLevel", [default: 20.0] ++ percent)
ack = Xignal.OPCUA.Server.input(hmi,fault: false)
Station.pump(level, high_switch, ovet_at, stop_at, ack, points)
|> Xignal.Modbus.sink(:run, Xignal.Modbus.output(io, coil: 0, desc: "Pump run"))
|> Xignal.OPCUA.Server.sink(:level, (hmi, "Station/Level", [default: 0.0]
++ percent))
|> Xignal.OPCUA.Server.sink(:run, Ximi, "Station/PumpRunning", default: fa
lse))
|> Xignal.OPCUA.Server.sink(:starts,t(hmi, "Station/PumpStarts", default:
0))
|> Xignal.OPCUA.Server.sink(:alarm, (hmi, "Station/Alarm", default: false)
)
Xignal.Modbus.print_io_list(io)
Xignal.Runner.run_forever([io, alarm)
The plan is to make Xignal open source once I get the necessary “go” from my employer.
Xignal has
- many IO servers supported, as client and server (modbus, bacnet, OPC-UA,)
- handles state changes for you allowing safe edits and hot reload
- had many primitives you would expect to see in PLS software (eg. ton(..) for signal time on)
- Is pure Elixir so you could meld it with anything else, like a web service
The yaopcua library came out as something I wanted to support Xignal, but in a separate library. I didn’t want to use the existing OPC client that was depending on C in the background, but rather have this project pure Elixir/Erlang. For a PLC type of application, you really want to avoid crashes. Anyway, the `yaopcua` library is already on hex.pm and should work well as far as I can tell. It has not been thoroughly tested outside the tests in the git repo.
If anyone would like to try it out who has access to OPC-UA hardware that would be interesting feedback. Obviously getting this from project idea + AI into something tested in a few real situations is a bigger task, that I am now starting to explore.
This is meant as a “pre annouce” post.


















