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.

MySQL with Arduino and Node-Red

In this blog I would like to document a project that I did with my kids. The goal of the project was to learn SQL in a couple of different environments.

mysql_overview

We did most of our initial testing/learning directly on the MySQL server. Once we got everything worked out we moved to Arduino, then Node-Red.

For our MySQL testing we used alwaysdata.com which has a free low use account. They run MariaDB which looks exactly like MySQL.

 

MySQL Setup

The plan was to have the Arduino module write 3 values:

  1. randint – a random integer from 1-100
  2. saw – a saw tooth integer from 0-100
  3. sstatus – a text value of : HIGH, OKAY or LOW

sample_data

We didn’t want to generate a time stamp from Arduino, instead we wanted MySQL to generate it. Our SQL structure was:

datal_structure

The field “thetime” was defined as a timestamp with CURRENT_TIMESTAMP as the default. By doing this we only needed to INSERT the Arduino data and MySQL would add the current timestamp.

insert_test

After the Arduino Tables was created and tested we were able to start inserting data from Arduino.

 

Arduino and MySQL

There is an excellent MySQL library for Arduino, that can be added via the Arduino IDE “Manage Libraries” option.

Our Arduino code wrote the 3 values to MySQL every 10 seconds.

/*
  MySQL Connector/Arduino Example : connect by wifi
*/
#include <ESP8266WiFi.h>           // Use this for WiFi instead of Ethernet.h
#include <MySQL_Connection.h>
#include <MySQL_Cursor.h>

IPAddress server_addr(111,222,111,111);  // IP of the MySQL *server* here
char user[] = "yourusername";              // MySQL user login username
char password[] = "yourpassword";        // MySQL user login password

// WiFi card example
char ssid[] = "xxxxx";         // your SSID
char pass[] = "xxxxx";     // your SSID Password

WiFiClient client;                 // Use this for WiFi instead of EthernetClient
MySQL_Connection conn(&client);
MySQL_Cursor* cursor;

int saw = 0;

void setup()
{
  Serial.begin(9600);

  // Begin WiFi section
  Serial.printf("\nConnecting to %s", ssid);
  WiFi.begin(ssid, pass);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  // print out info about the connection:
  Serial.println("\nConnected to network");
  Serial.print("My IP address is: ");
  Serial.println(WiFi.localIP());

  Serial.print("Connecting to SQL...  ");
  if (conn.connect(server_addr, 3306, user, password))
    Serial.println("OK.");
  else
    Serial.println("FAILED.");
  
  // create MySQL cursor object
  cursor = new MySQL_Cursor(&conn);
}

void loop()
{
  // Sample insert query
  //
  char INSERT_SQL[] = "INSERT INTO datal (randint, saw, sstatus) VALUES ('%d','%d','%s')";
  char query[128];
  int randint = random(100);


saw++;
if (saw == 100){
  saw = 0;
}

  if (randint <= 10){
     sprintf(query, INSERT_SQL, randint, saw, "LOW");
  } else if (randint >= 90){
     sprintf(query, INSERT_SQL, randint, saw, "HI");
  } else {
     sprintf(query, INSERT_SQL, randint, saw, "OKAY");
  }
  
  Serial.println(query);

  if (conn.connected())
    cursor->execute(query);


  // Sample select query - Get the last 2 values and show them 
  // Table: arduino1 - this is written to from Node-Red 

  int arddata;
  // Initiate the query class instance
  row_values *row = NULL;
  MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);

  char squery[] = "SELECT thetime,arddata FROM arduino1 ORDER BY thetime DESC limit 2;";
  cur_mem->execute(squery);
  // Fetch the columns (required) but we don't use them.
  column_names *columns = cur_mem->get_columns();

  do {
    row = cur_mem->get_next_row();
    if (row != NULL) {
      //thetime = (row->values[0]);
      arddata = atoi(row->values[1]);
      Serial.print("Time: "); Serial.print(row->values[0]); Serial.print(" data: "); Serial.println(arddata);
      
    }
  } while (row != NULL);
  delete cur_mem;
  
  delay(10000);
  
}

 

MySQL Views

The table(s) that we created were nice and simple for Arduino data, but they couldn’t be used directly for any types of statistics like daily/hourly/minute averages/maxs or minimums. So we created some MySQL views.

The first things that we needed to do was create some “time” columns like HOUR and MINUTE.

CREATE VIEW v_raw as 
 SELECT HOUR(thetime) as HOUR, 
   MINUTE(thetime) as MINUTE, 
   randint, saw, sstatus from datal 
   order by thetime DESC

This SQL statement creates a view (v_raw)  that shows the hour, minute fields with data ordered from newest to oldest. The query of : SELECT  * FROM v_raw; gives:

v_raw

We then created a number of statistic views like:

CREATE VIEW v_min AS 
  SELECT HOUR(thetime) as HOUR, MINUTE(thetime) as MINUTE, 
    AVG(randint), AVG(saw) 
   from datal 
     GROUP BY HOUR,MINUTE ORDER BY thetime LIMIT 720

This gave us some minute averages:

v_min

 

Node-Red with MySQL

The component node-red-node-mysql can be added manually or via the “Manage Palettes” option.

node-red-node-mysql

For our project we wanted to show:

  • Last 100 Arduino values
  • Latest Average Minute Values
  • Latest Average Hourly Value

Then we wanted to Insert a new 0/1 value that could be read back in the Arduino. This inserted value could be used to turn on a relay connected to the the Arduino (like a start a fan, turn on a light etc.)

The logic used Dashboard buttons (to select an action).  MySQL nodes do the SELECT or INSERT command, and a Dashboard Table to show the results.

mysql_nodered_logic

The buttons are configured to pass the SQL views statement to the MySQL nodes, with the results showing in a Dashboard table node.

nr_v_min

After the logic is complete, use the Deploy button to make the logic live. The web dashboards can be viewed at: http://yourNodeRed_IP:1880/ui . Below is an example of 1) Minute Averages, and 2) Last 100 (raw values).

 

Final Comments

For this project we kept things simple, and used tables to show the results. As a next step it would be good to show the results in a trend chart. In a previous blog I looked at putting SQLite into a Node-Red chart. I would use this technique with MySQL data.