Motion Sensing Nightvision Camera

I fancied figuring out if it was my cat or the neighbourhood cats pooping in my side alley (oh er missus!) so thought I’d find a use for my PiNoIR camera and that spare B+

I usually use RaspiMJPEG for webcam sorts of things like timelapse or somesuch, but it seems in a state of flux – the internal motion-detection system simply doesn’t work it would seem, and the external system is still using Motion and not the MMAL version, so its slow and can only cope with about VGA resolution on a B+, and it doesn’t seem to work in timelapse mode, and seems flaky at best anyway.

MotionEyeOS seemed like it was much more professional and motion-detection with timelapse worked really well, but again was limited to pretty low resolutions with weird aspect ratios. Also doesn’t seem to use motion-mmal anymore (think it did when it was motionpie).

Anyway, I figured the problem was software motion-detection using image analysis, so I decided to go the hardware route and use a PIR. So far I’ve come up with this Python script which is based on the excellent picamera module. It can do useful things like vertically flip the image (on the GPU!) which is handy as my Sainsmart camera module is a bit hard to mount the right way around!

Also the code uses interrupts so events don’t get missed and the CPU is mostly asleep. I chose to use 1080p resolution, but you could go up to to full 5MP (or 8MP on the new camera boards!) if you wanted to, forget 640×480

The only problem is that the PIR sensor doesn’t work through double-glazing, so I need to mount it in a box outside, which I was going to do eventually anyway.

#!/usr/bin/python

# import module
import picamera
import time
import RPi.GPIO as GPIO

# setup gpio mode
GPIO.setmode(GPIO.BCM)
PIR_PIN = 14
GPIO.setup(PIR_PIN,GPIO.IN)

# instantiate class
camera = picamera.PiCamera()

# vertical flip
camera.vflip = True

# set resolution
camera.resolution = '1080p'

# interrupt function
def onMotion(PIR_PIN):
    filename = time.strftime("image-%Y%m%d-%H%M%S.jpg")
    camera.capture(filename)

try:
    GPIO.add_event_detect(PIR_PIN, GPIO.RISING, callback=onMotion)

    # loop until interrupted
    while 1:
        time.sleep(100)

except KeyboardInterrupt:
    GPIO.cleanup()

You have to run the script using sudo, or from an init script like /etc/rc.local perhaps, and maybe set a directory to write the files to rather than just cwd.

Edit: just bought a v1.3 Raspberry Pi Zero and cable for this, as it should use a lot less power than the B+ and is a lot smaller to encase.

Auto-BST for LED Clock

I’ve finally got around to adding British Summer Time detection to my LED matrix clock, I’ve also reduced flash wear and connection time by using the new persistent() method from the WiFi library. I’m still saving the UTC time to the RTC module, just displaying +/- the hour. The LiFePO4 battery is still putting out over 3.1v after 2 months.

The final code is below:

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include "LedControl.h"
#include <Wire.h>
#include <RTClib.h>

// ntp server pool
IPAddress timeServerIP;

// ntp time stamp is in the first 48 bytes of the message
const int NTP_PACKET_SIZE = 48;

// buffer to hold incoming and outgoing packets
byte packetBuffer[NTP_PACKET_SIZE];

// create a udp instance
WiFiUDP udp;

// data, clock, cs, numdevices
LedControl lc = LedControl(D7,D5,D6,4);

// ds3231 constuctor
RTC_DS3231 RTC;

const int num[10][8] = {
    {0x00,0x78,0xcc,0xec,0xfc,0xdc,0xcc,0x78}, // zero
    {0x00,0xfc,0x30,0x30,0x30,0x30,0xf0,0x30}, // one
    {0x00,0xfc,0xcc,0x60,0x38,0x0c,0xcc,0x78}, // two
    {0x00,0x78,0xcc,0x0c,0x38,0x0c,0xcc,0x78}, // three
    {0x00,0x0c,0x0c,0xfe,0xcc,0x6c,0x3c,0x1c}, // four
    {0x00,0x78,0xcc,0x0c,0x0c,0xf8,0xc0,0xfc}, // five
    {0x00,0x78,0xcc,0xcc,0xf8,0xc0,0x60,0x38}, // six
    {0x00,0x60,0x60,0x30,0x18,0x0c,0xcc,0xfc}, // seven
    {0x00,0x78,0xcc,0xcc,0x78,0xcc,0xcc,0x78}, // eight
    {0x00,0x70,0x18,0x0c,0x7c,0xcc,0xcc,0x78}  // nine
};

void drawNum(int number, int display)
{
    lc.setColumn(display, 0, num[number][0]);
    lc.setColumn(display, 1, num[number][1]);
    lc.setColumn(display, 2, num[number][2]);
    lc.setColumn(display, 3, num[number][3]);
    lc.setColumn(display, 4, num[number][4]);
    lc.setColumn(display, 5, num[number][5]);
    lc.setColumn(display, 6, num[number][6]);
    lc.setColumn(display, 7, num[number][7]);
}

bool isBST(int year, int month, int day, int hour)
{
    // bst begins at 01:00 gmt on the last sunday of march
    // and ends at 01:00 gmt (02:00 bst) on the last sunday of october

    // january, february, and november are out
    if (month < 3 || month > 10) { return false; }

    // april to september are in
    if (month > 3 && month < 10) { return true; }

    // last sunday of march
    int lastMarSunday =  (31 - (5* year /4 + 4) % 7);

    // last sunday of october
    int lastOctSunday = (31 - (5 * year /4 + 1) % 7);

    // in march we are bst if its past 1am gmt on the last sunday in the month
    if (month == 3)
    {
        if (day > lastMarSunday)
        {
            return true;
        }

        if (day < lastMarSunday)
        {
            return false;
        }

        if (hour < 1)
        {
            return false;
        }

        return true;
    }

    // in october we must be before 1am gmt (2am bst) on the last sunday to be bst
    if (month == 10)
    {
        if (day < lastOctSunday)
        {
            return true;
        }

        if (day > lastOctSunday)
        {
            return false;
        }

        if (hour >= 1)
        {
            return false;
        }

        return true;
    }
}

// send an ntp request to the time server at the given address
unsigned long sendNTPpacket(IPAddress& address)
{
    // set all bytes in the buffer to 0
    memset(packetBuffer, 0, NTP_PACKET_SIZE);

    packetBuffer[0] = 0b11100011;   // li, version, mode
    packetBuffer[1] = 0;            // stratum, or type of clock
    packetBuffer[2] = 6;            // polling interval
    packetBuffer[3] = 0xEC;         // peer clock precision

    // 8 bytes of zero for root delay & root dispersion
    packetBuffer[12] = 49;
    packetBuffer[13] = 0x4E;
    packetBuffer[14] = 49;
    packetBuffer[15] = 52;

    // all ntp fields have been given values, send request
    udp.beginPacket(address, 123);
    udp.write(packetBuffer, NTP_PACKET_SIZE);
    udp.endPacket();
}

void displayDate(unsigned long unixtime)
{
    // power up led matrices
    for (int i=0; i<4; i++)
    {
        lc.shutdown(i,false); // come out of powersaving
        lc.setIntensity(i,5); // set brightness 0-15
        lc.clearDisplay(i);   // clear display
    }

    // handle british summer time
    DateTime nowntp = unixtime;
    int myhour;
    if (isBST(nowntp.year(), nowntp.month(), nowntp.day(), nowntp.hour()))
    {
        myhour = ((unixtime % 86400L) / 3600) + 1; // bst
    }
    else
    {
        myhour = (unixtime % 86400L) / 3600; // utc
    }

    // print hour to led
    if (myhour == 24)
    {
        drawNum(0,0);
        drawNum(0,1);
    }
    else
    {
        drawNum(myhour/10,0);
        drawNum(myhour%10,1);
    }

    // print minute to led
    int myminute = (unixtime % 3600) / 60;
    if (myminute < 10)
    {
        drawNum(0,2);
    }
    else
    {
        drawNum(myminute/10,2);
    }
    drawNum(myminute%10,3);
}

void gotoSleep()
{
    for (int i=0; i<4; i++)
    {
        lc.clearDisplay(i);
        lc.setIntensity(i,0);
        lc.shutdown(i,true);
    }

    // latch cs pin and pause to work around display0 being stuck high on sleep problem
    digitalWrite(D6, LOW);
    delay(2000);

    // sleep mcu forever
    ESP.deepSleep(0);
}

void setup()
{
    // setup ds3231
    Wire.begin();
    RTC.begin();

    // display rtc time on led matrices
    DateTime now = RTC.now();
    displayDate(now.unixtime());

    // connect to wifi network
    if (WiFi.SSID() != "myssid") {
        WiFi.begin("myssid", "mypassword");
        WiFi.persistent(true);
        WiFi.setAutoConnect(true);
        WiFi.setAutoReconnect(true);
    }

    // static ip, gateway, netmask
    WiFi.config(IPAddress(192, 168, 1, 2), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));

    // connect - could drain battery if never connects to wifi
    int wifitries = 0;
    while (WiFi.status() != WL_CONNECTED)
    {
        // give it a moment
        delay(1000);

        // only try 5 times before sleeping
        wifitries++;
        if (wifitries >5)
        {
            gotoSleep();
        }
    }

    udp.begin(2390);

    // get a random server from the pool
    WiFi.hostByName("europe.pool.ntp.org", timeServerIP);

    int cb = 0;
    int ntptries = 0;
    while (!cb)
    {
        // send an ntp packet to a time server
        sendNTPpacket(timeServerIP);

        // wait to see if a reply is available
        delay(3000);
        cb = udp.parsePacket();

        // only try 5 times before sleeping
        ntptries++;
        if (ntptries >5)
        {
            gotoSleep();
        }
    }

    // we've received a packet, read the data into the buffer
    udp.read(packetBuffer, NTP_PACKET_SIZE);

    // the timestamp starts at byte 40 of the received packet and is four bytes,
    // or two words, long. first, extract the two words:
    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);

    // combine the four bytes (two words) into a long integer
    // this is ntp time (seconds since jan 1 1900):
    unsigned long secsSince1900 = highWord << 16 | lowWord;

    // unix time starts on jan 1 1970. in seconds, that's 2208988800:
    const unsigned long seventyYears = 2208988800UL;

    // subtract seventy years:
    unsigned long epoch = secsSince1900 - seventyYears;

    // update ds3231 from ntp unixtime
    RTC.adjust(DateTime(epoch));

    // display ntp time on led matrices
    displayDate(epoch);

    // ntp has a dot rtc does not
    lc.setLed(3, 7, 0, true);

    // wait five seconds before shutting down
    delay(5000);
    gotoSleep();
}

void loop()
{
}

NodeMCU Motor Shield Review

I just received my NodeMCU Motor Shield to go with my NodeMCU v1 ESP12e development board and robot chassis.

So the board has a bunch of screw terminals on it – A+, A-, B+ and B- to connect two motors, and then VM/GND (up to 9v for the microcontroller) and VIN/GND (up to 36v for the motors). However the mostly undocumented feature is that if you jumper the VIN/VM pins near to the power switch, you can supply up to 9v to VIN/GND and it will feed that into the motors and also into the NodeMCU’s 3.3v regulator. I’ve used this configuration with two 18650 Li-Ion batteries that provide about 8v when fully charged (nominal 3.7v each).

Alternatively, but with some caution you can power the motors and the ESP12e via the NodeMCU’s micro USB port.

I had a poke around with a multimeter and found that if you put 8v into the VIN screw terminal and jumper the VIN/VM pins you get:

  • 8v from the VM screw terminal;
  • 8v from the VIN pin on the nodemcu itself;
  • 8v from the two VIN pins on the motor shield;
  • 3.3v from the three 3.3v pins on the modemcu;
  • 3.3v from the 3.3v pin on the motor shield;

The motors seem to pull whatever they need – mine are 6v motors and they’re pulling about 6.25v when full on (1023 PWM) from the A+/B+ screw terminals.

If you power from USB the VIN pins run at about 4.4v and the motors run slower. The power button also does nothing. I think I’ll just use USB for programming rather than power, but you could use a USB power bank I guess, but at least it means you don’t have to unplug the NodeMCU from the motor shield to reprogram it, although it is a bit tight as the USB connector ends up only a few millimeters from the screw terminal blocks.

I initially wrote my own Arduino sketch which is a webserver that runs on the ESP12e and contains forward/backward/right/left/stop buttons.

// include libraries
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

// configure server
ESP8266WebServer server(80);

const char *form = "<center><form action='/'>"
"<button name='dir' type='submit' value='4'>Forward</button><br>"
"<button name='dir' type='submit' value='1'>Left</button> "
"<button name='dir' type='submit' value='2'>Right</button><br>"
"<button name='dir' type='submit' value='3'>Reverse</button><p>"
"<button name='dir' type='submit' value='5'>Stop</button>"
"</form></center>";

void stop(void)
{
    analogWrite(5, 0);
    analogWrite(4, 0);
}

void forward(void)
{
    analogWrite(5, 1023);
    analogWrite(4, 1023);
    digitalWrite(0, HIGH);
    digitalWrite(2, HIGH);
}

void backward(void)
{
    analogWrite(5, 1023);
    analogWrite(4, 1023);
    digitalWrite(0, LOW);
    digitalWrite(2, LOW);
}

void left(void)
{
    analogWrite(5, 1023);
    analogWrite(4, 1023);
    digitalWrite(0, LOW);
    digitalWrite(2, HIGH);
}

void right(void)
{
    analogWrite(5, 1023);
    analogWrite(4, 1023);
    digitalWrite(0, HIGH);
    digitalWrite(2, LOW);
}

void handle_form()
{
    // only move if we submitted the form
    if (server.arg("dir"))
    {
        // get the value of request argument "dir"
        int direction = server.arg("dir").toInt();

        // chose direction
        switch (direction)
        {
            case 1:
                left();
                break;
            case 2:
                right();
                break;
            case 3:
                backward();
                break;
            case 4:
                forward();
                break;
            case 5:
                stop();
                break;
        }

        // move for 300ms, gives chip time to update wifi also
        delay(300);
    }
    
    // in all cases send the response
    server.send(200, "text/html", form);
}

void setup()
{
    // connect to wifi network
    WiFi.begin("essid", "passphrase");

    // static ip, gateway, netmask
    WiFi.config(IPAddress(192,168,1,2), IPAddress(192,168,1,1), IPAddress(255,255,255,0));

    // connect
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(200);
    }
  
    // set up the callback for http server
    server.on("/", handle_form);

    // start the webserver
    server.begin();

    pinMode(5, OUTPUT); // 1,2EN aka D1 pwm left
    pinMode(4, OUTPUT); // 3,4EN aka D2 pwm right
    pinMode(0, OUTPUT); // 1A,2A aka D3
    pinMode(2, OUTPUT); // 3A,4A aka D4
}

void loop()
{
    // check for client connections
    server.handleClient();
}

I also tried the Blynk joystick code with my Android phone. I’m still not happy with either solution though, as I need a way to move in more than one direction at once to give a smooth turning action – for example I can rotate left/right, but can’t drive forward at the same time, so I need to play with timings a bit more. Also I found that PWM at 512 seemed quite slow, so sticking with ultra-fast 1023 for full speed motors!

There’s a bunch of pins broken out on the motor shield – GPIO 0-8, UART, ADC, SPI etc. so you could add a servo or LED’s. Here is another useful post, and here is the pretty useless documentation.

Simply Dashing!

I’ve finally gotten around to making a web frontend for my Raspberry Pi & Arduino wireless sensor network.

I chose Shopify’s Dashing dashboard, as its opensource and can be installed locally and not via some cloud crap.

Essentially you have a HTML template into which you feed JSON data. I got a bit tied up in the JSON formatting so it took longer than it should have to get working (about 3 hours) but once I realised that most of the job placeholders were a single line of JSON, and not a whole entity, it was quite straightforward. Here’s 90% of my entire dashboard code:

SCHEDULER.every '30m', :first_in => 0 do
    require 'sqlite3'
    db = SQLite3::Database.open "/var/tmp/weather.db"
    
    study = db.execute "SELECT reading FROM weather WHERE node=2 ORDER BY date DESC LIMIT 3"
    send_event('study_temp', { current: study[0][0].round(2), last: study[2][0].round(2) })

    shed = db.execute "SELECT reading FROM weather WHERE node=1 AND type=2 ORDER BY date DESC LIMIT 3"
    send_event('shed_temp', { current: shed[0][0].round(2), last: shed[2][0].round(2) })

    outside = db.execute "SELECT reading FROM weather WHERE node=1 AND type=1 ORDER BY date DESC LIMIT 3"
    send_event('outside_temp', { current: outside[0][0].round(2), last: outside[2][0].round(2) })

    lounge = db.execute "SELECT reading FROM weather WHERE node=3 ORDER BY date DESC LIMIT 3"
    send_event('lounge_temp', { current: lounge[0][0].round(2), last: lounge[2][0].round(2) })
     
    light = db.execute "SELECT reading FROM weather WHERE node=4 ORDER BY date DESC LIMIT 1"    
    send_event('lightsensor', { value: light[0][0].round(1) })

    bedroom = db.execute "SELECT reading FROM weather WHERE node=0 ORDER BY date DESC LIMIT 3"
    send_event('bedroom_temp', { current: bedroom[0][0].round(2), last: bedroom[2][0].round(2) })

    db.close
end

Here’s a screenshot of the finished dashboard, including an additional widget that shows the RPi-1B’s very stretched RAM:

It installed a lot easier on Debian Sid than Raspbian Wheezy, but I hear Raspbian Jessie is easier due to updated bundler/exec_json/nodejs packages – every time I use Ruby I seem to go through dependency hell, Ruby seems to have a lot of issues with backwards/forwards compatibility with Gems – why things like bundler/rvm are necessary I guess. Makes Java look portable.

Obstacle Bot

I’ve finished my obstacle avoiding robot project.

Basically its an ATmega328p microcontroller that uses an HC-SR04 ultrasonic sensor to detect obstacles.

The head moves using an SG-90 servo and a L293D dual H-bridge drives the two motors, using a library I’ve written and put on GitHub.

The whole robot is powered by two 3.7v 18650 Li-Ion batteries with a DC/DC converter that drops it to 6v for the motors and a diode (and smoothing cap) to drop it further to 5v for the microcontroller and servo.

My next robot will be remote controlled using Bluetooth or Wifi. I may use an old Android phone for camera/GPS, and maybe some LED’s or laser pointers.

The code is below:

// include libraries
#include <Servo.h>
#include <DCMotor.h>

// setup servo
#define SERVORIGHT   50
#define SERVOCENTRE 100
#define SERVOLEFT   150
#define SERVOPIN      4
Servo servo;

// setup sensor
#define TRIGPIN 2
#define ECHOPIN 3

// setup motors - en1, m1-i1, m1-i2, m1-speed, en2, m2-i1, m2-i2, m2-speed
DCMotor motors(6,7,8,255,9,10,11,255);

int ping()
{
    // pause for 50ms between scans
    delay(50);

    // send ping
    digitalWrite(TRIGPIN, LOW);
    delayMicroseconds(2);
    digitalWrite(TRIGPIN, HIGH);
    delayMicroseconds(10);
    digitalWrite(TRIGPIN, LOW);

    // read echo
    long duration = pulseIn(ECHOPIN, HIGH);

    // convert distance to cm
    unsigned int centimetres = int(duration / 2 / 29.1);

    return centimetres;
}

char scan()
{
    // ping times in microseconds
    unsigned int left_scan, centre_scan, right_scan;
    char choice;

    // scan left
    servo.write(SERVOLEFT);
    delay(300);
    left_scan = ping();

    // scan right
    servo.write(SERVORIGHT);
    delay(600);
    right_scan = ping();

    // scan straight ahead
    servo.write(SERVOCENTRE);
    delay(300);
    centre_scan = ping();

    if (left_scan>right_scan && left_scan>centre_scan)
    {
        choice = 'l';
    }
    else if (right_scan>left_scan && right_scan>centre_scan)
    {
        choice = 'r';
    }
    else
    {
        choice = 'c';
    }

    return choice;
}

void setup()
{
    // set the servo data pin
    servo.attach(SERVOPIN);

    // set the trig pin to output (send sound waves)
    pinMode(TRIGPIN, OUTPUT);

    // set the echo pin to input (receive sound waves)
    pinMode(ECHOPIN, INPUT);
}

void loop()
{
    // get distance from obstacle straight ahead
    unsigned int distance = ping();

    if (distance < 50 && distance > 0)
    {
        if (distance < 10)
        {
            // turn around
            motors.backward();
            delay(300);
            motors.left();
            delay(500);
        }
        else
        {
            // stop both motors
            motors.stop();

            // scan for obstacles
            char turn_direction = scan();

            // turn left/right or ignore and go straight
            if (turn_direction == 'l')
            {
                motors.left();
                delay(200);
            }
            else if (turn_direction == 'r')
            {
                motors.right();
                delay(200);
            }
        }
    }
    else
    {
        // no obstacle, keep going forward
        motors.forward();
    }
}