Bateau Elorn

[EN] Raspberry Pi + Mihini, controlling an off-the-grid electrical installation, part I

11 May 2014

French summary: Cet article détaille l'installation logicielle qui me permet de contrôler les systèmes électriques du bateau par informatique, à l'aide d'une carte Raspberry Pi. Comme il s'adresse à un public d'informaticiens et d'électroniciens, très majoritairement anglophones, il est en Anglais.

Edit: In a previous version of this post, I did data acquisition through a separate process, with something akin to os.execute("timeout 1.5s /dev/ttyAMA0 > /tmp/buffer_file"). Not only was it overkill to read 200 bytes at 19200 bauds, it bugged under some circumstances, not yet fully explained (I suspect some Unix signals inheritance issues in Mihini's app_daemon, investigations are ongoing). In the reviewed version, I'll just use uart=serial.open() then uart:read() and be done with it.

(end of preliminaries)

As I've worked a lot in software engineering and telecom, embedded or not, I wanted to be able to log, remotely monitor and control the boat, and I felt compelled to do it myself. For that, I bought a Raspberry Pi, and planned to connect it with the various systems I want to monitor or control. This first post describes the first step: acquiring and reportingdata from the Victron BMV-601 battery monitor.

Those whose definition of "fun" differs from mine should look at Victron's Color Control GX, which requires more money but less time and fiddling.

System Overview

The boat has several energy sources:

Electrical diagram Overview of the electrical installation, excluding appliances (in French).

Raspberry Power Supply

I don't want the Raspberry to shut down when I cut the 230VAC, so I'll power it with the boat's batteries, which are 24VDC. DC-DC power converters come in two flavors:

For a couple of euros on eBay, you can find such converters based on TI's LM2596S. They deliver up to 3A, accept up to 37V and can bring it down to 3.3V. All it takes to tune their in/out ratio is a screwdriver.

BMV-601 Battery Monitor

The monitor has a serial output. Not RS232, but the bare minimum needed to talk with other embedded systems: a 4-pins interface (GND, +3.3V, TX, RX; no +5V, no flow control). The Raspberry also has a +3.3V serial line, on its side connector. So let's connect them together and see if we can get anything:

Back view Installation, viewed from the back

Front View The same, viewed from the front

Wiring Wiring scheme

Note: although the Raspberry has a +3.3V output (facing the +5V in the corner of the card), it's not only useless to connect it to the BMV, it has a nasty side-effect: it resets the BMV whenever the Raspberry is switched off.

Operating System

There are several OS distributions for Raspberry. I chose Raspbian because 1/ I've been using Debian derivatives for more than a decade and 2/ Mihini is ported and tested on it.

Software Setup

By default on Raspbian, the serial line is used to serve a terminal. Let's disable that and reclaim the port for ourselves. Also, let's put a WiFi dongle (it'll save some wiring) and configure it.

Optionally, write your PC's SSH key (generally found in ~/.ssh/id_rsa.pub) in the Raspberry's ~/.ssh/authorized_keys. It will save you from typing passwords when you log in.

Time to check if we see something on the serial port:

$ ssh pi@pi
$ stty -F /dev/ttyAMA0 speed 19200 cs8 -icrnl -ixon -icanon
$ cat /dev/ttyAMA0
V   26717
I   0
CE  0
SOC 1000
TTG -1
Alarm   OFF
Relay   OFF
AR  0
BMV 600S
FW  212
Checksum    ?
H1  0
H2  0
H3  0
H4  0
H5  0
H6  0
H7  26708
H8  26718
H9  0
H10 1
H11 0
H12 0
Checksum    ?
^C
$ _

Great :) Moreover, the protocol is pretty readable, and fully documented by Victron. Time to write a small program to digests this.

Mihini

Eclipse Mihini is a development framework, primarily targetting embedded Linux systems, which allows to easily create and deploy M2M applications in Lua. Since I've participated a lot in its architecture and implementation, the choice was obvious, besides the many inherent qualities of that system--if I may say so.

Porting Mihini to a Raspberry isn't difficult, and is thoroughly documented on Internet. As a bonus, and unsurprisingly for an Eclipse product, it has an excellent support from the IDE, including autocompletion, out-of-the box remote debugging, application deployment etc.

Data acquisition

Mihini comes with a serial interface, allowing to read line-by-line, or by arbitrary chunks, with optional timeouts. Some quick experiments have shown that reading line-by-line causes a lot of errors. It's not completely surprising, given the lack of control signals on the UART; I guess it also means that the Raspberry has little to no buffer on this UART interface.

Anyway, we'll just do a single read operation that's sufficient to acquire enough data to parse, i.e. at least two full frames. How much is enough? A quick test shows that I get 110 bytes long frames, so 220 bytes for two frames. If I read in the middle of a frame, I might lose up to 109 bytes, so I need to read ~330 bytes. Let's say 400 bytes to play safe.

The two frames are pushed on the uart every second, with half-a-second of time-shidt. 110 bytes = 880 bits at 19200 baud should go through in about 1/20s. The reading operation shouldn't block for more than 1.1s.

require 'serial'

uart = serial.open('/dev/ttyAMA0', { baudrate=19200 })

function get_raw_data()
  return uart:read(400)
end

Now, we can extract a sequence of two frames out of that raw data. Indeed, the BMV keeps both instant informations about the batteries' state, and some historical statistics. They're organized in two frames; each kind of frame is written every second on the serial line, as a series of "<label>\t<value>" lines; the last line of a frame always has "Checksum" as a label.

function get_two_frames()
  local REGEXP = '\13\10Checksum\t.()'
  local raw_data = get_raw_data()
  local checksums = { }
  -- find 3 consecutive checksum lines in `raw_data`
  local last_position = 1
  for i=1,3 do
    local next_position = string.match(raw_data, REGEXP, last_position)
    checksums[i]  = next_position
    last_position = next_position
  end
  assert(frame_checksum(raw_data, checksums[1], checksums[2]-1)==0)
  assert(frame_checksum(raw_data, checksums[2], checksums[3]-1)==0)
  return raw_data :sub (checksums[1], checksums[3]-1)
end

Checking the frames for data corruption is fairly easy: the value associated with Checksum is an 8-bits ASCII byte chosen so that the sum of all bytes in the frame is a multiple of 256.

function frame_checksum(frame, a, b)
  local bytes = { frame:byte(a,b) }
  local sum = 0
  for i=1,#bytes do sum=sum+bytes[i] end
  return sum%256
end

Finally, we want to turn those raw textual data into proper Lua records, that will be easily exploited by the rest of the program:

function get_record()
  local frame = get_two_frames()
  local record = { }
  for line in frame :gmatch "[^\10\13]+" do
    local label, value = line :match '(..-)\t(.+)'
    if label~='Checksum' then
      record[label] = value
    end
  end
  return record
end

The actual code is slightly more sophisticated: error checking, some data conversion and a mutex, but the 4 functions above are indeed the core of my BMV data acquisition system.

Data Restitution

The simplest way to check the result remotely is through telnet. In a couple of lines of Lua, I can open a telnet shell, and use it to run my commands above. Notice that Mihini support collaborative multi-tasking through its sched module, which allows to run the shell and the data acquisition concurrently. Let's launch such a shell on port 3000:

local sched = require 'sched'
local shell = require 'shell.telnet'

-- Insert the BMV monitoring functions here --

function main()
  shell.init{ address='0.0.0.0', port=3000, editmode='edit', historysize=64 }
end

sched.run(main)
sched.loop()

On that telnet:

$ telnet pi 3000
Trying 192.168.13.100...
Connected to pi.
Escape character is '^]'.
Lua interactive shell
> :get_record()
= {
   discharge_average = 0,
   discharge_n_cycles = 0,
   time_to_go = -0.016666666666667,
   n_auto_synchro = 1,
   ah_total = 0,
   relay = "OFF",
   firmware = 212,
   state_of_charge = 100,
   discharge_max = 0,
   discharge_n_full = 0,
   voltage_max = 26.718,
   alarm = "OFF",
   consummed_energy = 0,
   n_alarms_voltage_low = 0,
   discharge_last = 0,
   days_since_full_charge = 0,
   n_alarms_voltage_high = 0,
   model = "600S",
   alarm_reason = 0,
   current = 0,
   voltage = 26.716,
   voltage_min = 26.708 }
> ^D

It works! For a more user-friendly approach, Mihini also comes with a simple web server. Nothing fancy, secured nor scalable, but enough to serve a couple of web pages or web services behind a firewall. We can write a simple data-dumping page:

require 'web.server'
require 'web.template'

web.site[''] = web.template 'default' {
    title = [[Data Monitoring: Elorn's batteries]],
    body = [[
    <table>
      <% for label, value in pairs(get_record()) do %>
      <tr>
        <th><%=label%></th>
        <td><%=value%></td>
      </tr>
      <% end %>
     </table>
     <p><a href=''>Refresh</a></p>
]] }

Add a web.start(9000) in the main() function, and we're set:

Reported on my phone

Further Improvements

The code outlined here was quick and dirty on purpose, to minimize the amount of code not directly addressing the issues. I've put the resulting program in a gist anyway, but you shouldn't reuse that. A much more proper version is available on github, with decent error checking and a somewhat more polished API.

Moreover, my solar panels controller uses VE.Direct as protocol, which is closely related to the BMV text protocol: the common code parts will be factored together.