Category Archives: Raspberry Pi

Matters relating to the development of Raspberry Pi applications, hardware and software

Particulates Sensing with the NOVA SDS011

Here at Cranfield University we are putting in place plans related to the new ‘Living Laboratory’ project, part of our ‘Urban Observatory’. This project sits within the wider UKCRIC initiative, across a number of universities. Of the many experiments in development, we are gathering environmental data from IoT devices and building data dashboards to show the data and related analyses. One of our projects will be to investigate air quality on the campus, in our lecture rooms and public spaces. Cranfield is a unique University in the UK for having its own airfield as part of the campus – we want to monitor any particular impacts that can arise from this. To do this, one of the tools we will use is the amazing Nova SDS011 particulates sensor (http://www.inovafitness.com/en/a/index.html).

The sensor itself, available from many outlets for instance here, is extremely cheap for what it offers, and is widely reported on with many projects on the Internet. We followed the excellent tutorial laid out on Hackernoon (https://hackernoon.com/how-to-measure-particulate-matter-with-a-raspberry-pi-75faa470ec35). We used a Raspberry Pi Zero, and we used the USB interface to speed the process of prototyping.

Rather than repeat the instructions laid out so well by Hackernoon, here we have some observations, and then some small adaptations to enable notifications and data logging.

One thing to remember in using the Raspberry Pi is that you need adapters (shown above) to connect traditional USB plugs to the micro plugs on the Pi. Also you need to remember that of the two USB ports, one is for powering the device and one is for peripherals. Plugging them in the wrong way round led to lots of unnecessary head scratching!

That said, once the instructions were followed, and the code put in place, the system was up and running and we could access the simple dashboard Hackernoon have developed using lighttpd.

This could be the end of the blog, all worked well, we have readings and a simple dashboard showing AQI. The device is incredibly sensitive – we can attest that during building the setup a late night pizza was accidentally burned (too busy hacking)! But the machine picked up the spike in particulates very well.

So the next challenge was to log the data being generated. In earlier blogs, we have used and liked ThingSpeak as a quick means to log data and build dashboards, so we decided to use this. This meant editing the Python code that hacker noon provided.

To write to ThingSpeak in Python, one can use the ‘urllib2’ library. We followed the excellent Instructables blog to do this. First, at the top of the code we import the urllib2 library and set up a variable to hold the connection string to ThingSpeak (using the API key for writing to the Channel we have created to hold the data):

<code>import urllib2 baseURL = 'http://api.thingspeak.com/update?api_key=CHANNEL_WRITE_API_KEY'</code>

Next, we located in the code where the particulate values for PM2.5 and PM10 are extracted and sent off to the web dashboard (full code used at the end). Here we inserted code to also send the same data to ThingSpeak:

<code>f = urllib2.urlopen(baseURL + '&amp;field1=' + str(values[0]) + '&amp;field2=' + str(values[1]))
f.read()
f.close()</code>

This worked well and data was transmitted to ThingSpeak and with its timestamp, this enabled a more comprehensive dashboard to be created that monitored the data values detected by the device (rather than the AQI values shown in the Hackernoon dashboard – clearly one could write that conversion in python in future if needed).

We then followed Hackernoon’s instructions to make the process start up on boot by placing the script into the crontab file. However, in doing this we realised it isn’t always possible to know when the script has started. As the script only starts on boot, if something goes wrong, the script never runs. We found that this was not a unique issue as others have found this also in other blogs. Thanks to the instructions on the Raspberry Pi website, we realised we could add a sleep command in to the crontab to ensure that the script was only started when there was a good chance the rest of the system was up and running. This solved the problem and now the crontab command was:

<code>@reboot sleep 60 &amp;&amp; cd /home/pi/ &amp;&amp; ./aqi.py</code>

The time could be extended from 60 seconds if needed. In any case, we now wanted to know it had indeed started up OK. We wanted a message sent to a mobile phone to say the process had started up OK. To do this we used the push notification approach of Prowl used in earlier blogs on this site (you need an iPhone for this although there will be equivalents for other phones. To get prowl to work in Python, we used the Python module for Prowl iPhone notification service from jacobb at https://github.com/jacobb/prowlpy. Installing this means downloading the ‘prowlpy.py’ script, and then a further adaptation in the aqi script at the start to call it appropriately, thus:

<code>import prowlpy
 apikey = 'PROWL_API_KEY'
 p = prowlpy.Prowl(apikey)
 try:
     p.add('AirQual','Starting up',"System commencing", 1, None, "http://www.prowlapp.com/")
     print('Success')
 except Exception,msg:
     print(msg)</code>

Finally, were it required, the push notification approach could also be used to inform particulate readings. The values of pm can also be intercepted, as per the ThingSpeak export, to send to the mobile phone too, code to do this would be thus:

<code>_message = "pm25: %.2f, pm10: %.2f, at %s" % (values[0], values[1], time.strftime("%d.%m.%Y %H:%M:%S"))          
print(_message) # debug line 
try:
    p.add('AirQual','Reading', _message, 1, None, "http://www.prowlapp.com/") 
except Exception,msg:
    print(msg)</code>

Although this worked perfectly, the phone was immediately overwhelmed with the number of messages, and this was quickly turned off! Notifications could be used however to message the user’s phone if important air quality thresholds were breached – reminding the operator to, for example, take the pizza out of the oven!

The final code script used for ‘aqi.py’ was:

<code>#!/usr/bin/python -u
# coding=utf-8
# "DATASHEET": http://cl.ly/ekot
# https://gist.github.com/kadamski/92653913a53baf9dd1a8
from __future__ import print_function
import serial, struct, sys, time, json, subprocess

# Customisations ######
import urllib2
baseURL = 'http://api.thingspeak.com/update?api_key=THINGSPEAK_API'

import prowlpy
apikey = 'PROWL_API_CODE'
p = prowlpy.Prowl(apikey)
try:
    p.add('AirQual','Starting up',"System commencing", 1, None, "http://www.prowlapp.com/")
    print('Success')
except Exception,msg:
    print(msg)
####################

DEBUG = 0
CMD_MODE = 2
CMD_QUERY_DATA = 4
CMD_DEVICE_ID = 5
CMD_SLEEP = 6
CMD_FIRMWARE = 7
CMD_WORKING_PERIOD = 8
MODE_ACTIVE = 0
MODE_QUERY = 1
PERIOD_CONTINUOUS = 0

JSON_FILE = '/var/www/html/aqi.json'

MQTT_HOST = ''
MQTT_TOPIC = '/weather/particulatematter'

ser = serial.Serial()
ser.port = "/dev/ttyUSB0"
ser.baudrate = 9600

ser.open()
ser.flushInput()

byte, data = 0, ""

def dump(d, prefix=''):
    print(prefix + ' '.join(x.encode('hex') for x in d))

def construct_command(cmd, data=[]):
    assert len(data) &lt;= 12
    data += [0,]*(12-len(data))
    checksum = (sum(data)+cmd-2)%256
    ret = "\xaa\xb4" + chr(cmd)
    ret += ''.join(chr(x) for x in data)
    ret += "\xff\xff" + chr(checksum) + "\xab"

    if DEBUG:
        dump(ret, '> ')
    return ret

def process_data(d):
    r = struct.unpack('&lt;HHxxBB', d[2:])
    pm25 = r[0]/10.0
    pm10 = r[1]/10.0
    checksum = sum(ord(v) for v in d[2:8])%256
    return [pm25, pm10]
    #print("PM 2.5: {} μg/m^3  PM 10: {} μg/m^3 CRC={}".format(pm25, pm10, "OK" if (checksum==r[2] and r[3]==0xab) else "NOK"))

def process_version(d):
    r = struct.unpack('&lt;BBBHBB', d[3:])
    checksum = sum(ord(v) for v in d[2:8])%256
    print("Y: {}, M: {}, D: {}, ID: {}, CRC={}".format(r[0], r[1], r[2], hex(r[3]), "OK" if (checksum==r[4] and r[5]==0xab) else "NOK"))

def read_response():
    byte = 0
    while byte != "\xaa":
        byte = ser.read(size=1)

    d = ser.read(size=9)

    if DEBUG:
        dump(d, '&lt; ')
    return byte + d

def cmd_set_mode(mode=MODE_QUERY):
    ser.write(construct_command(CMD_MODE, [0x1, mode]))
    read_response()

def cmd_query_data():
    ser.write(construct_command(CMD_QUERY_DATA))
    d = read_response()
    values = []
    if d[1] == "\xc0":
        values = process_data(d)
    return values

def cmd_set_sleep(sleep):
    mode = 0 if sleep else 1
    ser.write(construct_command(CMD_SLEEP, [0x1, mode]))
    read_response()

def cmd_set_working_period(period):
    ser.write(construct_command(CMD_WORKING_PERIOD, [0x1, period]))
    read_response()

def cmd_firmware_ver():
    ser.write(construct_command(CMD_FIRMWARE))
    d = read_response()
    process_version(d)

def cmd_set_id(id):
    id_h = (id>>8) % 256
    id_l = id % 256
    ser.write(construct_command(CMD_DEVICE_ID, [0]*10+[id_l, id_h]))
    read_response()

def pub_mqtt(jsonrow):
    cmd = ['mosquitto_pub', '-h', MQTT_HOST, '-t', MQTT_TOPIC, '-s']
    print('Publishing using:', cmd)
    with subprocess.Popen(cmd, shell=False, bufsize=0, stdin=subprocess.PIPE).stdin as f:
        json.dump(jsonrow, f)


if __name__ == "__main__":
    cmd_set_sleep(0)
    cmd_firmware_ver()
    cmd_set_working_period(PERIOD_CONTINUOUS)
    cmd_set_mode(MODE_QUERY);
    while True:
        cmd_set_sleep(0)
        for t in range(15):
            values = cmd_query_data();
            if values is not None and len(values) == 2 and values[0] != 0 and values[1] != 0:
              print("PM2.5: ", values[0], ", PM10: ", values[1])
              time.sleep(2)

	      # ThingSpeak ######
	      f = urllib2.urlopen(baseURL + '&amp;field1=' + str(values[0]) + '&amp;field2=' + str(values[1]))
	      f.read()
	      f.close()
              ###################

              # Push notifications ######
              #_message = "pm25: %.2f, pm10: %.2f, at %s" % (values[0], values[1], time.strftime("%d.%m.%Y %H:%M:%S"))
              #print(_message)
              #try:
              #	p.add('AirQual','Reading', _message, 1, None, "http://www.prowlapp.com/")
              #except Exception,msg:
              #  print(msg)
              ####################


        # open stored data
        try:
            with open(JSON_FILE) as json_data:
                data = json.load(json_data)
        except IOError as e:
            data = []

        # check if length is more than 100 and delete first element
        if len(data) > 100:
            data.pop(0)

        # append new values
        jsonrow = {'pm25': values[0], 'pm10': values[1], 'time': time.strftime("%d.%m.%Y %H:%M:%S")}
        data.append(jsonrow)

        # save it
        with open(JSON_FILE, 'w') as outfile:
            json.dump(data, outfile)

        if MQTT_HOST != '':
            pub_mqtt(jsonrow)

        print("Going to sleep for 1 min...")
        cmd_set_sleep(1)
        time.sleep(60)</code>

Raspberry Pi – Headless Setup

It’s been some time since we wrote our earlier blog describing setting up a Raspberry Pi, and a lot has changed since, including the base operating system. Raspbian Stretch, the latest version of the Debian port for the Raspberry Pi has a lot of great new features and so it is time for an update.

In this blog, we are setting up a Pi in headless mode – that is to say we want it to work over the WiFi via an ssh session from a remote computer from the start – and don’t want to be plugging it into a monitor with a keyboard etc.

The first step is to visit the Raspberry Pi Downloads page. Here, we can either download the ‘Noobs’ installer, or as we will the full Raspbian image. Downloading the Raspbian image, there is a choice between a version with and without a set of recommended software packages installed, Python, Scratch, Sonic Pi, Java etc. Although that is a very useful facility, in this case, we wanted a clean version of Raspbian, so downloaded the file ‘2019-04-08-raspbian-stretch.img’ (the other image file would have ‘-full’ as a suffix. When the file is downloaded, it is a zip file. This is then unzipped to the ‘img’ file.

We now need to use the ‘Etcher’ tool to install the image on our new microSD card. Since last using etcher, we note there is also a new version of this excellent utility from Balena too. We inserted the MicroSD card into a USB reader, inserted into the laptop and ran Etcher. From here we select the image, the destination card and hit ‘Flash’. The image is copied to the card and verified.

The new Balena Etcher programme

Once the image is copied over, we need to make the edits to the new installation to make it work on out network. Using a MacBook laptop, we unplugged the USB reader, and then plugged it back in again. This led to a new volume ‘Boot’ being mounted – an icon appears on the desktop.

The MicroSD card, its USB reader – and for Mac users, the dongle to get the Mac to read the USB ‘A’ device (to USB ‘C’).

We opened a terminal and changed to the new volume:

cd /Volumes/boot

Now we need to add two things, a file in this location called ‘ssh’ to enable secure shell access, and secondly the WiFi credentials.

sudo touch ssh

and to create and edit the Wifi configuration file:

sudo nano wpa_supplicant.conf

In the new file, for Raspbian Stretch, we add the following:

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
 network={
     ssid="WIFI-SSID"
     psk="WIFI-PASSWORD"
     key_mgmt=WPA-PSK
 }
Inserting the MicroSD card in the Pi (already in its case)

The card is now ready to be inserted into the Pi, and the machine booted up for the first time. Hopefully, the Pi will authenticate correctly on the WiFi network.

We can check the Router utility to see what DHCP address the Pi was assigned, or run a command such as ‘ifconfig’ or ‘arp -a’ to inspect connected devices. In our case the IP address, on a local network, could be for example 192.168.1.100

We should now then be able to ssh onto the Pi

ssh pi@192.168.1.100

The default password is ‘raspberry’. Once we are logged in a few important things. First is to change the system password:

passwd

Next, update and upgrade the system (see link):

sudo apt-get update
sudo apt-get dist-upgrade

If necessary, raspi-config can be run to permit further configuration:

sudo raspi-config

We now have a functioning Raspberry Pi, ready for our next project.

Node-RED and the Internet of Things

Here at Cranfield University we are putting in place plans related to the new ‘Living Laboratory’ project, part of our ‘Urban Observatory’. This project sits within the wider UKCRIC initiative, across a number of universities. Of the many experiments in development, we are gathering environmental data from IoT devices and building data dashboards to show the data and related analyses.

In this blog we investigate the use of Node-RED (https://nodered.org) as a programming tool for wiring together hardware devices, APIs and online services, using its browser-based editor to wire together flows using the wide range of nodes in the palette that can be deployed to its runtime in a single-click. Node-RED provides graphical programming tool for Node-JS that permits complex programs to be built pictorially with great ease. To undertake the project, we used a WIO Node device collecting temperature values, exposing these values via a web service, and the Node-RED receiving device being a Raspberry Pi.

Sourcing temperature data – the Wio Node

The Wio Node temperature sensor was described in an earlier blog here (http://www.geothread.net/voice-activated-wio-node-temperature-sensor). Temperature values are extracted via a web-based API call, with the REST URL taking the form, thus:

https://us.wio.seeed.io/v1/node/GroveTemp1WireD1/temp?access_token=TOKEN_GOES_HERE

The temperature values are then returned as a JSON string, appearing thus:

{"temperature":19.1800000000001}

Preparing the Raspberry Pi – installing Node-RED

To prepare the Raspberry Pi and install Node-RED, we first followed instructions to install Node-JS on the Pi at https://www.w3schools.com/nodejs/nodejs_raspberrypi.asp. Next we followed the instructions on the Node-RED site (https://nodered.org/docs/hardware/raspberrypi). In brief, we ran the Node-RED upgrade script:

bash <(curl -sL https://raw.githubusercontent.com/node-red/raspbian-deb-package/master/resources/update-nodejs-and-nodered)

We then set Node-RED to start automatically on boot, with:

sudo systemctl enable nodered.service

Running Node-RED

The Raspberry Pi was then rebooted. We were then able to start using the Node-RED editor (https://nodered.org/docs/hardware/raspberrypi#using-the-editor), calling the web-based interface with the URL (the IP address being that if the Raspberry Pi):

http://{the-ip-address-returned}:1880/

The general Node-RED interface, ‘palette’ to the left, properties to the right, and design canvas centrally.

Node-RED allows installation of many modules, one of which permits data dashboards. The data dashboard module is described at https://flows.nodered.org/node/node-red-dashboard. Installation can be via npm, as described at the link above. However, we used the ‘Manage Palette’ option within the graphical interface to install the new functions.

With this installed, the next task was to develop the ‘flow’, or programme. This starts with a HTTP GET call to the WIO Node as described above. For this the ‘http request’ node is called, and configured with the URI to the temperature value. After consideration of the various configuration options, we elected to return a ‘parsed JSON object’.

To drive the process whereby the URI is called continuously, the http request call is preceded with an ‘inject node’, set to run continuously on a timed basis (shown here at 5 seconds, although that could be a longer period).

The data that is returned from this process, the ‘payload’, can now be passed directly to the first element of the dashboard – the gauge. The payload JSON object has a member ‘temperature’, referenced via the value format {{payload.temperature}}.

The next dashboard elements we wanted are firstly a line graph of temperature over time, and secondly a custom node recording the ‘minimum’ and ‘maximum’ temperatures over time. These nodes will need data prepared in a particular way. The graph, or chart, needs data in the form described at https://github.com/node-red/node-red-dashboard/blob/master/Charts.md.

{topic:"temperature", payload:22}

In addition, further JSON elements for minimum and maximum values will be required. In order to construct a revised message payload, a custom script is required. Explanations are in the code below:

// Create a new empty object 'newMsg' to return at the end
// then fill it with another empty object 'bounds'
var newMsg={bounds:{}}; // create

// Create two local variables min and max initialised from the persistent 
// context variables of the same names where these values exist, or else
// seed with values we know are off the scale
var min=context.get('min') || 100;
var max=context.get('max') || -100;

// Set an element 'topic' and give the value the string 'temperature'
newMsg.topic = 'temperature';
// Set the payload element to the incoming message payload temperature
newMsg.payload = msg.payload.temperature

// update the min and max, comparing the incoming values to the context
if (msg.payload.temperature < min) {
   newMsg.bounds.min = msg.payload.temperature;
   context.set('min', msg.payload.temperature);
} else {
   newMsg.bounds.min = min;
}
if (msg.payload.temperature > max) {
   newMsg.bounds.max = msg.payload.temperature;
   context.set('max', msg.payload.temperature);
} else {
   newMsg.bounds.max = max;
}

// and finally return the new object 'newMsg'
return newMsg;

What is always a good idea when processing data is to have a debug that shows the whole message object constructed by this process. To do this, a ‘debug node’ is added and configured – here to show the ‘complete msg object’. We can see the min and max are contained in the bounds node, and that the ‘topic’ and ‘payload’ elements are correctly configured.

As a result, the two additional dashboard node widgets can be added, first the chart node. The line interpolation is set here to ‘bezier’ to provide a smoother visualisation. The time interval is set to 15 minutes.

Next we wanted to add a new custom node widget to show a running maximum and minimum value. To do this, we added a ‘Template node’ and configured it thus:

<div layout="row" layout-align="start center">
  <span flex>Temp Min: </span>
  <span flex>Temp Max: </span>
</div>
<div layout="row" layout-align="start center" ng-repeat="bounds in msg">
  <span flex style="color: green">{{bounds.min}}</span>
  <span flex style="color: red">{{bounds.max}}</span>
</div>

Once these elements are all in place, the ‘flow’ programme can be deployed. This commences the running of the code, and then the dashboard can be accessed. The easiest means to do this is to follow the link in the properties section as shown:

The result is the display of the dashboard. To get this to display as required, one can change the visual style (e.g. to ‘dark’), and the dimensions of the canvas. Node dashboard widgets are always rendered to the top left according to the layout properties.

Epilogue

In this blog, we have shown how the Node-RED environment can be used to streamline Node-JS code, with customised elements, and inclusion of libraries of functionality (dashboard). Node-RED is a powerful yet easy to configure environment that is cable of a whole range of functionality though its graphical ‘flows’. There are many example flows available on websites that can be downloaded and tested. Flows are designed to be easily imported and exported. Below is the export for the flow described above – to load it, select ‘Import’ and ‘Clipboard’ from the main menu options and paste in the following.

<div layout="row" layout-align="start center">[{"id":"d988539b.52bdc8","type":"tab","label":"Temperature","disabled":false,"info":""},{"id":"35963a2e.6aa056","type":"tab","label":"Temperature","disabled":false,"info":""},{"id":"166841a0.b19cce","type":"mqtt-broker","z":"","broker":"192.168.0.6","port":"1883","clientid":"Teste","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"a76a54d5.4c5998","type":"ui_tab","z":"d988539b.52bdc8","name":"ESP_DTH11","icon":"dashboard","order":3,"disabled":false,"hidden":false},{"id":"519167a8.570e5","type":"ui_group","z":"d988539b.52bdc8","name":"DHT11","tab":"a76a54d5.4c5998","order":1,"disp":true,"width":"12","collapse":false},{"id":"1785bc54.de4d24","type":"ui_base","theme":{"name":"theme-dark","lightTheme":{"default":"#0094CE","baseColor":"#0094CE","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif","edited":true,"reset":false},"darkTheme":{"default":"#097479","baseColor":"#097479","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif","edited":true,"reset":false},"customTheme":{"name":"Untitled Theme 1","default":"#4B7930","baseColor":"#4B7930","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"},"themeState":{"base-color":{"default":"#097479","value":"#097479","edited":false},"page-titlebar-backgroundColor":{"value":"#097479","edited":false},"page-backgroundColor":{"value":"#111111","edited":false},"page-sidebar-backgroundColor":{"value":"#000000","edited":false},"group-textColor":{"value":"#0eb8c0","edited":false},"group-borderColor":{"value":"#555555","edited":false},"group-backgroundColor":{"value":"#333333","edited":false},"widget-textColor":{"value":"#eeeeee","edited":false},"widget-backgroundColor":{"value":"#097479","edited":false},"widget-borderColor":{"value":"#333333","edited":false},"base-font":{"value":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"}},"angularTheme":{"primary":"indigo","accents":"blue","warn":"red","background":"grey"}},"site":{"name":"Node-RED Dashboard","hideToolbar":"false","allowSwipe":"false","lockMenu":"false","allowTempTheme":"true","dateFormat":"DD/MM/YYYY","sizes":{"sx":48,"sy":48,"gx":6,"gy":6,"cx":6,"cy":6,"px":0,"py":0}}},{"id":"749056a0.0a1d28","type":"ui_group","z":"","name":"Chart","tab":null,"order":2,"disp":true,"width":"12","collapse":false},{"id":"684a7caa.4db0f4","type":"ui_group","z":"","name":"Chart","tab":"a76a54d5.4c5998","order":2,"disp":true,"width":"12","collapse":false},{"id":"84ea1128.ec6fd","type":"ui_tab","z":"35963a2e.6aa056","name":"ESP_DTH11","icon":"dashboard","order":3,"disabled":false,"hidden":false},{"id":"c86b0ed1.65efc8","type":"ui_group","z":"35963a2e.6aa056","name":"DHT11","tab":"84ea1128.ec6fd","order":1,"disp":true,"width":"12","collapse":false},{"id":"a7d331dd.9d8078","type":"debug","z":"d988539b.52bdc8","name":"Message object","active":true,"tosidebar":true,"console":true,"tostatus":false,"complete":"true","targetType":"full","x":1129.75,"y":286.9166564941406,"wires":[]},{"id":"aa810265.1f789","type":"ui_gauge","z":"d988539b.52bdc8","name":"Gauge","group":"519167a8.570e5","order":0,"width":"6","height":"2","gtype":"gage","title":"Temperature","label":"Celsius","format":"{{payload.temperature}}","min":0,"max":"60","colors":["#00b500","#e6e600","#ca3838"],"seg1":"25","seg2":"28","x":1086.833251953125,"y":432.8055419921875,"wires":[]},{"id":"aa922201.f96eb8","type":"inject","z":"d988539b.52bdc8","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":401.5,"y":394,"wires":[["cbeca854.f6174"]]},{"id":"cbeca854.f6174","type":"http request","z":"d988539b.52bdc8","name":"Wio Temperature","method":"GET","ret":"obj","paytoqs":false,"url":"https://us.wio.seeed.io/v1/node/GroveTemp1WireD1/temp?access_token=7c6297dfa2e48793c58a53269bc23ef0","tls":"","proxy":"","authType":"basic","x":610.5,"y":394,"wires":[["aa810265.1f789","cd69dcd.1c5d3a"]]},{"id":"af52c259.fecbd8","type":"ui_chart","z":"d988539b.52bdc8","name":"Chart","group":"684a7caa.4db0f4","order":2,"width":"12","height":"7","label":"Temperature chart","chartType":"line","legend":"true","xformat":"HH:mm:ss","interpolate":"bezier","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"15                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ","removeOlderPoints":"50","removeOlderUnit":"60","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1087.8333740234375,"y":384.22222900390625,"wires":[[]]},{"id":"cd69dcd.1c5d3a","type":"function","z":"d988539b.52bdc8","name":"Process temperature","func":"var newMsg={bounds:{}};\nvar min=context.get('min') || 100;\nvar max=context.get('max') || -100;\n\n// http://www.steves-internet-guide.com/node-red-variables/\nnewMsg.topic = 'temperature';\nnewMsg.payload = msg.payload.temperature\n\nif (msg.payload.temperature &lt; min) {\n   newMsg.bounds.min = msg.payload.temperature;\n   context.set('min', msg.payload.temperature);\n} else {\n   newMsg.bounds.min = min;\n}\nif (msg.payload.temperature &gt; max) {\n   newMsg.bounds.max = msg.payload.temperature;\n   context.set('max', msg.payload.temperature);\n} else {\n   newMsg.bounds.max = max;\n}\n\nreturn newMsg;","outputs":1,"noerr":0,"x":875.5,"y":336,"wires":[["af52c259.fecbd8","a7d331dd.9d8078","81bda4f5.6f104"]]},{"id":"259fa218.53bdbe","type":"comment","z":"d988539b.52bdc8","name":"Useful links","info":"see:\nhttps://github.com/node-red/node-red-dashboard/blob/master/Charts.md\nhttp://noderedguide.com/tutorial-node-red-dashboards-multiple-lines-on-a-chart/#more-1612\nhttp://www.steves-internet-guide.com/node-red-functions/\nhttp://www.steves-internet-guide.com/node-red-dashboard/","x":400.5,"y":337,"wires":[]},{"id":"81bda4f5.6f104","type":"ui_template","z":"d988539b.52bdc8","group":"519167a8.570e5","name":"Max and Min","order":2,"width":"6","height":"2","format":"</p><div layout="\&quot;row\&quot;" layout-align="\&quot;start" center\"="">\n  <span flex="">Temp Min: </span>\n  <span flex="">Temp Max: </span>\n</div>
<p>\n</p><div layout="\&quot;row\&quot;" layout-align="\&quot;start" center\"="" ng-repeat="\&quot;bounds" in="" msg\"="">\n  <span flex="" style="\&quot;color:" green\"="">{{bounds.min}}</span>\n  <span flex="" style="\&quot;color:" red\"="">{{bounds.max}}</span>\n</div>
<p>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":1109.4444122314453,"y":336.6666717529297,"wires":[[]]},{"id":"f5bb5785.45e55","type":"debug","z":"35963a2e.6aa056","name":"Message object","active":true,"tosidebar":true,"console":true,"tostatus":false,"complete":"true","targetType":"full","x":1129.75,"y":286.9166564941406,"wires":[]},{"id":"27187c33.85c07c","type":"ui_gauge","z":"35963a2e.6aa056","name":"Gauge","group":"c86b0ed1.65efc8","order":0,"width":"6","height":"2","gtype":"gage","title":"Temperature","label":"Celsius","format":"{{payload.temperature}}","min":0,"max":"60","colors":["#00b500","#e6e600","#ca3838"],"seg1":"25","seg2":"28","x":1086.833251953125,"y":432.8055419921875,"wires":[]},{"id":"cd94fffc.6f0da8","type":"inject","z":"35963a2e.6aa056","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":401.5,"y":394,"wires":[["ab8d1686.0264d"]]},{"id":"ab8d1686.0264d","type":"http request","z":"35963a2e.6aa056","name":"Wio Temperature","method":"GET","ret":"obj","paytoqs":false,"url":"https://us.wio.seeed.io/v1/node/GroveTemp1WireD1/temp?access_token=&lt;<your token="" here="">&gt;","tls":"","proxy":"","authType":"basic","x":610.5,"y":394,"wires":[["27187c33.85c07c","ce9f0009.faab98"]]},{"id":"dbe1013a.863be8","type":"ui_chart","z":"35963a2e.6aa056","name":"Chart","group":"684a7caa.4db0f4","order":2,"width":"12","height":"7","label":"Temperature chart","chartType":"line","legend":"true","xformat":"HH:mm:ss","interpolate":"bezier","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"15                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ","removeOlderPoints":"50","removeOlderUnit":"60","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1087.8333740234375,"y":384.22222900390625,"wires":[[]]},{"id":"ce9f0009.faab98","type":"function","z":"35963a2e.6aa056","name":"Process temperature","func":"var newMsg={bounds:{}};\nvar min=context.get('min') || 100;\nvar max=context.get('max') || -100;\n\nnewMsg.topic = 'temperature';\nnewMsg.payload = msg.payload.temperature\n\nif (msg.payload.temperature &lt; min) {\n   newMsg.bounds.min = msg.payload.temperature;\n   context.set('min', msg.payload.temperature);\n} else {\n   newMsg.bounds.min = min;\n}\nif (msg.payload.temperature &gt; max) {\n   newMsg.bounds.max = msg.payload.temperature;\n   context.set('max', msg.payload.temperature);\n} else {\n   newMsg.bounds.max = max;\n}\n\nreturn newMsg;","outputs":1,"noerr":0,"x":875.5,"y":336,"wires":[["dbe1013a.863be8","f5bb5785.45e55","b809b11b.7f47c8"]]},{"id":"b809b11b.7f47c8","type":"ui_template","z":"35963a2e.6aa056","group":"c86b0ed1.65efc8","name":"Max and Min","order":2,"width":"6","height":"2","format":"</your></p><div layout="\&quot;row\&quot;" layout-align="\&quot;start" center\"="">\n  <span flex="">Temp Min: </span>\n  <span flex="">Temp Max: </span>\n</div>
<p>\n</p><div layout="\&quot;row\&quot;" layout-align="\&quot;start" center\"="" ng-repeat="\&quot;bounds" in="" msg\"="">\n  <span flex="" style="\&quot;color:" green\"="">{{bounds.min}}</span>\n  <span flex="" style="\&quot;color:" red\"="">{{bounds.max}}</span>\n</div>
<p>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":1109.4444122314453,"y":336.6666717529297,"wires":[[]]}]

Logging footfall counts with a Raspberry Pi and camera – technical considerations

Here at Cranfield University we are putting in place plans related to the new ‘Living Laboratory’ project, part of our ‘Urban Observatory’. This project sits within the wider UKCRIC initiative, across a number of universities. Of the many experiments in development, we are exploring machine vision as a means to provide  footfall counts of pedestrian traffic in parts of the campus. This blog provides summarises some of the technical considerations relating to this work.

Siting the Raspberry Pi and camera

Earlier blogs here on GeoThread have described the use of a Raspberry Pi Zero running the Kerboros software, which uses OpenCV to capture movement in different ways from an attached, or network connected, camera. One of the great additions to Kerboros is a feature permitting ‘counting’ of objects passing between two identified ‘fences’ within the image view. The results of an event being triggered in this way can be output to a range of IO streams. Thus IO can send a result to a stored image file to disk (Disk), send a snippet of video to disk (Video), trigger the GPIO pins on the Raspberry Pi (GPIO), send a packet to a TCP Socket (TCPSocket), send a HTTP POST REST communication (Webhook), run a local (Script), trigger a MQ Telemetry Transport communication (MQTT) or send a push notification message (Pushbullet) – a great selection of choices, to which is also added the ability to write to the Amazon S3 cloud as well as local disk. Outputs can be made in either single streams, or multiple streams – thus one can save an image to disk ‘and‘ run a script for example.

In an earlier blog, we had logged movements in this way to a bash script that executed a series of one-line Python commands to store the triggered output data to disk, appending to a CSV file. However, this approach only offers a limited solution, especially if there are multiple cameras involved. A better strategy is to write data out to a REST endpoint on a web server, using the Webhook capability. In this way the posted JSON data can be collated into a central database (from multiple camera sources).

The option for Webhook was selected, with the URL form of:
<<IP_ADDRESS>>:3000/counter

The reason for this port number, and REST endpoint are made clear in the NodeJS receiver code below.

We set up a separate server running Linux Ubuntu to receive these data. For a database, we selected Postgres as a popular and powerful Open Source SQL database. As a server, able to receive the HTTP POST communication, we selected NodeJS. Once installed, we also used the Node Package Manager (NPM) to install the Express lightweight web server, as well as the Node Postgres framework, node-postgres.

ssh username@IPADDRESS-OF-SERVER
# Update the new server
sudo apt-get update
sudo apt-get upgrade

# Install curl, to enable loading to software repositories
sudo apt install curl

#Installation of Node:
curl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash -<sudo apt-get install -y nodejs
node -v
    v11.13.0
npm -version
    6.7.0

#Installation of Postgres:
#To install PostgreSQL, as well as the necessary server software, we ran the following command:
sudo apt-get install postgresql postgresql-contrib
sudo apt install postgresql-10-postgis-2.4
sudo apt install postgresql-10-postgis-scripts

We then configured Postgres to accept communication from the Raspberry Pi devices

sudo nano /etc/postgresql/10/main/pg_hba.conf<br>
#      host     all     all     <<IPADDRESS>>/24     md5

sudo nano /etc/postgresql/10/main/postgresql.conf
# search for 'listen' in file
#      listen_addresses = '<<SETTING>>'

We then used PSQL (the Postgres command line utility) to update the system password, and then to add a new user to Postgres to own the resultant database.

# Change password for the postgres role/account:
sudo -u postgres psql
\password postgres
#enter a new secure password for the postgres user role account

#create a new user role/account for handling data, and allow it to create new databases
CREATE ROLE <<USERNAME>> WITH LOGIN PASSWORD '<<NEW_PASSWORD>>';
ALTER ROLE <<USERNAME>> CREATEDB;

Show all the users to check this worked

\du
# and then quit psql
\q

Finally, a review of the commands to start, stop and restart Postgres.

sudo service postgresql start
sudo service postgresql stop
sudo service postgresql restart

Next, the new Postgres user was used to create a new table with the following fields

id integer NOT NULL DEFAULT [we used the data type SERIAL to outnumber records, and set this field as the Primary Key]
“regionCoordinates” character varying(30)
“numberOfChanges” integer
incoming integer
outgoing integer
“timestamp” character varying(30)
microseconds character varying(30)
token integer
“instanceName” character varying(30)

Next the nodeJS project environment was established:

cd
mkdir node-api-postgres
cd node-api-postgres
npm init -y

# the editor 'nano' is used to inspect and edit the resultant file 'package.json', and to edit the description as required:
nano package.json  (to edit description)

Lastly, the node package manager npm was used to install the two required node libraries Express and pg:

npm i express pg

Next, drawing from the excellent LogRocket blog, an application was written to receive the data, comprising two files, ‘index.js’, and ‘queries.js’, as follows:

index.js

// index.js
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
const db = require('./queries')
const port = 3000

app.use(bodyParser.json())
app.use(
   bodyParser.urlencoded({
     extended: true,
   })
)

app.get('/', (request, response) =&gt; {
   response.send('Footfall counter - Service running')
})

app.post('/counter', db.createFootfall)

app.listen(port, () =&gt; {
   console.log('App running on port ${port}.')
})

queries.js

// queries.js

const Pool = require('pg').Pool
const pool = new Pool({
   user: '<<username>>',
   host: '<<hostname>>',
   database: '<<database>>',
   password: '<<password>>',
   port: 5432,
})

const createFootfall = (request, response) => {
  const {regionCoordinates, numberOfChanges, incoming, outgoing, timestamp, microseconds, token, instanceName} = request.body
  pool.query('INSERT INTO <<tablename>> ("regionCoordinates", "numberOfChanges", "incoming", "outgoing", "timestamp", "microseconds", "token", "instanceName") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *', [regionCoordinates, numberOfChanges, incoming, outgoing, timestamp, microseconds, token, instanceName], (error, result) => {
    if (error) {
      console.log(error.stack)
    }
    console.log(`Footfall added with the ID: ${result.rows[0].id}`)
    response.status(200).send(`Footfall added with ID: ${result.rows[0].id}\n`)
  })
}

module.exports = {
  createFootfall
}

One thing to note in the code above is the use of ‘RETURNING *’ within the SQL statement – this allows the ‘result’ object to access the full row of data, including the automatically generated Id reference. Otherwise this would not be accessible as it it generated on the server and not passed in by the INSERT statement.

Now the Node application is started up, listening on port 3000 on the REST endpoint /counter’ (both defined in index.js).

node index.js

The result is data streaming into the Postgres database.

Once this is in place, we can run a test to check the system can receive data. For this we can use the curl command again, like this:

curl --data "regionCoordinates=[413,323,617,406]&numberOfChanges=1496&incoming=1&outgoing=0&timestamp=1539760397&microseconds=6-928567&token=722&instanceName=LivingLabTest" http://<<IPADDRESS>>:3000/counter
Footfall added with ID: 1

The result is a new record is added to the database. To check the data row was added correctly, a quick way is to use the graphical pgAdmin tool that is used to interact with Postgres databases. Set up a new connection to the server database, and inspect the table to see the record, thus:

pgAdmin utility

The next step is to connect to the Raspberry Pi Zero running Kerboros, and configure its output to the IO output ‘Webhook’ taking, as noted above, a URL form of:
<<IP_ADDRESS>>:3000/counter.

Note here the port number of 3000 referenced, and also a request from Manchester. Once the Kerboros software is updated, data should be seen to be arriving at the database. pgAdmin can be used again to inspect the result.

A final step then is to set up a daemon service that can stop and start the node programme. This means that if the server is rebooted, the We followed the Hackernoon blog and took the following steps:

First, a file was created “/etc/systemd/system/node-api-postgres.service”, with the following content:

[Unit]
Description=Node.js node-api-postgres Footfall service

[Service]
PIDFile=/tmp/node-api-postgres.pid
User=<<USERNAME>>
Group=<<GROUP>>
Restart=always
KillSignal=SIGQUIT
WorkingDirectory=/home/<<USERFOLDER>>/node-api-postgres/
ExecStart=/home/<<USERFOLDER>>/node-api-postgres/index.js

[Install]
WantedBy=multi-user.target

Next, make the file executable:

chmod +x index.js 

Next, the service can be prepared:

sudo systemctl enable node-api-postgres.service

If the service file needs any editing after the service is prepared, the daemons need to all be reloaded, thus:

systemctl daemon-reload

Finally, the service may now be started, stopped or restarted:

sudo systemctl start node-api-postgres.service
sudo systemctl stop node-api-postgres.service
sudo systemctl restart node-api-postgres.service

A command can also be added to restart the Postgres database on a reboot:

update-rc.d postgresql enable

Conclusion and Epilogue
This blog has shown how to capture footfall counting data sourced from a Rasberry Pi with a camera running Kerboros, to a separate server running the database Postgres, using NodeJS and related packages. The result is a robust logging environment capable of receiving data from one or more cameras logged to database.
In order to operate the system, the general instructions are as follows:
First, log onto the Unix box

ssh <<USER>>@<<IPADDRESS>>

Next, update and upgrade the system (do this regularly)

sudo apt-get update
sudo apt-get upgrade

Ensure Postgres is running

sudo service postgresql start
sudo service postgresql stop
sudo service postgresql restart

Next, ensure the Node app is running

sudo systemctl start node-api-postgres.service

You should now just wait for data to arrive – using pgAdmin to inspect and interrogate the database table.

Logging footfall counts with a Raspberry Pi and camera – ethical considerations

Here at Cranfield University we are putting in place plans related to the new ‘Living Laboratory’ project, part of our ‘Urban Observatory’. This project sits within the wider UKCRIC initiative, across a number of universities. Of the many experiments in development, we are exploring machine vision as a means to provide  footfall counts of pedestrian traffic in parts of the campus. This blog provides summarises some of the ethical considerations relating to this work.

An important early part of this project involves preparing, submitting and securing ethical approval for the planned work. In the first instance we are running an experiment in a particular campus office area. Before commencing any technical work, a full approval case has to be prepared and submitted for assessment.

In the case of this experiment, we are intending to log only a count of pedestrian movements, with no personally identifiable imagery captured. Informed written consent is obtained from all residents of the office area, and signage put up for visitors, see below. The case outlining these intentions is drawn up and submitted.

These matters are raised here as a precursor to the technical description to follow, as IoT projects require careful consideration of privacy matters.

Note, image above is shown with the specific details removed

Machine Vision with a Raspberry Pi

In this blog we will describe the steps needed to do some machine vision using the Raspberry Pi Zeros we described in the earlier blog. Here at Cranfield University we are building these amazing devices into our research. In this case we are interested in using the Pi as a device for counting pedestrians passing a site – trying to understand how different design choices influence people’s choice of walking routes.

Contents:
Background
Toolkits
Kerberos
   Installation of Kerberos
   Configuration of Kerberos
   Configuration of the Pi
   Output and Data Capture from Kerberos
Epilogue

Background:

top
In the earlier blog we showed how to set up the Raspberry Pi Zero W, connecting up the new v2 camera in a case and connecting power. Once we had installed Rasbian on a new microSD card all was ready to go.

A bit of research was needed to understand the various options for machine vision on a Pi. There are three levels we might want. First a simple motion detection with the camera would give a presence or absence of activity, but not much more. This could be useful when pointing the camera directly at a location. Second, we can use more sophisticated approaches to consider detecting movement passing across the camera’s view, for example left to right or vice versa. This could be useful when pointing the camera transverse to a route along which pedestrians are travelling. Thirdly, and with the ultimate sophistication, we could try and classify the image to detect what the ‘objects’ passing across the view are. Classifier models might for example detect adults, young persons, and other items such as bicycles and push buggies etc. Needless to say, we wanted to start off easy and then work up the list!

Looking at the various software tools available, it is clear that many solutions draw on OpenCV (Open Source Computer Vision Library) (https://opencv.org). OpenCV is an open source computer vision and machine learning software library, built to provide a common infrastructure for computer vision applications and to accelerate the use of machine perception. There are many other potential libraries for machine vision – for example, SOD (https://sod.pixlab.io), and other libraries such as Dlib (http://dlib.net). OpenCV can be daunting, and there are wrappers such as SimpleCV (http://simplecv.org) to try and simplify the process.

Toolkits:

top
We then looked at options for toolkits that use these basic building blocks. A useful reference is Jason Antman’s blog here https://blog.jasonantman.com/2018/05/linux-surveillance-camera-software-evaluation/. Although not Jason’s final choice, the tool that stuck out to us was Kerberos (https://kerberos.io), developed by Cedric Verstraeten and grown out of his earlier OpenCV project (https://github.com/cedricve/motion-detection).

Kerberos:

top
Kerberos has a number of key resources:
Main home website – https://kerberos.io
Documentation – https://doc.kerberos.io
Git – https://github.com/kerberos-io
Helpdesk – https://kerberosio.zendesk.com
Corporate – https://verstraeten.io
Gitter – https://gitter.im/kerberos-io/home

Although the full source for Kerberos is available, and also a docker implementation, what we really liked was the SD image for the Raspberry Pi Zero – so really made for the job.

Installation of Kerberos:

top
We downloaded the cross-platform installer from the Kerberos website. This is based on the Etcher tool, used to install Rasbian so familiar to any Pi user. In our case we selected the Mac installer, downloading an installer dmg file (c.80Mb). Then, ensuring the Micro SD card destined for the Pi was in a flash writer dongle attached to the Mac, we were able to easily install the image. The Etcher app asks a couple of questions on the way about the WiFi network SSID and WiFi and system passcodes, as well as a name for the device, and writes these details onto the SD card with the rest of the image. As a result, on inserting the SD card and booting the Pi with the Kerberos image, the device started up and connected correctly and without issue on the WiFi network. A check on the router on our closed network showed the device had correctly registered itself at IP address 192.168.1.24.

Management of the Pi and camera is achieved via app running on a web server on the Pi. So to access our device, we entered browsed the URL http://192.168.1.24/login.

Configuration of Kerberos:

top
The dashboard app provides complete control over the operation of the Pi and camera.The image here shows the ‘heatmap’ camera view, and statistical graphs and charts of timings of activations.
. To configure the many settings we headed over to https://doc.kerberos.io for the documentation. The concept is that the image processing is undertaken on the ‘Machinery’ configuration, and that the ‘Web’ then controls access to the results.

Selecting ‘Configuration’ we could start adjusting the settings for the Machinery as we required. There are default settings for all the options.
However, the settings you will use depend on the application for the device. We followed the settings for ‘People Counter‘ recommended both in the docs, and a subsequent blog. It seems that there the settings are very sensitive, so one has to adjust until the desired results are obtained.

Being on a Raspberry Pi, one can also ssh connect directly to the device on a terminal connection (eg from terminal on the Mac, or via Putty from a PC). Connect to the device with the command:

ssh root@192.168.1.22
cd /data/machinery/config

This takes you to the location of the configuration files, as written out by the web app. Below are the settings we used to get the People Counter working (the values here correspond to the settings in the web app).

less config.xml
<!--?xml version="1.0"?-->
<kerberos>
    <instance>
        <name type="text">Stationery</name>
        <logging type="bool">false</logging>
        <timezone type="timezone">Europe-London</timezone>
        <capture file="capture.xml">RaspiCamera</capture>
        <stream file="stream.xml">Mjpg</stream>
        <condition file="condition.xml" type="multiple">Enabled</condition>
        <algorithm file="algorithm.xml">DifferentialCollins</algorithm>
        <expositor file="expositor.xml">Rectangle</expositor>
        <heuristic file="heuristic.xml">Counter</heuristic>
        <io file="io.xml" type="multiple">Webhook</io>
        <cloud file="cloud.xml">S3</cloud>
    </instance>
</kerberos>
less capture.xml
<!--?xml version="1.0"?-->
<captures>
    <ipcamera>
        <url type="text">xxxxxxxxx</url>
        <framewidth type="number">640</framewidth>
        <frameheight type="number">480</frameheight>
        <delay type="number">500</delay>
        <angle type="number">0</angle>
    </ipcamera>
    <usbcamera>
        <framewidth type="number">640</framewidth>
        <frameheight type="number">480</frameheight>
        <devicenumber type="number">0</devicenumber>
        <fourcc type="text">MJPG</fourcc>
        <delay type="number">500</delay>
        <angle type="number">0</angle>
    </usbcamera>
    <raspicamera>
        <framewidth type="number">640</framewidth>
        <frameheight type="number">480</frameheight>
        <delay type="number">500</delay>
        <angle type="number">0</angle>
        <framerate type="number">20</framerate>
        <sharpness type="number">0</sharpness>
        <saturation type="number">0</saturation>
        <contrast type="number">0</contrast>
        <brightness type="number">50</brightness>
    </raspicamera>
    <videocapture>
        <framewidth type="number">640</framewidth>
        <frameheight type="number">480</frameheight>
        <path type="text">0</path>
        <delay type="number">500</delay>
        <angle type="number">0</angle>
    </videocapture>
</captures>
less stream.xml
<!--?xml version="1.0"?-->
<streams>
    <mjpg>
    	<enabled type="bool">true</enabled>
    	<streamport type="number">8889</streamport>
    	<quality type="number">75</quality>
    	<fps type="number">15</fps>
    	<username type="text"></username>
    	<password type="text"></password>
    </mjpg>
</streams>
less condition.xml
<!--?xml version="1.0"?-->
<conditions>
    <time>
        <times type="timeselection">0:01,23:59-0:01,23:59-0:01,23:59-0:01,23:59
-0:01,23:59-0:01,23:59-0:01,23:59</times>
        <delay type="number">10000</delay>
    </time>
    <enabled>
    	<active type="bool">true</active>
        <delay type="number">5000</delay>
    </enabled>
</conditions>
less algorithm.xml
<!--?xml version="1.0"?-->
<algorithms>
	<differentialcollins>
		<erode type="number">5</erode>
    	        <threshold type="number">15</threshold>
        </differentialcollins>
	<backgroundsubtraction>
		<shadows type="text">false</shadows>
		<history type="number">15</history>
		<nmixtures type="number">5</nmixtures>
		<ratio type="number">1</ratio>
		<erode type="number">5</erode>
		<dilate type="number">7</dilate>
    	<threshold type="number">10</threshold>
    </backgroundsubtraction>
</algorithms>
less expositor.xml
<!--?xml version="1.0"?-->
<expositors>
	<rectangle>
	    <region>
		    <x1 type="number">0</x1>
		    <y1 type="number">0</y1>
		    <x2 type="number">800</x2>
		    <y2 type="number">600</y2>
		 </region>
	</rectangle>
        <hull>
	    <region type="hullselection">779,588|781,28|588,48|377,31|193,31|32
,45|33,625|191,591|347,600|456,572|556,601|659,629</region>
	</hull>
</expositors>
less heuristic.xml
<!--?xml version="1.0"?-->
<heuristics>
	<sequence>
	    <minimumchanges type="number">20</minimumchanges>
	    <minimumduration type="number">2</minimumduration>
        <nomotiondelaytime type="number">1000</nomotiondelaytime>
	</sequence>
	<counter>
	    <appearance type="number">3</appearance>
	    <maxdistance type="number">140</maxdistance>
	    <minarea type="number">200</minarea>
	    <onlytruewhencounted type="bool">false</onlytruewhencounted>
	    <minimumchanges type="number">5</minimumchanges>
        <nomotiondelaytime type="number">100</nomotiondelaytime>
		<markers type="twolines">34,29|36,461|617,22|614,461</markers>
	</counter>
</heuristics>

Note the settings above for the twolines markers on the video image – used for counting pedestrians passing from left to right, and from right to left, (coordinate position 0,0 is the top left corner)

less io.xml
<!--?xml version="1.0"?-->
<ios>
    <disk>
        <fileformat type="text">timestamp_microseconds_instanceName_regionCoord
inates_numberOfChanges_token.jpg</fileformat>
        <directory type="text">/etc/opt/kerberosio/capture/</directory>
        <markwithtimestamp type="bool">false</markwithtimestamp>
        <timestampcolor type="text">white</timestampcolor>
        <privacy type="bool">false</privacy>
        <throttler type="number">0</throttler>
    </disk>
    <video>
        <fps type="number">30</fps>
        <recordafter type="number">5</recordafter>
        <maxduration type="number">30</maxduration>
        <extension type="number">mp4</extension>
        <codec type="number">h264</codec>
        <fileformat type="text">timestamp_microseconds_instanceName_regionCoord
inates_numberOfChanges_token</fileformat>
        <directory type="text">/etc/opt/kerberosio/capture/</directory>
        <hardwaredirectory type="text">/etc/opt/kerberosio/h264/
        <enablehardwareencoding type="bool">true</enablehardwareencoding>
        <markwithtimestamp type="bool">false</markwithtimestamp>
        <timestampcolor type="text">white</timestampcolor>
        <privacy type="bool">false</privacy>
        <throttler type="number">0</throttler>
    </hardwaredirectory></video>
    <gpio>
        <pin type="number">17</pin>
        <periods type="number">1</periods>
        <periodtime type="number">100000</periodtime>
        <throttler type="number">0</throttler>
    </gpio>
    <tcpsocket>
        <server type="number">IP_ADDRESS:3000/counter</server>
        <port type="number"></port>
        <message type="text">motion-detected</message>
        <throttler type="number">0</throttler>
    </tcpsocket>
    <webhook>
        <url type="text">IP_ADDRESS:3000/counter</url>
        <throttler type="number">500</throttler>
    </webhook>
    <script>
        <path type="text">/etc/opt/kerberosio/scripts/run.sh</path>
        <throttler type="number">0</throttler>
    </script>
    <mqtt>
        <secure type="bool">false</secure>
        <verifycn type="bool">false</verifycn>
        <server type="number">IP_ADDRESS</server>
        <port type="number">1883</port>
        <clientid type="text"></clientid>
        <topic type="text">kios/mqtt</topic>
        <username type="text"></username>
        <password type="text"></password>
        <throttler type="number">0</throttler>
    </mqtt>
    <pushbullet>
        <url type="text">https://api.pushbullet.com</url>
        <token type="text">xxxxxx</token>
        <throttler type="number">10</throttler> 
    </pushbullet>
</ios>

Configuration of the Pi:

top
Another configuration required was to tun off the bright green LED on the Raspberry Pi as it draws attention when the unit is operating. To turn OFF the LEDs for Zero, we followed the instructions at https://www.jeffgeerling.com/blogs/jeff-geerling/controlling-pwr-act-leds-
raspberry-pi. Note that unlike other Raspberry Pi models, the Raspberry Pi Zero only has one LED, led0 (labeled ‘ACT’ on the board). The LED defaults to on (brightness 0), and turns off (brightness 1) to indicate disk activity.

To turn off the LEDs interactively, the following commands can be run each time the Pi boots.

# Set the Pi Zero ACT LED trigger to 'none'.
echo none | sudo tee /sys/class/leds/led0/trigger
# Turn off the Pi Zero ACT LED.
echo 1 | sudo tee /sys/class/leds/led0/brightness

To make these settings permanent, add the following lines to the Pi’s ‘/boot/config.txt’ file and reboot:

# Disable the ACT LED on the Pi Zero.
dtparam=act_led_trigger=none
dtparam=act_led_activelow=on

Note the ‘/’filesystem is made read-only by default in the Kerberos build. To temporarily fix this to force read write for the root ‘/’ filesystem, type:

mount -o remount,rw /

Now the config.txt file can be edited normally, e.g. in the editor nano, and then the Pi can be rebooted.

cd /boot
nano config.txt
reboot

Output and Data Capture from Kerberos:

top
To obtain data from the tool, we are using the ‘script’ setting in io.xml, which runs the script ‘/data/run.sh’ (a bash script). This script just writes the data receives (a JSON structure) out to disk.

#!/bin/bash

# -------------------------------------------
# This is an example script which illustrates
# how to use the Script IO device.
#

# --------------------------------------
# The first parameter is the JSON object
#
# e.g. {"regionCoordinates":[308,250,346,329],"numberOfChanges":194,"timestamp":"1486049622","microseconds":"6-161868","token":344,"pathToImage":"1486049622_6-161868_frontdoor_308-250-346-329_194_344.jpg","instanceName":"frontdoor"}

JSON=$1

# -------------------------------------------
# You can use python to parse the JSON object
# and get the required fields

echo $JSON &amp;gt;&amp;gt; /data/capture_data.json

coordinates=$(echo $JSON | python -c "import sys, json; print json.load(sys.stdin)['regionCoordinates']")
changes=$(echo $JSON | python -c "import sys, json; print json.load(sys.stdin)['numberOfChanges']")
incoming=$(echo $JSON | python -c "import sys, json; print json.load(sys.stdin)['incoming']")
outgoing=$(echo $JSON | python -c "import sys, json; print json.load(sys.stdin)['outgoing']")
time=$(echo $JSON | python -c "import sys, json; print json.load(sys.stdin)['timestamp']")
microseconds=$(echo $JSON | python -c "import sys, json; print json.load(sys.stdin)['microseconds']")
token=$(echo $JSON | python -c "import sys, json; print json.load(sys.stdin)['token']")
instancename=$(echo $JSON | python -c "import sys, json; print json.load(sys.stdin)['instanceName']")

printf "%(%m/%d/%Y %T)T\t%d\t%d\t%d\t%d\n" "$time" "$time" "$changes" "$incoming" "$outgoing" &amp;gt;&amp;gt; /data/results.txt

Note the use of the parameters to convert the Julian timestamp to a readable date/time.

When an event triggers the system (someone walking past the camera view) two actions follow, an image is saved to disk, and the script is run, with a parameter of the JSON structure. The script then processes the JSON. The script here both writes out the whole JSON structure to a the file ‘capture_data.json’ (this is included as a debug and could be omitted), and also extracts out the data elements we actually wanted and writes these to a CSV file called ‘results.txt’.

A sample of ‘capture_data.json’ look like this:

{"regionCoordinates":[413,323,617,406],"numberOfChanges":1496,"incoming":1,"outgoing":0,"name":"Dream","timestamp":"1539760397","microseconds":"6-928567","token":722,"instanceName":"Dream"}
{"regionCoordinates":[190,318,636,398],"numberOfChanges":2349,"incoming":1,"outgoing":0,"name":"Dream","timestamp":"1539760405","microseconds":"6-747074","token":814,"instanceName":"Dream"}
{"regionCoordinates":[185,315,279,436],"numberOfChanges":1793,"incoming":0,"outgoing":1,"name":"Dream","timestamp":"1539760569","microseconds":"6-674179","token":386,"instanceName":"Dream"}

A sample of ‘results.txt’ looks like this:

10/17/2018 08:17:08	1539760628	917	0	1
10/17/2018 08:17:18	1539760638	690	0	1
10/17/2018 08:18:56	1539760736	2937	0	1
10/17/2018 08:19:38	1539760778	3625	1	0
10/17/2018 08:22:05	1539760925	1066	1	0
10/17/2018 08:24:06	1539761046	2743	0	1
10/17/2018 08:24:45	1539761085	1043	1	0
10/17/2018 08:26:11	1539761171	322	0	1

Epilogue:

top
This blog has shown how the Kerberos toolkit has been used with an inexpensive Raspberry Pi for detecting motion and also directional movement across the camera view. The data captures a JSON data structure for each event triggered, and a script extracts from this the data required, which is saved off to disk for later use.

There are still issues to grapple with – for example reduce false positives, and perhaps more importantly not missing events as they occur. The settings of the configuration machinery are very sensitive. The best approach is to successively vary these settings (particularly the expositor and heuristic settings) until the right result is obtained. Kerberos has a verbose setting for event logging, and inspecting the log with this switched on reveals that the Counter conditions are very sensitive – so many more people may be walking past the camera than are being directly logged as such (e.g. motion activations may be greater than count events).

The commands below show how to access the log – it is also shown in the ‘System’ tab of the web dashboard. The command ‘tail -f’ is useful as it shows the log update in real time – helpful if the video live feed screen is being displayed alongside on-screen. Then you can see what is and isn’t being logged very easily.

cd /data/machinery/logs
tail -f log.stash

Ultimately, the Raspberry Pi may not have enough power to operate full classifier models, such as that developed by Joseph Redmon with the Darkweb YOLO tool he developed (‘You Only Look Once’) (https://pjreddie.com/darknet/yolo/). However, Kerberos itself has a cloud model that provides post-processing of images in the cloud on AWS servers, with classifier models available – perhaps something to try in a later blog.