Memcached: fast lightweight network cache

For many projects an SQL database is overkill for simple storage of values. As an alternate solution there are a number of excellent distributed in-memory caching systems that can be used.

Two of the most popular in-memory caching systems are Redis and Memcached.

I’m big fan of Redis, and I’ve enjoyed doing projects with it. Redis offer some awesome speed with a small footprint, and it has many features that make it even superior to a messaging system like MQTT.

However if you’re looking for something 100% dead simple you should take a look a memcached, it has a super simple setup and with only a dozen commands so you’ll get up and running in no time.

There are API’s in all the common programming languages. It only takes 1 line of Bash to read or write to memchached.

Getting Started

Memached can be installed on all major OS’s. To install it on Ubuntu/Raspberry Pi:

sudo apt-get install memcached

If you’re using Docker there are some lightweight memcached images (89MB) that can be used. To run the memcached docker image the –net host option should be used:

$ sudo docker run -it --net host memcached

The -it (interactive) option could be useful if there are any errors kicked out.

See the man pages for a full description of memcached options.

Telnet can be used to enter manual enter commands (port 11211 is the default):

$ telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
version
VERSION 1.6.12

lru_crawler metadump all
key=mynum exp=-1 la=1642363105 cas=2 fetch=no cls=1 size=66
END
quit
Connection closed by foreign host.

For more information on the commands and Telnet.

Bash Interface to Memcached

There are lot of help guides for other Python, PHP, GO etc., but not a lot on how to use Bash.

The Bash nc command can be used to read and write to socket. The -q option will close the socket after 0 seconds (after the echo command is sent). The default port is 11211 but this can be changed along with adding some security options. The first example is to use the stats command:

$ echo "stats" | nc -q 0  192.168.0.120 11211 
STAT pid 1
STAT uptime 722
STAT time 1642359428
STAT version 1.6.12
...
END

$ # Find the number of current key items
$ echo "stats" | nc -q 0  192.168.0.120 11211 | grep curr_items
STAT curr_items 1

Set/Get a Key and Value

To set an in-memory key-value store, the syntax is:

set mykey <flags> <ttl> <size>
value

The flags option is typical set to 0. The ttl “time to live” is in seconds, a ttl of 0 is indefinite.The size of the value also needs to be define. (This is taken care of in the Python, PHP… libraries). It’s important to note that a newline (\r\n) is required before the value.

To set a key to a variable mynum with a value of 55 and a indefinite time to live:

$ # hard code the arguments in a set key command
$ # Note: echo -e is used to pass the \r\n for new lines
$ echo -e "set mynum 0 0 2 \r\n55\r" | nc -q 0  127.0.0.1 11211
STORED

A more flexible approach would be to pass variables:

$ # Set a new key/value pair, with no flags and indefinite timeout
$ ipp="192.168.0.120 11211"
$ thekey="mykey1"
$ thenum=55
$ numsize=${#thenum}
$ # send command 
$ echo -e "set $thekey 0 0 $numsize\r\n$thenum\r" | nc -q 0  $ipp
STORED

Get a key/value

The get key command returns 3 lines with the value being the 2nd line. Some awk code can be used to parse out just the value.

$ # Get a new key/value pair
$ ipp="192.168.0.120 11211"
$ thekey="mykey1"
$ 
$ echo "get $thekey" | nc -q 0  $ipp
VALUE mykey1 0 2
60
END
$ # Get just the 2nd line with the value
$ echo "get $thekey" | nc -q 0  $ipp | awk '{if (NR == 2) print $0}'
55
$ 
$ # Store the result in a variable
$ mykey1=$(echo "get $thekey" | nc -q 0  $ipp | awk '{if (NR == 2) print $0}')
$ echo "mykey1 = $mykey1"
mykey1 = 55

Increment/Decrement a Value

The inc / decr commands will increase or decrease a numeric stored key value by a defined amount:

$ #incr/decr a value's number
#
ipp="192.168.0.120 11211"
thekey="mykey1"

# Get starting value
echo "get mynum" | nc -q 0 $ipp | awk '{if (NR == 2) print $0}'
55

thediff=10; # increase the value (only positive values)
# incr and show the value
echo "incr $thekey $thediff" | nc -q 0  $ipp
65

thediff=5; # decrease the value (only positive values)
# incr and show the value
echo "decr $thekey $thediff" | nc -q 0 $ipp
60

Prepending and Appending

Memcached does not have queue or list functionality, if you need this take a look at Redis.

Below is an example script that creates a diagnostic log in a key/value. The append command adds msg text to the end of the overall string.

#!/usr/bin/bash
#
# diagmsg.sh - append msgs to a memcached variable string
#
ipp="192.168.0.120 11211"
msg="1:00 - Base Software Loaded\r"
size=${#msg}

echo -e "set mymsg 0 0 $size\r\n$msg\n\r" | nc -q 0  $ipp

# Create an array of diagnostics
diagmsgs=("2:00 - System Started\r" "2:15 - Getting Data\r" "2:30 - Backing up\r") 
# Append array to value text
for msg in "${diagmsgs[@]}"
do
  size=${#msg}
  #echo "Size: $size $msg";
  echo -e "append mymsg 0 0 $size\r\n$msg\n\r" | nc -q 0  $ipp;
done

# Show the result
echo -e "\nDiagnostic Results\n" 
echo "get mymsg" | nc -q 0 $ipp 

The results of this script would be:

$ bash diagmsg.sh
STORED
STORED
STORED
STORED

Diagnostic Results

VALUE mymsg 0 92
1:00 - Base Software Loaded
2:00 - System Started
2:15 - Getting Data
2:30 - Backing up

END

Final Thought

I played with the Python and PHP library and they were quite easy to use.

By adding a Bash interface to memcache it allows me to use programs like Octave/Matlib where a native interface isn’t available (just use the System call and pass the Bash code).

Sample Code: PHP

PHP example:

<?php
// memc.php - test memcached
//
// Load the PHP memcached component:
//
//    sudo apt-get install php-memcached

$memcache = new Memcached();
$memcache->addServer('localhost', 11211) or die ("Could not connect");

$version = $memcache->getVersion();
var_dump( $version);

$tmp_object = new stdClass;
$tmp_object->str_attr = 'test';
$tmp_object->int_attr = 123;

$memcache->set('key', $tmp_object);
echo "Store data in the cache (data will expire in 10 seconds)<br/>\n";

$get_result = $memcache->get('key');
echo "Data from the cache:<br/>\n";

var_dump($get_result);

?>

Sample Code: Python

Python example:

$ # Install Python memcache library
$ python3 -m pip install pymemcache

$ # Example set/get
$ python3
Python 3.8.10 (default, Nov 26 2021, 20:14:08) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pymemcache.client import base
>>> client = base.Client(('localhost', 11211))
>>> client.set('mykey','teststring')
True
>>> client.get('mykey')
b'teststring'
>>> client.set('mynum',44)
True
>>> client.get('mynum')
b'44'

Gnuplot Speedometer Gauge

Gnuplot is a great package that allows you to do charting from the command line.

Gnuplot has a wide range of plot types but unfortunately a speedometer gauge is not one of them.

This blog documents my notes in creating a dynamic gauge.

Some useful takeaways:

  • A gnuplot can be created with only graphic elements (no chart)
  • A named object can be re-positioned without redrawing the background
  • A gnuplot script can be called like a Bash or Python script with the first line being: #!/usr/bin/gnuplot

A Dynamic Gauge

The script gnuplot.gp is an example that refreshes a gauge chart every 5 seconds with the processor’s idle time. The idle time is obtained using the Linux top command (with some awk to get the 8th line item).

#!/usr/bin/gnuplot
#
# gnuplot.gp - speedometer dial with title as the description/value
#
set xrange [-1:1]
set yrange [0:1]
set angles degrees
set size ratio -1
# r1 = annulus outer radius, r2 = annulus inner radius
r1=1.0
r2=0.5
unset border; unset tics; unset key; unset raxis

set style fill solid noborder

# define a "needle" pointer as object 1
set object 1 circle at first 0,0 front size r1 arc [181:182]  fillcolor rgb 'black'

# define the gauge background
set object circle at first 0,0  size r1 arc [0:180]  fillcolor rgb 'green'
set object circle at first 0,0  size r1 arc [0:10]  fillcolor rgb 'red'
set object circle at first 0,0  size r1 arc [10:20]  fillcolor rgb 'yellow'
set object circle at first 0,0 front size r2 arc [0:180] fillcolor rgb 'black'

# plot the static background 
plot -10 notitle

# Define a partial heading  
variable = "Idle Time\n"
unit = "%"

# Refresh the plot with a new dial setting
while (1) {
  # Get the idle time using the top utility
  idle = system("top -n 1 | grep Cpu | awk '{print $8}'")
  # scale the value from 0-100 to 180-0 (Note: arc starts on the right)
  value = (100 - real(idle)) * 1.8
  # show the value in the title
  set title sprintf('%s %s %s', variable, idle, unit)  font "Ariel,28"
  # reposition the value in the gauge
  set object 1 circle at first 0,0 front size r1 arc [value:(value+2)]  fillcolor rgb 'black'
  replot
  pause 5 
}

The script can be run either from gnuplot or like a Bash script:

$ # run script from gnuplot
$ gnuplot -c gauge.gp
Use Control-C to exit ...
^C
$ # make script executable
$ chmod +x gauge.gp
$ # run script like a Bash script
$ ./gauge.gp
Use Control-C to exit ...
^C

Next Steps…

From here the next steps could be to add command line arguments to make the script more generic, such as:

  • pass the calculation to run (eg. pass the bash top command)
  • pass the scaling, title and units

Julia programming on a Raspberry Pi

Julia is a free and open-source general purpose programming language with an emphasis on scientific computing. Like Python and R, Julia can be used in Jupyter notebooks.

Julia was released in 2012 and compared to the popularity of other programming languages Julia ranks fairly low #24 , (behind Lua and ahead of Perl). Julia appears to be gaining popularity with an 87% increase last year in user downloads.

Some of the reasons that I wanted to look at Julia:

  • Julia scripts can be compiled or run from command line
  • Statistical and plotting libraries are built into Julia
  • Code from : Python, Matlab, Lua can be used directly in Julia
  • Libraries from Java, C, Python
  • Python code can be embedded in Julia script

In this blog I wanted to document my Julia testing on a Raspberry Pi.

Some key take-aways:

  • Integration with Python is awesome. I was able to use Rasp Pi LED’s without any issues.
  • Julia has some nice plotting options
  • Julia is very very slow the first time a new library is loaded from the command line.

Getting Started

Julia is supported on Windows, MacOS and Linux. See the download documentation for your specific system at : https://julialang.org/downloads/. To install Julia on a Raspberry Pi enter:

sudo apt install julia

To check your install, you can run Julia at the command line:

pi@raspberrypi:~ $ julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.3
 _/ |\__'_|_|_|\__'_|  |  Raspbian ⛬  julia/1.5.3+dfsg-3+rpi1
|__/                   |

julia> 

Julia packages can be installed at the command line by:

$ julia --eval "using Pkg; Pkg.add('some_package')"

Or within Julia by:

julia> using Pkg
julia> Pkg.add("some_package")

For my setup I used a Pi 4 with a proto shield top and I wired in an LED, a 4 digital tm1637 module and a 2 line I2C LED screen.

Julia with Raspberry Pi GPIO

A Raspberry Pi package “PiGPIO” is installed by:

julia> using Pkg
julia> Pkg.add("PiGPIO")

Once the package is installed a daemon ( pigpiod) is created that the Julia script connects to. The pigpiod daemon is started by:

sudo pigpiod

Below is some code to write to and read from a Pi GPIO pin:

julia> using PiGPIO
julia> ledpin = 17;
julia> p = Pi();
julia> set_mode(p,ledpin, PiGPIO.OUTPUT);
julia> PiGPIO.write(p, ledpin, PiGPIO.HIGH);
julia> PiGPIO.read(p, ledpin)
1

Note: adding a semi-colon at the end of a line will suppress debug information.

GUI’s with Gtk

Like most programming languages Julia has a number of GUI options. For my testing I used the Gtk library which feels quite familiar to the Python Tkinter GUI library.

To install Gtk in Julia:

julia> using Pkg
julia> Pkg.add("Gtk")

Below is a Julia script to create a 2 button window that will turn on/off a GPIO pin:

# 
# Gtk_17.jl - Use a Gtk GUI to toggle a PI GPIO LED
#
using Gtk;
using PiGPIO;

# Setup GPIO 
p=Pi();
led_pin = 17;
set_mode(p, led_pin, PiGPIO.OUTPUT) 

# Init GUI objects
win     = GtkWindow("Toggle a Pi LED", 300, 300);
g       = GtkGrid();

# Text label
label   = GtkLabel(string("Pin : $led_pin"));
# Buttons
btn_on  = GtkButton("ON");
btn_off  = GtkButton("OFF");

# Put GUI widgets into a grid
g[1:2,1] = label;   # Cartesian coordinates, g[x,y]
g[1,2] = btn_on;
g[2,2] = btn_off;

# Show the GUI Window
push!(win, g);
showall(win);

# Callbacks
function turn_on(w1)
	PiGPIO.write(p, led_pin, PiGPIO.HIGH)
	println("ON button pressed")
end
signal_connect(turn_on, btn_on, "clicked")

function turn_off(w2)
	PiGPIO.write(p, led_pin, PiGPIO.LOW)
	println("OFF button pressed")
end
signal_connect(turn_off, btn_off, "clicked")

# Close cleanly
signal_connect(win, :destroy) do widget
    Gtk.gtk_quit()
end
Gtk.gtk_main()

Running a Julia Script

To run the Julia script with some debug and compiler options minimized:

julia -O0 –compile=min –startup=no Gtk_17.jl

Julia Web Server

There are a few web server options. I found that the Genie web frame work was easy to get up and running with minimal code and reading.

Below is an example to call a static web page with a form to turn a GPIO pin on/off:

#
# web2.jl - Julia Web Page with routes to turn on/off a Pi GPIO pin
#
using Genie
using Sockets # to get IP address
using PiGPIO;

# Setup GPIO 
p=Pi();
led_pin = 17;
set_mode(p, led_pin, PiGPIO.OUTPUT) 

# get the Rasp Pi IP address
myip = string(getipaddr() )

println("Loading Genie...")

# get the HTML document
s = open("webjulia.html") 
html_file = read(s, String)

route("/") do
  return html_file
  println("Connected")
end

route("/ON?") do
  PiGPIO.write(p, led_pin, PiGPIO.HIGH)
  return html_file
  println("ON Selected")
end

route("/OFF?") do
  PiGPIO.write(p, led_pin, PiGPIO.LOW)
  return html_file
  println("OFF Selected")
end

# Start the web app on port 8001
up( 8001, myip, async = false)

The HTML page is:

<html>
<head>
<title>Julie GPIO Test Page</title>
</head>
<body>
  <h1>Toggle PI LED</h1>
  <form>
    <button type="submit" formaction="/ON">ON</button>
    <button type="submit" formaction="/OFF">OFF</button>
  </form>
</body>
</html>

To start the Julia script enter:

julia -O0 --compile=min --startup=no web2.jl

Julia calling Python

I thought that it was pretty amazing that script languages like Lua, Matlab, R and Python could be integrated directly into a Julia script.

To enable this functionality the first step is to install the required library. For example to add the Python interface from the command line:

$  julia --eval "using Pkg; Pkg.add('PyCall')"

The PyCall library allows for Python libraries to be used in Julia or Python code can be run directly. An example to run Python code directly to write text to a 2 line I2C LED screen:

julia> using PyCall

julia> py"""
       # Use Python to write text to a 2 line LCD screen
       from rpi_lcd import LCD
       lcd = LCD()
       lcd.text('Hello World!', 1)
       lcd.text('Raspberry Pi', 2)
       """
julia> # Now back into Julia

To do the same functionality using a Python library in Julia:

using PyCall
rpi_lcd = pyimport("rpi_lcd")
lcd = rpi_lcd.LCD()
lcd.text("Hello from Julia", 1)
lcd.text("Raspberry Pi!", 2)

Functions can be created in Python and then called in Julia. An example to call a Python Tkinter window:

using PyCall

py"""

import tkinter as tk

def show_win():
	window = tk.Tk()
	tk.Label(text="Python TK Window from Julia").pack()
	window.mainloop()
"""
py"show_win"()

Plotting

Julia has some good plotting libraries and examples, but they are slow to come up the first time.

I did a dynamic example that plots the Pi’s CPU temperature every 1/4 second.

This example shows a couple of different Julia syntax items:

  • the pipeline command is used for calling Bash command with pipes
  • backtick notation (`) is used for command strings (eg. `echo hello`)
  • append to an array with push!
  • remove the first item in an array with popfirst!
#
# plot_temp.jl - create a dynamic plot of CPU temperature
#
using Plots
println("Show Pi CPU temperature on a plot...")
cputemp = Vector{Float64}()

gr(); # use the GR backend for plotting
display(plot(cputemp))

println("Cycle every 1/4 seconds and add a new temp to the plot...") 
for i = 1:200
   # get the Pi GPU temperature
   t = read(pipeline(`vcgencmd measure_temp`,`awk '{ print substr($1,6,4) }'`), String)
   push!(cputemp, parse(Float64,t) )
   # Limit the array to 10 values
   if length(cputemp) > 50
     popfirst!(cputemp)
   end
   #println(cputemp)   
   display(plot(cputemp, title = "Raspberry Pi CPU Temperature", label = "Deg C"));
   sleep(0.25)
end

As mentioned earlier loading the Plots package the first time is quite slow, if you are working in the Julia command mode you can cut and paste code in without calling “using Plots” multiple times. I found that this greatly improved my testing time. To call a script into the Julia command mode:

julia> include("my_script.jl")

Final Comments

As a Python user l found using Julia PyCall to be great way to re-use existing code.

There are some great features in Julia but until first time call up time is improved it will be frustrating using Julia. I looked at compiling Julia into an executable but unfortunately this isn’t an easy one-step operation.

6 Lines of Python to Plot SQLite Data

There are some great tutorials on SQL, SQLite and Matplotlib, however I wanted something super simple that I could use to teach my daughters with.

This blog documents my notes on what I used to teach them SQL, SQLite3 and then how to plot the results in Python. Yes it can only takes 6 lines of Python code to read an SQLite3 file and plot it, if you are doing 2 line charts with a title and legends its 9 lines.

Getting Started with SQL

There are some great SQL databases and SQL tools that can are quite user friendly.

For us I started with SQLite3 which is file based, no server component is required and it runs on all major OS’s and Raspberry Pi’s. There are a number of SQL Query builder tools but we found the DB Browser for SQLite gave us all the functionality we needed.

Check the install documentation for your hardware/OS, to install SQLite3 and DB Browser on Debian/Ubuntu/Raspberry Pi’s :

sudo apt install sqlite3
sudo apt-get install sqlitebrowse

SQLite Databases

For testing there are a lot of excellent databases that can downloaded from Kaggle. These data files are in CSV format and they can be imported into a SQLite database file using the DB Browser.

For our first test we used the Kaggle SARS 2003 data set.

A new database file (sars.db) was created and then the CSV was imported.

Create VIEWS to Simplify

SQL Views can be created to simply the field names, reduce the number of fields or add custom fields.

Use the “Execute SQL” tab to create a view. An example to create a view with simplified field names would be:

Views and SELECT queries can be generated to add custom fields. An example to extract the month from the date field and add 2 other fields (month number and month name):

Test SQL Queries

The DB Browser tool is good for testing out SQL queries and plot the data before moving to Python.

A query to find the worst 5 countries affected by SARS would:

select sum(deaths) sum_deaths, country from v_sars group
  by Country having sum_deaths > 0 order by sum_deaths desc limit 5

Plotting in Python

There are number of ways to plot SQL data in Python. I found that the easiest way was to use Pandas dataframes.

To load the necessary Python libraries:

pip install sqlite3
pip install pandas
pip install matplotlib

The Python code to connect to SQLite3, run the SQL query (of sum of deaths vs. country) and plot the data is:

#
# sql_bar0.py - Sqlite to Bar Charts
#
import sqlite3, pandas , matplotlib.pyplot as plt

conn = sqlite3.connect("/media/pete/RED_4GB/sql/sars.db")

sql = """select sum(deaths) sum_deaths, country from v_sars group
  by Country having sum_deaths > 0 order by sum_deaths desc limit 5"""

data = pandas.read_sql(sql, conn)
#x values: data.Country,  y values: data.sum_deaths
plt.bar(data.Country, data.sum_deaths)
plt.title("SARS Death in 2003")
plt.show()

An example with 2 lines to shows the monthly deaths and cases would be:

#
# sql_line2.py - Sqlite to 2 Line Charts
#
import sqlite3, pandas , matplotlib.pyplot as plt

conn = sqlite3.connect("/media/pete/RED_4GB/sql/sars.db")

sql = """select s_month, sum(deaths) as sum_deaths, sum(cases) as sum_cases from v_month group by n_month"""

data = pandas.read_sql(sql, conn)

plt.plot(data.s_month,data.sum_deaths, label = "Deaths")
plt.plot(data.s_month,data.sum_cases, label = "Cases")
plt.legend()
plt.title("SARS Death in 2003")
plt.show()

Summary

By keeping the Python code simple we were able to focus on SQL queries.

Using the basic Python code the SQL connection we later changed from SQLite3 to MySQL or Progresql.

Octave: A Free Matlab Alternative

Matlab has been an extremely popular tool for Engineering and Mathematical problem solving. Matlab has a wide range of practical applications and use-cases. It is also used in complex industrial applications such as multi-variable control in chemical reactors.

Matlab is a full featured commercial product that has a perpetual license for $2,150 US (or $860 yearly). Student pricing, (starting at $49 US, add-ons extras ), home pricing ($149 US, add-ons extra) and 30 day trials are available for non-commercial projects.

Octave is an open source alternative to Matlab that runs on GNU/Linux, macOS, BSD, and Microsoft Windows. The Octave syntax is largely compatible with Matlab.

I wish I had known about Octave when I was trying to help my kids with their high school Algebra and Calculus homework. In this blog I was look at introducing Octave with some high school math questions. I will finish with a quick data analysis example and a sensor example.

Getting Started with Octave

Octave/Matlab has a lot of potential applications and it can be loaded on a standard PC for math/stats projects or on a Raspberry Pi for projects that use GPIO (General Purpose Inputs and Outputs) to connect to sensors and I/O devices.

See the Octave web site (https://www.gnu.org/software/octave/download) for installation details for your specifics system. To load Octave on a Raspberry Pi/Ubuntu/Debian:

# load the base Octave software
sudo apt-get install octave
# add some of the key Octave packages
sudo apt-get install octave-control octave-image octave-io octave-optim octave-signal octave-statistics

The Octave Forge site (https://octave.sourceforge.io/packages.php ) has a list of useful community based libraries that can be added. To load a custom library, first install any dependencies then use the Octave pkg (package) command to install and load new library. For example to load the Symbolic library:

$ # install package dependencies
$ pip3 install sympy==1.5.1
$ octave
octave:1> # install symbolic package from forge site
octave:1> pkg install -forge symbolic
octave:2> # make package available by loading
octave:2> pkg load symbolic
octave:3> # show installed and loaded (*) packages
octave:3> pkg list
Package Name  | Version | Installation directory
--------------+---------+-----------------------
     control  |   3.2.0 | /usr/share/octave/packages/control-3.2.0
   dataframe  |   1.2.0 | /home/pete/octave/dataframe-1.2.0
       image  |  2.12.0 | /usr/share/octave/packages/image-2.12.0
          io  |  2.4.13 | /usr/share/octave/packages/io-2.4.13
       optim  |   1.6.0 | /usr/share/octave/packages/optim-1.6.0
      signal  |   1.4.1 | /usr/share/octave/packages/signal-1.4.1
  statistics  |   1.4.1 | /usr/share/octave/packages/statistics-1.4.1
      struct  |  1.0.16 | /usr/share/octave/packages/struct-1.0.16
    symbolic *|   2.9.0 | /home/pete/octave/symbolic-2.9.0

Octave GUI

Octave can be run in either a command line or a GUI mode.

The command line mode allows for basic testing and it is good for slower hardware like a Raspberry Pi 3.

The Octave GUI, which is called in Linux by:

$ octave --gui 

The Octave GUI offers a development environment with a script debugger and online help.

Solve Quadratic Equations

The power of Octave or Matlab can shown in it’s handling of Algebraic equations.

A typical high school math question would be to find where 2 equations intersect. Below is an example to solve where:

2*x^2 - 8*x - 4 = 3*x -x^2

Some comments on Octave/Matlab syntax, an exponent is define by: “.^” , and a “==” denotes that symbolic equation1 = equation2. A single “=” sets a variable to a value. If a line ends in “;” the lines results are hidden, otherwise the lines results are shown. The Octave prompt can be simplified by the command: PS1(“>> #”) .

>> #Solve for the intercept between 2 equations
>> pkg load symbolic
>> syms x
>> eq1 = 2*x.^2 - 8*x - 4 == 3*x -x.^2
eq1 = (sym)

     2                2      
  2⋅x  - 8⋅x - 4 = - x  + 3⋅x

>> solve(eq1)
ans = (sym 2×1 matrix)

  ⎡-1/3⎤
  ⎢    ⎥
  ⎣ 4  ⎦
>> # Show the number numerically
>> double(ans)
ans =

  -0.33333
   4.0
>># Plot the first equation
>> ezplot(@(x) 2*x.^2 - 8*x - 4 )
>> # Use "hold on" so the first plot isn't erased
>> hold on;
>> ezplot(@(x) 3*x -x.^2 )
>> title ('2*x.^2 - 8*x - 4 = 3*x -x.^2')

Balance Chemical Equations

An example of balancing chemical equations, would be to determine how much methane (CH4 ) and oxygen (O2) would burn cleanly to water(H20) and carbon dioxide (CO2):

CH4 + O2 → CO2 + H2O

To determine the actual amounts of each element variables X1-X4 are added:

x1(CH4) + x2(O2) → x3(CO2) + x4(H2O)

This equation can be broken down by each element:

Carbon (C): 1*x1 + 0*x2 = 1*x3 + 0*x4
Hydrogen (H): 4*x1 + 0*x2 = 0*x3 + 2*x4
Oxygen (O): 0*x1 + 2*x2 = 2*x3 + 1*x4

This equation can be rewritten as:

1*x1 + 0*x2 – 1*x3 + 0*x4 = 0
4*x1 + 0*x2 – 0*x3 – 2*x4 = 0
0*x1 + 2*x2 – 2*x3 – 1*x4 = 0

Now there are 3 equations and 4 variable. To solved these equations, as a first pass x4=1 (now there are 4 equations). The 4 equation can be defined in a matrix equation of : Ax = b:

To solve this in Octave/Matlab the matrices are A and b are defined then the equation is rearranged to:

x = A-1 *b

>> # Balance equation of: Methane + Oxygen -> Carbon Dioxide + Water
>> # Create a Matrix A of elements
>> A = [
1 0 -1 0
4 0 0 -2
0 2 -2 -1
0 0 0 1];

>> # Create a Matric b of results
>> b = [
0
0
0
1];

>> # Ax = b, or x=inverse(A) * b, solve for x
>> x = inv(A) *b
x =

   0.50000
   1.00000
   0.50000
   1.00000

>> # Multiple by 2 to get whole numbers
>> x = 2*x
x =

   1
   2
   1
   2

Originally a guess of x4 = 1 was used, this gave a result with decimal/fractional values. By setting X4 = 2 (multiple results by 2) whole values can returned. The final balanced equation is:

CH4 + 2O2 → CO2 + 2H2O

Dataframes

An Octave dataframe (similar to a Python Pandas dataframe) is used to store rows and columns of data.

Dataframes can be used to filter, sort and do stats on data files.

For my test project I used the data from Wikipedia on the tallest buildings (https://en.wikipedia.org/wiki/List_of_tallest_buildings), and I created a csv file called: buildings.csv .

CSV files can be imported into dataframe objects. I found that for some files the dataframe import failed, as a work around I imported the CSV file first into a csv2cell object.

Below is an example of imported a CSV file and showing the first 6 rows in a bar chart:

>> pkg load dataframe
>> c = csv2cell("buildings.csv");
>> df = dataframe(c);
>> # show the first 3 rows and all the columns
>> df(1:3, : )
ans = dataframe with 3 rows and 6 columns                                
_1   rank           name height theyear         city              country
Nr double           char double  double         char                 char
 1      1   Burj Khalifa   2717    2010        Dubai United Arab Emirates
 2      2    Merdeka 118   2227    2022 Kuala Lumpur             Malaysia
 3      3 Shanghai Tower   2073    2015     Shanghai                China
>>
>> # create a bar chart with the first 6 tallest buildings, show height and name 
>> bar(df(1:6,['height']) )
>> set(gca,'XTickLabel',df(1:6,['name'])

Dataframes can filtered based on conditions with a field. Below is an example of the tallest building in the US that was built after 2014:

>> us = df(strcmp(cellstr(df.country),'United States') & df.theyear > 2014, : )
us = dataframe with 6 rows and 6 columns                                 
_1   rank                 name height theyear          city       country
Nr double                 char double  double          char          char
15     15   Central Park Tower   1550    2020 New York City United States
28     28 111 West 57th Street   1428    2021 New York City United States
29     29       One Vanderbilt   1401    2020 New York City United States
31     31      432 Park Avenue   1397    2015 New York City United States
45     45      30 Hudson Yards   1270    2019 New York City United States
64     64    St. Regis Chicago   1191    2020       Chicago United States

Statistical functions can also be used with filters.

>> # Find the tallest building built up until 2000
>> max(df.height(df.theyear <= 2000))
ans =  1483

Data Sensor Example

Octave has a number of libraries that allow for sensor inputs from Arduino, Raspberry Pi modules and hardware interfaces such as serial, TCP sockets and I2C devices. The data can then be analysed and outputed to SQL databases, CSV files, plots, dialogs and Excel files.

For the last example a script will periodically read a sensor value then:

  • save times/values to a CSV file
  • show the last 20 values in a dynamic plot
  • show a dialog with a moving average of the last 5 values.

The Octave script (sensor1.m) is launched at the command line by: octave sensor1.m .

For this example the sensor value is simulated by using the CPU idle time that is returned from the top utility. The zenity library is loaded and used for the moving average dialog.

There are a number of way to export data to a CSV file. This example uses a simple bash echo with the results piped to sensor1.csv .

#!/usr/bin/octave
#
# sensor1.m - scan a simulated sensor, then graph and save values
#
pkg load zenity

cmd = "top -n 1 | grep %Cpu | awk '{printf $8}'";

p = zenity_progress("Sensor Value ");

# Cycle for 1000 iterations
x = [];
for i = 1:1000
  # get the sensor (idletime) value and time
  thetime = strftime("%X",localtime (time ()));
  [~, idletime] = system(cmd);
  idletime_n = str2num(idletime);
  x(end+1) = idletime_n;
  # Only graph the last 20 values
  if length(x) > 20
    x(1) = [];
  endif
  plot(x)
  title(strcat("Sensor Values   Last Value: ", idletime, "%"))
  # Show a move average of the last 5 points in a dialog
  pause (2);
  if length(x) > 5
    movavg = mean(x(end-5:end));
    the_comment = strcat(idletime, " % (Ave of last 5 samples");
    zenity_progress(p,the_comment,movavg);
  endif
  # append the time and sensor value to a text file
  outcmd = ["echo " thetime " , "  idletime " >> sensor1.csv"];
  system(outcmd);
endfor

Use Raspberry Pi Sensors/Displays

Octave can be loaded on a Raspberry Pi and then I2C, SPI and hardwired sensors and displays can be interfaced to.

Unfortunately Octave doesn’t have the native libraries for these devices but an Octave library called Pythonic can be used to access Python libraries.

The Pythonic library can be used to connected to Pi GPIO or any device that has a Python library. See the documentation for more specifics.

An example to connect to a TM1637 four digit LCD screen with the Octave pyexec commands:

>> pyexec("import tm1637")
>> tm = pyexec("tm1637.TM1637(clk=23, dio=24)")
>> pyexec("tm.temperature(44)")

Below is an example to connect an I2C LCD text display with Python objects mapped to Octave objects. The key is to first use the pyexec command to import the required Python library and then create a Python device object (lcd in this case). Once the Python object is defined the Octave object is by:

Octave_obj = py.Python_object()

octave:5> pyexec("from rpi_lcd import LCD")
octave:6> pyexec("lcd = LCD()")
octave:7> pyexec("lcd.text('Hello Octave', 1)")
octave:8> o = py.LCD()
o = [Python object of type rpi_lcd.LCD]

  <rpi_lcd.LCD object at 0xa316f058>

octave:9> o.clear()
octave:10> o.text('Hi Pete',1)
octave:11> o.text('From Octave',2)

Final Comments

For a high school or university student who needs to do some mathematical or engineer work and doesn’t have access to Matlab, Octave is a great open source alternative.

Animated SVG Graphics in Python

Home and small Python projects can use animated graphics without adding a lot of code.

In this blog I wanted to document my testing for animating SVG (Scalar Vector Graphics) in QT GUI’s.

Getting Started with SVG

There are two main types of graphics, raster based (GIF, PNG, JPEG) and vector based (SVG). Raster or bitmap based graphics are excellent for applications like photographs. Vector based graphics are good for re-scaling or application where individual items in the graphic need to be adjusted.

There are a number of open source SVG editing tools, such as Inkscape that works on Windows, Linux and Mac.

Creating SVG images from scratch can be a lot of work, luckily there are a number of sites that offer free SVG libraries. Two libraries that I used were https://www.svgrepo.com/ for smaller icon graphics and the Opto22 site for industrial images.

Python Libraries

There are a number of Python graphic libraries (Tkinter, xW, QT …). For SVG support I found that the documentation for QT to be probably the easiest to follow.

To get the Python QT and SVG libraries loaded:

sudo apt install python3-pyqt5
sudo apt install python3-pyqt5.qtsvg

To load a static SVG file:

# svg1.py - load an SVG file in QT
#
import sys
from PyQt5 import QtSvg
from PyQt5.QtWidgets import QApplication

app = QApplication(sys.argv)

svgWidget = QtSvg.QSvgWidget()

svgWidget.load('smeter.svg')
svgWidget.show()

sys.exit(app.exec_())

QT GUI apps need an instance of QApplication, which is passed the command line arguments or nothing ( [ ] ).

Next an SVG widget is created and it is loaded with a SVG file (or a string of SVG text).

The final step is to show the Widget and pass control to the QApplication object.

SVG ID’s and Viewbox’s

The SVG syntax is similar to XML or HTML. There are different types of objects, (text, rectangles, etc.) and objects are identified by ID’s.

A simple SVG example with a rectangular box and some text would be:

<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" 
    xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 200 100" xml:space="preserve">
  <svg>
  <rect id="thebox" fill="#aaa" stroke="#000" stroke-width="2" x="2" y="2" width="150" height="46"></rect>
  <text id="thetext" fill="#333" font-size="20px"  style="font-weight: bold;" y="30.5" x="5.5">Some Text</text>
  </svg>
</svg>

For this example the default SVG window size is set by the viewBox to be: “0 0 200 100” in the opening svg tag.

The rectangular box is defined with the rect tag and it is given an id=”thebox”. Similarly the text is defined using a text tag and it is given an id=”thetext”.

To get or change an SVG element, the element type is required, such as text or rect and the id is needed.

Change SVG Text

For the next example an SVG flowmeter file from the Opto22 site is loaded into the Inkscape app and a text item is added with an id of flowval.

This example’s Python program uses the lxml library to load the SVG into an etree object that can be searched with the ETXPath method.

# svg_timer.py - update an SVG text/color on a timer
#
import sys
import random
from lxml import etree
from PyQt5 import QtCore, QtGui, QtSvg
from PyQt5.QtWidgets import QApplication

def timerEvent():
    # set the flowval object text to a number 1-10
    find_text(root)[0].text = str(random.randint(1,10))
    # reload the SVG widget with the text
    svgWidget.load(etree.tostring(root))
                                  
root = etree.parse(r'flow0.svg')
# create an object for the flowval 
find_text = etree.ETXPath("//{http://www.w3.org/2000/svg}text[@id='flowval']")

app = QApplication(sys.argv)

svgWidget = QtSvg.QSvgWidget()
svgWidget.load("flow0.svg")

# Setup a timer event
timer = QtCore.QTimer()
timer.timeout.connect(timerEvent)
timer.start(1000)

svgWidget.show()

sys.exit(app.exec_())

The command : etree.ETXPath(“//{http://www.w3.org/2000/svg}text[@id=’flowval’]“), needs the know that the format of the document (http://www.w3.org/2000/svg), the type of object (text) and the id (flowval).

A QtCore.QTimer() object is created and it calls the timerEvent function every second. The timerEvent function puts a random number into the text of the flowval SVG entity.

Change SVG Item Attributes

For the last example a QT GUI is created with 2 buttons. The buttons will change the color of some solar panels and update a power reading.

The panels are found by an SVG path object type and an id of panels. The attributes of theplanel object are read and modified using the attribute[“the_attribute”] method. For example to change the color of the panels to red:

# Define SVG root object
svg_root = etree.parse(r'solarpanels.svg')
# create a panel object
thepanel = etree.ETXPath("//{http://www.w3.org/2000/svg}path[@id='panels']")
# set the fill colors of the panel to red
thepanel(svg_root)[0].attrib['fill'] = "red"

The full 2-button PyQt code is below:

import sys
# svg_2buttons.py - change solar panel colors and power text
#
from lxml import etree
from PyQt5 import QtGui, QtSvg
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout


def turn_to(thecolor,theval):
    # change the color of the panels, and power text
    thepanel(svg_root)[0].attrib['fill'] = thecolor
    thevalue(svg_root)[0].text = theval
    svgWidget.load(etree.tostring(svg_root))
    
# Define SVG info
svg_root = etree.parse(r'solarpanels.svg')

thepanel = etree.ETXPath("//{http://www.w3.org/2000/svg}path[@id='panels']")
thevalue = etree.ETXPath("//{http://www.w3.org/2000/svg}text[@id='watts']")
svg_str = etree.tostring(svg_root)

app = QApplication([])
window = QWidget()
layout = QVBoxLayout()

# Create 2 buttons to change the panel color and power text
btn_on = QPushButton('TURN ON')
btn_on.clicked.connect(lambda: turn_to("red","200 W") )
layout.addWidget(btn_on)

btn_off = QPushButton('TURN OFF')
btn_off.clicked.connect(lambda: turn_to("silver", "0 W") )
layout.addWidget(btn_off)

svgWidget = QtSvg.QSvgWidget()
svgWidget.load(etree.tostring(svg_root))

layout.addWidget(svgWidget)
window.setLayout(layout)
window.setWindowTitle("Solar Panel")
window.show()

sys.exit(app.exec_())

Final Comments

Combining SVG graphics and Python can add some useful presentations for your next project.

SVG items within a QT GUI can also be made select-able.

There are also a number of SVG charting object that can be used (https://pypi.org/project/svg.charts/ and https://www.pygal.org/en/stable/).

OPC UA Protocol with Python and Node-Red

Industrial operations such as chemical refineries, power plants and mineral processing operations have quite different communications requirements than most IT installations. Some of the key industrial communications requirements include: security, multi-vendor connectivity, time tagging and quality indications.

To meet industrial requirements a communications standard called OPC (OLE for Process Control) was created. The original OPC design was based on Microsoft’s Object Linking and Embedding (OLE) and it quickly became the standard for communications between control systems consoles, historians and 3rd party applications.

The original OPC standard worked well but it had major limitations in the areas of Linux/embedded systems, routing across WANs, and new security concerns. To better address new industrial requirements the OPC UA, (Open Platform Communications Unified Architecture) standard was created.

In this article I will create an OPC UA server that will collect sensor data using Python and Node-Red, and the results will be shown in a Node-Red web dashboard.

Install Python OPC UA Server

There are a number of OPC UA open source servers to choose from.

For “C” development applications see the Open62541 project (https://open62541.org/), it offers a C99 architecture that runs on Windows, Linux, VxWorks, QNX, Android and a number of embedded systems.

For light weight quick testing OPC UA servers are available in Python and Node-Red.

The Free OPC-UA Library Project (https://github.com/FreeOpcUa) has a great selection of open source tools for people wishing to learn and play with OPC UA.

I keep things a little simple I will be using the python-opcua library which is a pure Python OPC-UA Server and client. (Note: a more complete Python OPCUA library, https://github.com/FreeOpcUa/opcua-asyncio, is available for more detailed work). Also an OPC-UA browser is a useful tool for monitoring OPC UA server and their tags. To load both of these libraries:

# Install the pure Python OPC-UA server and client
sudo apt install python-opcua
# Install the OPC UA client and the QT dependencies
sudo apt install PyQT*
pip3 install opcua-client

Simple Python OPC-UA Server

As a first project a simple OPC-UA server will be created to add OPC-UA tags and then simulate values.

The first step in getting this defined is to set an endpoint or network location where the OPC-UA server will be accessed from.

The default transport for OPC-UA is opc.tcp. The Python socket library can be used to determine a node’s IP address. (To simplify my code I also hard coded my IP address, opc.tcp://192.168.0.120:4841).

The OPC-UA structure is based on objects and files, and under an object or file tags are configured. Tags by default have properties like value, time stamp and status information, but other properties like instrument or alarm limits can be added.

Once a tag object is define, the set_value function is used to simulate the tag values.

# opcua_server1.py - Create an OPC UA server and simulate 2 tags
#
import opcua
import random
import time
 
s = opcua.Server()
s.set_server_name("OpcUa Test Server")
s.set_endpoint("opc.tcp://192.168.0.120:4841")
  
# Register the OPC-UA namespace
idx = s.register_namespace("http://192.168.0.120:4841")
# start the OPC UA server (no tags at this point)  
s.start() 
  
objects = s.get_objects_node()
# Define a Weather Station object with some tags
myobject = objects.add_object(idx, "Station")
  
# Add a Temperature tag with a value and range
myvar1 = myobject.add_variable(idx, "Temperature", 25)
myvar1.set_writable(writable=True)
  
# Add a Windspeed tag with a value and range
myvar2 = myobject.add_variable(idx, "Windspeed", 11)
myvar2.set_writable(writable=True)
 
# Create some simulated data
while True:
    myvar1.set_value(random.randrange(25, 29))
    myvar2.set_value(random.randrange(10, 20))
    time.sleep(5)

The status of the OPC-UA server can be checked using the OPC-UA browser:

# start the Python OPC-UA browser client
opcua-client

Items within an OPC-UA server are defined by their name space index (ns) and their object index. The name space index is returned after an name space is register. An object’s index is defined when a new object is create. For this example the Windspeed tag has a NodeId of “ns-2;i=5”, or an index 5 on name space 2.

The opcua-client application can view real-time changes to a tag’s value using the subscription option.

In OPC the terms “tags” and “variables” are often used interchangeably. In the instrument lists the hardware signals are usually referred to as “tags”, but within the OPC UA server the term “variables” is used. The key difference is that a variable can also be an internal or soft point such as a counter.

Python OPC-UA Client App

For my Python client application I loaded up a simple gauge library (https://github.com/slightlynybbled/tk_tools):

pip install tk_tools

The Python client app (station1.py) defines an OPC-UA client connection and then it uses the NodeId definition of the Temperature and Windspeed tags to get their values:

# station1.py - Put OPC-UA data into gauges 
#
import tkinter as tk
import tk_tools
import opcua

# Connect to the OPC-UA server as a client
client = opcua.Client("opc.tcp://192.168.0.120:4841")
client.connect()

root = tk.Tk()
root.title("OPC-UA Weather Station 1")

# Create 2 gauge objects
gtemp = tk_tools.Gauge(root, height = 200, width = 400,
            max_value=50, label='Temperature', unit='°C')
gtemp.pack()
gwind = tk_tools.Gauge(root, height = 200, width = 400,
            max_value=100, label='Windspeed', unit='kph') 
gwind.pack()

def update_gauge():
    # update the gauges with the OPC-UA values every 1 second
    gtemp.set_value(client.get_node("ns=2;i=2").get_value())
    gwind.set_value(client.get_node("ns=2;i=5").get_value())
    root.after(1000, update_gauge)

root.after(500, update_gauge)

root.mainloop()

XML Databases

In the earlier Python OPC-UA server example tags were dynamically added when the server was started. This method works fine for simple testing but it can be awkward for larger tag databases.

All industrial control vendors will have proprietary solutions to create OPC-UA tag databases from process control logic.

Users can also create their own tag databases using XML. The OPC-UA server tag database can be imported and exported to XML using the commands:

# to export from the online system to an XML file:
# where: s = opcua.Server()
s.export_xml_by_ns("mytags.xml")
# to import an XML file:
s.import_xml("mytags2.xml","")

The XML files can be viewed in a web browser, and unfortunately the format is a little ugly. The XML files have a header area with a large number of options.The Name Space Uris is the custom area that defines the OPC UA end point address.

After the header there are object and variable definitions (<AUVariable>). In these section the variable’s NodeID, tag name and description are defined.

The Free OPC-UA modeler that can help with the creation of XML tag databases. To install and run the Free OPC-UA modeler:

$ pip install opcua-modeler
$ opcua-modeler

The OPC-UA modeler will read existing XML files and then allow for objects, tags and properties to be inserted into the XML structure.

CSV to XML

A CSV file is an easy format for defining tag databases. For example a file mytags.csv could be defined with 3 fields; tagname, description and default value.

$ cat mytags.csv
# field: tag, description, default-value
TI-101,temperature at river, 25
PI-101,pressure at river, 14

A basic CSV to XML import tool can be created to meet your project requirements. There are a number of good programming options to do this migration. For my project I created a small Bash/AWK program to translate the 3 field CSV file to the required OPC-UA XML format.

The first awk section prints out the header information. The second awk section reads the input (CSV) text line by line and pulls out each of the three fields ($1, $2 and $3) and prints out the XML with these fields inserted in the output.

#!/usr/bin/bash
# csv2xml.sh - create an OPC UA XML file from CSV
# 
 
# add the xml header info
awk ' BEGIN {
  print "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
  print "<UANodeSet xmlns=\"http://opcfoundation.org/UA/2011/03/UANodeSet.xsd\"" 
  print "           xmlns:uax=\"http://opcfoundation.org/UA/2008/02/Types.xsd\""
  print "           xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"" 
  print "           xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
  print "<NamespaceUris>"
  print "  <Uri>http://192.168.0.120:4841</Uri>" ; # This address would be passed in
  print "</NamespaceUris>"
}'

# Read the input CSV format and process to XML
awk ' {
   FS="," ; # separate fields with a comma
# Skip any comment lines that start with a #
  if ( substr($1,1,1) != "#" )
  {
    i = i+1 ; # increment the NodeID index
    print "<UAVariable BrowseName=\"1:"$1"\" DataType=\"Int32\" NodeId=\"ns=1;i="i"\" ParentNodeId=\"i=85\">"
    print "  <DisplayName>"$1"</DisplayName>" ; # set the display name to the 1st field
    print "  <Description>"$2"</Description>" ; # set the description to the 2nd field
    print "      <References>"
    print "        <Reference IsForward=\"false\" ReferenceType=\"HasComponent\">i=85</Reference>"
    print "      </References>"
    print "    <Value>"
    print "      <uax:Int32>"$3"</uax:Int32>" ; # set the default value to the 3rd field
    print "    </Value>"
    print "</UAVariable>"
  }   
}
END{ print "</UANodeSet>"} '

To run this script to read a CSV file (mytags.csv) and create an XML file (mytags.xml) :

cat mytags.csv | ./csv2xml.sh > mytags.xml

Node-Red OPC UA Server

There is a good OPC UA node (https://flows.nodered.org/node/node-red-contrib-opcua) that includes a server and most common OPC UA features. This node can be install within Node-Red using the “Manage Palette” option.

To setup a Node-Red OPC UA server and a browser, define a OPCUA server node to use the Node Red IP address and set a custom nodeset directory. For my example I set the directory to /home/pi/opcua and in this directory I copied the XML file that I created from CSV (mytags.xml) into.

The OPCUA Browser node will send messages directly into the debug pane. This browse node allows me to see the objects/variables that I defined in my XML file.

The next step is to look at writing and reading values.

The simplest way to communicate with an OPC UA server is to use an OpcUa Item node to define the NodeID and an OpcUa Client node to do some action. For the OpcUa Client node the End point address and an action needs to be defined.

In this example the pressure (PI-101) has a NodeID of “ns=5;i=2”, and this string is entered into the OpcUA item node. The OpcUA Client node uses a Write action. When a Write action is issued a Good or Bad status message is returned.

The OpcUa Client node supports a number of different actions. Rather than doing a Read action like in the Python client app, a Subscribe can be used. A Subscribe action will return a value whenever the value changes.

NodeRed Dashboards with the Python OPC UA Server

For the last example I will use the Python OPC UA server from the first example. The Temperature and WindSpeed will use the same simulation code, but an added Waveheight tag will be a manually entered value from Node-Red.

A Node-Red application that connects to the Python OPC UA server and presents that data in a Node-Red dashboard would be:

This example subscribes to two real-time inputs (Temperature and Windspeed) and presents the values in gauges. The OpcUA Item nodes define the OPC UA NodeId’s to be used.

All the OpcUa Client nodes will need their Endpoints defined to the Python OPC UA server address.

The subscribed data values are returned as a 2 item array (because the data type is a Int64). The Gauge node will only read the first payload array item, (which is 0) so a small function node copies the second payload item (msg.payload[1]) to the payload message:

// Copy the second payload array item to be the payload
//  Note: msg.payload[0] = 0 and the Dashboard Gauge needs to use the value at payload[1]
msg.payload = msg.payload[1]
return msg;

For this example a manual input was included. The WaveHeight is subscribed to like the other tags, and the slider position is updated to its value. The slider can also be used to manually set the value by having the slider output passed to an OpcUa Client node with a WRITE action.

After the logic is complete the Deploy button will make the application live. The Node-Red dashboard can be viewed at: http://node-red-ip:1880/ui

Final Comments

This is a quick and dirty set of examples on how to use Python and Node-Red with OPC UA.

OPC UA has a ton of other features that can be implemented like : Alarms and Events and Historical Data.

Also it should be noted that most high end OPC UA servers support accessing the OPC UA items via their browse names. So instead of accessing a point using “ns=5;i=6” a browser name string can be used, such as “ns=5;s=MYTAGNAME”.

OpenPLC on a Raspberry Pi

For home automation projects there are a lot of good software packages like Home Assistant and Node Red that can be used to control and view sensors and devices.

If you are interested in looking at an industrial controls approach to your automation projects then OpenPLC is a good package to consider.

A PLC (Programmable Logic Controller) is an industrial hardened hardware device that manages I/O and logic using the IEC 61131-3 standard.

OpenPLC is open source software that runs on a Raspberry Pi, Linux or Windows PC and it offers users a great way to learn industrial control concepts, programming languages and communications protocols.

In this article I will create three small Raspberry Pi projects using the IEC 61131-3 ladder logic, function blocks and structure text programming languages. Finally I will have these projects pass their data via Modbus TCP to a Node Red dashboard.

Getting Started

The OpenPLC software comes in three packages, a logic editor, the runtime component, and a graphic builder. See https://www.openplcproject.com/getting-started/ for specific instructions for your installation.

For my installation I put the OpenPLC editor on my Ubuntu PC so that I could do remote configuration. I loaded the OpenPLC runtime on a Raspberry PI. The OpenPLC runtime web interface is used to load and monitor logic.

I didn’t install the OpenPLC graphic builder instead I used Node-Red Dashboards as my final user interface

OpenPLC has a good number of optional communications packages and slave I/O components. A typical layout could be as below.

For my application I created a project with three programs; a ladder program, a function block program and a structure text program. The Resource object (Res0) defines global variables that can be used by all programs, and the task cycle times. This is a small project so I put all the programs into the same task execution (task0). For larger project I might put all my digital logic into a fast task execution (20ms) and my analog logic into a slower task execution (250ms).

I setup the Raspberry Pi with a push button on pin 17 and an LED on pin 23.

On the Raspberry Pi GPIO pins are referenced using the IEC 61131-3 addressing. So the pushbutton at BCM pin 17 (physical pin 11) is addressed by %IX0.3 , an input bit on bus 0 at bit 3. The LED at BCM pin 23 (physical pin 16) is addressed by %QX0.2 , as output bit on bus 0 bit 2.

It’s important to note that OpenPLC has allocated all the left side (odd) pins as inputs and all the right side (even) pins as outputs.

Ladder Diagrams (LD)

Ladder logic was the first IEC 61131-3 programming languages, it was developed as a graphic representation for circuit diagrams of relay logic hardware. The term “ladder” comes from the fact that the logic looks a little like a ladder with the left side having a vertical power rail and a a vertical ground rail on the right side, then there are a series of horizontal lines or “rungs” wiring hardware components between the rails.

Most electricians feel very comfortable using Ladder logic and it is a good programming method for managing digital logic. If you come from a programming background Ladder logic may feel a little strange at first, for example to do AND / OR logic to light an LED would be:

For my Ladder program I wanted to light an LED for 3 seconds with a single push of a button. In the OpenPLC editor I referenced an external variable PB1 (it’s defined in Resource object Res0) and I created two local variables, LED2, my output LED and TOF0, an off delay timer.

IEC 61131-3 has a wide range functions that can be used in Ladder rungs. In this example a TOF function was inserted after the push button, and the time parameter is wired in variable.

Function Block Diagrams (FBD)

One of limitations of Ladder logic is that managing analog logic can be a little messy, for this reason Function Block Diagrams (FBD) was developed.

If you feel comfortable using graphic programming applications like Node-Red then you shouldn’t have any problems working in Function Block Diagrams.

For my FBD program I wanted to count the number of times the LED was lite and output the value to a Modbus hold register.

Like in the Ladder program the external PB1 variable is referenced. A new output CNT_FB is defined as an output word on bus 100, %QW100.

The FBD uses a Rising Edge Trigger (R_TRIG) to catch when the LED turns on. The output from R_TRIG is a boolean so the value is converted to an INT and added to the value of CNT_FB.

Structured Text (ST)

One of the advantages of Function Block Diagrams is that it is very readable and somewhat self documenting. The downside of FBD is that it can be messy for complex conditional logic.

Structured Text (ST) was developed as a programming option that can work along with the other 61131-3 languages. Structure Text is block structured and syntactically resembles Pascal.

For my Structured Text program I wanted to do the same functionality that was done in the earlier Function Block Diagram program. To do the same functionality in ST as the FBD programs it only took 3 lines of code vs. 5 Function Blocks.

In my ST program I added a simple IF condition to reset the push button counter if the value reached 1000.

It’s important to note that library functions such as R_TRIG are available in all the 61131-3 programming languages. It is also possible to create your own custom functions in one programming language and they can then we used in all the other languages.

Running OpenPLC Programs

After the three programs have been compiled and saved they can be install into the OpenPLC runtime application. To manually start the runtime application:

pi@pi4:~ $ cd OpenPLC_v3
pi@pi4:~/OpenPLC_v3 $ sudo ./start_openplc.sh &

The OpenPLC runtime will start a Web application on port 8080 on the Raspberry Pi. After logging into the web interface, the first step is to select the “Hardware” option and set the OpenPLC Hardware Layer to “Raspberry Pi”. Next select the “Programs” option and upload the OpenPLC configuration file. After a new configuration file is uploaded and compiled, the final step is to press the “Start PLC” button.

The “Monitoring” option can be used to view the status of variables in the PLC configuration.

Modbus with Node-Red

Modbus was the earliest and most common communication protocol used to connect Industrial devices together. Modbus can be used on serial interfaces (Modbus RTU) or on Ethernet networks (Modbus TCP), both are supported by OpenPLC.

Node-Red has a number of Modbus TCP nodes that can be used. I found that : node-red-contrib-modbustcp worked well for my application. New nodes can be added to Node-Red using the “Manage Palette” option.

A simple Node-Red application that can monitor the LED and counter statuses would use three modbustcp input nodes and a text and two numeric nodes.

The Modbus read call returns 16 bits of information, so a small function was created (“Only pass Item 0”) to change the msg payload to be just the first item in the array:

msg.payload = msg.payload[0];
return msg;

Modbus supports 4 object types; coils, discrete inputs, input registers and holding registers.

For this project the LED’s IEC addressing is %QX0.2 and this would be a coil at address 2. The Function Block counter (CNT_FB) address of %QW100 is a Hold Register of 100, (CNT_ST is a Hold Register of 0).

Modbus Writing from Node-Red

The Ladder logic program was updated to light the LED from either the push button or a hold register. The hold register (%QW1) is an integer so the value is converted to a boolean then “OR”-ed with the push button interface.

On Node-Red a slider node is used to pass a 0/1 to a modbus tcp output node, that write to hold register 1.

The Node-Red web dashboard is accessed at: http://your_rasp_pi:1880/ui/

Final Comments

OpenPLC is an excellent testing and teaching tool for industrial controls.

Elixir: Easy distributed programming

Creating distributed and concurrent applications doesn’t have to be difficult. Elixir allows hobbyist and new programmers to easily create projects that can work across multiple nodes.

Elixir is a general purpose programming language that runs on top of the Erlang Virtual Machine , which is known for running low-latency, distributed, and fault-tolerant systems.

In this article I will look at three projects that will use basic Elixir functions, (no custom project setup or imported libraries).

The first projects will do remote functions between a PC and a Raspberry Pi. The second project will use multi-node requests to get Pi statistics, and the final project will look at dynamic sharing of data between three nodes.

These projects will show that distributed projects don’t have to be complicated. Each of these projects will require only 10 – 25 lines of Elixir code.

Getting Started

See the Elixir site (https://elixir-lang.org/install.html) for your specific installation.

The Elixir installation will install the Erlang VM and three new executables: iex (Interactive Elixir shell), elixir (Elixir script runner) and elixirc (Elixir compiler).

For all the projects I will keep to standard Bash command line tools so no extra Erlang or Elixir libraries will be needed.

A good first example is use the interactive Elixir shell (iex) on a Raspberry PI to write to a GPIO (General Purpose Input/Output) pin.

The iex shell is opened and the Raspberry Pi gpio command line tool is called using the base Erlang :os.cmd function:

$ iex

iex> :os.cmd(:"gpio write 7 1")
[]
iex> :os.cmd(:"gpio read 7")   
'1\n'

Elixir can call Erlang functions, just remember to place a “:” in front of the Erlang function or custom variable.

Ok that was pretty basic, the next step is to control a Pi’s GPIO from a different node.

Remote Control GPIO

A two node network can be configured by defining a username with a node address and a common cookie between the two nodes.

For my setup I logged into the Rasp Pi iex shell with a name of pi3@192.168.0.105 and I used a cookie name of “pitest”. Then I logged into the PC iex session with a name of pete@192.168.0.120 and the same cookie name of “pitest”.

From my PC iex session I only need two lines of Elixir code to remotely write to a Pi GPIO pin (Figure 2). The first line connects to the Pi Elixir node, and the second line issues a remote procedural call to run an :os.cmd statement on the Pi node:

$ iex --name pete@192.168.0.120  --cookie pitest

iex> Node.connect :"pi3@192.168.0.105"
true
iex> :rpc.call(:"pi3@192.168.0.105",:os,:cmd ,[:"gpio write 7 1"]) 
[]

It’s important to note that the underlying Erlang VM on the Raspberry Pi manages the RPC request, and for this example no Elixir code was required on the Pi node.

A Remote GPIO Write Dialog

The next step is to create a simple way to select the GPIO pin and value to write.

Elixir tends to be used for backend applications, but there are a number of good web server options and an Erlang wx module is available.

One user interface approach is to use the Elixir IO module to do text console reads and writes. To get user input the IO.gets() function is called and IO.puts will write to the console. Variables can inserted into text strings by: #{the_variable}.

iex> thepin = IO.gets("Select the Pi GPIO pin: ")
Select the Pi GPIO pin: 7
"7\n"

iex> IO.puts "Selected GPIO pin: #{thepin}" 
Selected GPIO pin: 7

For simple dialogs I like to use the command line Zenity package. Zenity support a number of different dialog types and it is pre-loaded on Raspian and most Linux OS’s.

The zenity –form command can be configured to return a GPIO pin number and pin value.

An Elixir script (Zen2gpio.exs) can be created that launches a zenity form and passes the pin data an :rpc.call function.

For this script a module Zen2gpio is created with the function show_form.

As mentioned earlier Elixir strings can have variables values inserted into strings by using: #{the_variable}.

If the user enters data and presses OK, the result string will be passed and the dialog will reopen. Pressing the Cancel button will pass no data and the script will close.

#-------------
# Zen2gpio.exs - Use a Zenity form to set the GPIO pin and value input
#-------------
defmodule Zen2gpio do
  def show_form (pnode) do
    thecmd = "zenity --forms --title='Set Pi GPIO Pins' --separator=' ' --add-entry='GPIO Pin (0-26)' --add-entry='New Value (0-1)' "
    pininfo = :os.cmd(:"#{thecmd}")
    # If some data is entered in form, write to GPIO and refresh
    if byte_size("pininfo") > 0 do
      :rpc.call(:"pi3@192.168.0.105",:os,:cmd ,[:"gpio write #{pininfo}"]) 
      show_form (pnode)
    end
  end
end
 
# Connect to the pi node
pnode = :"p3@192.168.0.105"
Node.connect  pnode

# Show the dialog
Zen2gpio.show_form(pnode)

The Elixir script runner can launch the code with the common project cookie and a unique user name.

Multi-Node RPC Requests

The goal for the next project is to have a PC node query the Pi nodes for diagnostic information. This project is a little different than the earlier project in that a module is loaded on the Raspberry Pi’s to send back custom status messages.

The Raspberry PI’s CPU temperature can be obtained using the Bash command:

# Bash command to get the Pi CPU temperature
$ /opt/vc/bin/vcgencmd measure_temp
temp=42.3'C

This Bash command can be incorporated into a small Elixir script that is loaded and compiled on each of the Pi nodes. The script (PI_stats.ex) has a module PI_stats that contains a function cpu_temp.

The cpu_temp function returns a string containing the Pi node name and the output from the shell command to get the CPU temperature.

#-----------------------------
# PI_stats.ex - Get Some Stats
#-----------------------------
defmodule PI_stats do
  def cpu_temp() do
   "#{Node.self()}  #{:os.cmd(:"/opt/vc/bin/vcgencmd measure_temp")}"
  end
  # Add more diagnostics like: available RAM, idle time ...
end

The elexirc command is used to compile an Elixir scrip. After scripts are compiled their modules are available to iex shells that are called from that directory. Below is the code to compile and then test the PI_stats module:

## compile an Elixir script
$ elixirc PI_stats.ex

## test the PI_stats.cpu_temp function locally
$ iex --name pi3@192.168.0.105  --cookie pitest
...
iex> PI_stats.cpu_temp()
{"pi3@192.168.0.105 temp=47.8\'C\n'}

An Erlang :rpc.multicall function can be used on the PC node to retrieve the Pi CPU temperatures. The :rpc.multicall function is passed the node list, module name, function call and then any addition arguments:

iex> :rpc.multicall( [:"pi3@192.168.0.105", :"pi4@192.168.0.101"], PI_stats, :cpu_temp, [])

{["pi3@192.168.0.105  temp=47.2'C\n", "pi4@192.168.0.101  temp=43.8'C\n"], []}

On the PC node a script (get_temps.exs) is created to connect to the PI nodes and get the results from the RPC multi-call. To make the code more flexible all the Pi nodes are stored in a list (pinodes). The Eum.map function will iterate the pinode list and connect to each node.

For this example the results from the RPC multi-call are a little messy so an Enum.map and Enum.join functions are used format the results to one long string that is passed to a Zenity info dialog box.

#----------------------------------------
# get_temps.exs - get PI CPU temperatures
#  - show results on Zenity Dialog
#----------------------------------------                     
pinodes = [ :"pi3@192.168.0.105", :"pi4@192.168.0.101"] 
Enum.map(pinodes, fn x-> Node.connect x end)
 
# Get results from remote PI nodes
{result,_badnodes}  = :rpc.multicall( pinodes, PI_stats, :cpu_temp, [])
 
# Format the output for a Zenity info dialog
output = Enum.map(result, fn x -> x end) |> Enum.join
:os.cmd(:"zenity --info --text=\"#{output}\" --title='Pi Diagnostics'")  

Like the earlier project the Elixir script is run with the common project cookie. and a unique user name is used.

It’s important to note that once the PI_stats.ex script is compiled on the Pi nodes no other action is required, like in the first project the RPC request is processed by the underlying Erlang VM.

Data Sharing between Nodes

Elixir offers a number of different data storage options. For simple multi-node data sharing I found that the Erlang :mnesia package to be a good fit.

For this last project Mnesia is setup to have a shared schema between the three nodes.

The Pi nodes will populate tables with their GPIO pin status every 2 seconds.

On the PC the first project will be used to write to GPIO pins, and a new script we be created to monitor the status of the GPIO pins within a Mnesia shared table.

To create a shared or distributed schema Mnesia needs to be stopped on all the nodes.

The :mnesia.create_schema function will create a shared schema for all the listed nodes.

After the schema is created Mnesia needs to be restarted. The :rpc.multicall function is extremely useful when identical actions need to done on distributed nodes:

iex> allnodes = [ :"pete@192.168.0.120" , :"pi3@192.168.0.105", :"pi4@192.168.0.101"]
iex> :rpc.multicall( allnodes, :mnesia, :stop, [])
iex> :mnesia.create_schema(allnodes)
iex> :rpc.multicall( allnodes, :mnesia, :start, [])

If you already have an existing schema, it can be deleted with by :mnesia.delete_schema([node()]).

The next step is to add tables to the schema. A table of GPIO pin values for Raspberry Pi 3 (Pi3) is created by:

iex> :mnesia.create_table(Pi3,  [attributes: [ :gpio, :value] ] )

For nodes that are writing to a specific table, it is recommended that the table be defined as both a ram and disk copy. To do this log into that node and enter:

:mnesia.change_table_copy_type(Pi3, node(), :disc_copies)

For larger projects where multiple nodes are reading and writing into tables transactions statements should be used. For small projects where only one node is writing into a table a “dirty” re:wxStaticText.setLabel(text1, “GPIO Pin 4: “ad and write can be used. Below is an example of writing into table Pi3 a value of 1 for pin 4, and then reading the record back.

iex> :mnesia.dirty_write({Pi3, 4,1})                                
:ok                                    
iex> pin4val = :mnesia.dirty_read({Pi3, 4}) 
[{Pi3, 4, 1}]

Now that simple writes and reads can be done the next step is to create a script that continually populates the Pi3 table with GPIO pin values.

Populating GPIO Data into a Mneisa Table

The Elixir programming language has some interesting syntax features that allow you to write some efficient code. Two features that will streamline a table input function are anonymous and enumeration functions.

Anonymous functions use the “&” to create short hard. Below is a simple example and then a complex example to read a GPIO pin value and remove the trailing new line character:

iex> # A basic example
iex> sum = &(&1 + &2)
iex> sum.(2, 3)
5

iex> getpin=&(:os.cmd(:"gpio read #{(&1)} | tr -d \"\n\" ") )
iex> getpin.(7)
'1'

The function Enum.map can do complex “for-each” loops.

These two Elixir features can be used together to read 27 Raspberry Pi GPIO pins and then write data to a Mnesia table. Below is a script (Gpio3write.exs) that will write GPIO values into a Mnesia table every 2 seconds.

#---------------
# Gpio3write.exs - Write Pi 3 GPIO values into Mnesia every 2 seconds
#---------------
defmodule Gpio3write do
  def do_write do
    getpin=&(:os.cmd(:"gpio read #{(&1)} | tr -d \"\n\" ") )
    Enum.map(0..26, fn x-> :mnesia.dirty_write({Pi3, x, getpin.(x) }) end)
    :timer.sleep(2000)
    do_write()
  end
end
# Start Mnesia
:mnesia.start()
# Cycle every 2 seconds and write values
Gpio3write.do_write()

The script on the Pi node is started by:

elixir --name pi3@192.168.0.105 --cookie pitest Gpio3write.exs

Get Records from Mnesia

Unfortunately Mnesia does not support SQL syntax, but it does support some basic filters using the dirty_match_object :

iex># Get all the records, use :_ for "all", add a sort at the end

iex> :mnesia.dirty_match_object({Pi3, :_ ,:_})  |> Enum.sort
[
  {Pi3, 0, '0'},
  {Pi3, 1, '0'},
  {Pi3, 2, '1'},
...
]
iex># Get only Pi Values that are '1'
 :mnesia.dirty_match_object({Pi3, :_ ,'1'})  |> Enum.sort
[
  {Pi3, 2, '1'},
  {Pi3, 21, '1'}
]

A Zenity list dialog can be used to show table output in columns. Below is an example of a zenity –list command in Bash. The last argument in the command is the data string which fills in the defined columns. An –extra-button option is included, and its button text is returned when the button is pressed.

A script is created (Show_gpio.exs) that reads the Mnesia results. As in the earlier example Enum.map and Enum.join functions are used to format the results as one long string for a Zenity list dialog.

#-------------------
# Show_Show_gpio.exs - show Mnesia table in a Zenity list dialog
#-------------------
defmodule Show_gpio do
  def getdata() do
    result = :mnesia.dirty_match_object({Pi3, :_ ,:_}) |> Enum.sort
    # create a presentable string for output
    output = Enum.map(result, fn x -> "#{elem(x,1)} #{elem(x,2)} " end) |> Enum.join
    feedback = :os.cmd(:"zenity --list --title=Pi3_Table --text='Pin Values' --extra-button Refresh --column=Pin --column=Value #{output}")
    if ("#{feedback}" =~ "Refresh") do
      getdata()
    end
  end
end
# Start Mnesia
:mnesia.start()
# Wait for tables to update
:timer.sleep(1000)
# Show a Zenity list dialog. 
# Refresh button to continue, other buttons to quit
Show_gpio.getdata()

Like in the earlier examples the Elixir script runner is used to call the script with the list dialog.

To test that things are working, the first project (Zen2gpio.exs) can toggle GPIO pin values, and then the Show_gpio.exs dialog can be refreshed to check the new values.

When the final project is running we have an example of Elixir concurrency. The Pi3 node is populating a Mnesia table and it is handling remote GPIO writes and CPU temperature messages.

Final Comments

Elixir with the Erlang VM offers a lot of functionality out of the box.

The next steps from here would be to start looking at messaging between nodes and creating projects with imported Elixir libraries.


Additional Code Snippets

Coloured Text Bars

The IO.ANSI library can be used to create text interfaces. Below is an example to create some horizontal bars:

#--------------------------
# bars.exs - Show some bars
#--------------------------
a = 10
b = 20

IO.puts IO.ANSI.underline() <> "\nBar Charts\n" <> IO.ANSI.no_underline()

# Bar A
barA = "temp:     " <> String.duplicate("█",a) <> to_string(a) <> " C\n"
IO.puts IO.ANSI.red() <> barA

# Bar B
barB = "pressure: " <> String.duplicate("█",b) <> to_string(b) <> " psi"
IO.puts IO.ANSI.blue() <> barB
IO.puts IO.ANSI.reset() <> "\n"

Erlang wX Graphic Library

The Erlang graphic library is added by:

sudo apt install erlang-wx

An example to show a dialog with a Label (static text) is:

# Create a simple wx Window with a text object
wx = :wx.new
frame = :wxFrame.new(:wx.new(), 1, "Elixir -> PI Temps", [{:size, {300,100}}])
id  = elem(frame,1)
# Add a static text component 
text1 = :wxStaticText.new(frame ,id, "Some Static Text")
:wxWindow.show(frame)
# refresh the text after 2 seconds
:timer.sleep(1000)
:wxStaticText.setLabel(text1, "Updated text" )

Erlang inets HTTP Web Server

The Erlang inets library has a number of useful networking components, it is loaded by:

sudo apt install erlang-inets

Below is an example of a Web Server with a callback function for the root call. This function (API) will show the present value of Raspi GPIO pin 7.

# -----------------------------
# Web Server
#------------------------------
defmodule Api do
  require Record

  # Wrap the Erlang Record to make the request_uri parameter easier to access
  Record.defrecord :httpd, Record.extract(:mod, from_lib: "inets/include/httpd.hrl")

  def unquote(:do)(data) do
    # Create a response page
    {pin7,_} = System.cmd("gpio",["read","7"])
    body = "<h1>Elixir Rasp Pi GPIO</h1><hr>GPIO 7 : " <> String.trim(pin7,"\n")
    response =
        case httpd(data, :request_uri) do
        '/' ->
            {200, '#{body}'}

        _ -> {404, 'Not found'}
      end
    {:proceed, [response: response]}
  end
end

#
# Start the HTTP Server
#
:inets.start()
Application.ensure_all_started(:inets)
:timer.sleep(1000)# -----------------------------
# Web Server
#------------------------------
defmodule Api do
  require Record

  # Wrap the Erlang Record to make the request_uri parameter easier to access
  Record.defrecord :httpd, Record.extract(:mod, from_lib: "inets/include/httpd.hrl")

  def unquote(:do)(data) do
    # Create a response page
    {pin7,_} = System.cmd("gpio",["read","7"])
    body = "<h1>Elixir Rasp Pi GPIO</h1><hr>GPIO 7 : " <> String.trim(pin7,"\n")
    response =
        case httpd(data, :request_uri) do
        '/' ->
            {200, '#{body}'}

        _ -> {404, 'Not found'}
      end
    {:proceed, [response: response]}
  end
end

#
# Start the HTTP Server
#
:inets.start()
Application.ensure_all_started(:inets)
:timer.sleep(1000)
# give some time ensure inets is started
{ok, Pid} = :inets.start(:httpd, [server_name: 'Api', server_root: '/tmp', document_root: '/tmp', port: 3000, modules: [Api] ])
# give some time ensure inets is started
{ok, Pid} = :inets.start(:httpd, [server_name: 'Api', server_root: '/tmp', document_root: '/tmp', port: 3000, modules: [Api] ])