I had the chance to play with some new components – namely the Adafruit Monochrome 128×32 OLED display and the Adafruit UP501 66 channel GPS receiver. As I was pondering ideas of what to build, I thought that it would be neat to be alerted when approaching a red light camera. In my local area (Southern Ontario), there are currently about a hundred or so of these cameras around the GTA. However, it appears that new legislation may see this number grow much larger. This is more of a proof of concept project to me than it is useful as, a) I don’t intend on trying to run any red lights, and b) there are only about 1 or 2 of them within the area. However, it was fun to build and tweak to make it useful. Read on…
The general idea is that the GPS tracks the coordinates while driving, the Arduino sketch has an array of red light camera location coordinates that it compares against the current reading. On a regular basis, it checks the current position of the car against the array of red light camera locations. With each check, it finds the closest red light camera location. If that location is less than a pre-determined buffer distance (e.g. 3 km away), it will begin reporting on it via the OLED as well as the Status LED.
The Status LED flashes with 4 modes:
- green – indicates that the system is functional and GPS is returning results.
- blue – early warning of approaching red light camera location less than 600 metres away.
- yellow -warning of red light camera less than 200 metres away.
- red – red light camera intersection less than 50 metres away.
The Parts:
- Adafruit Monochrome 128×32 OLED display
- Adafruit UP501 66 channel GPS receiver
- Adafruit Perma-Proto Quarter Sized Breadboard
- Atmega 328P with UNO Bootloader
- 10mm Diffused RGB LED
- Other bits (5v/3.3v regulator, female headers, resistors etc)
The OLED will display the distance to the Red Light Camera in Km, Direction that the vehicle is travelling, Speed, # of Satellites, Latitude and Longitude of the vehicle.
The Build:
The main unit consists of an Atmega 328P mounted to an Adafruit Perma-Proto Quarter sized breadboard. The board has both 5v and 3.3v rails running down either side to provide power for various components. The rest is a spaghetti mess of wires for all the components. I also decided to make this unit (somewhat) modular. The main board has been fitted with female header pins to allow all components to be plugged/unplugged. So, if I want to re-use the controller for another project, I can just unplug all the bits and I have a dual power supply-ready Arduino. Given that I would likely want to re-use the GPS on other projects, I hacked in USB ports on the main unit as well as a mini-USB connector on the GPS module.
As noted above, the GPS module is modular. With the mini-USB connector, I can move it over to other projects in a breeze. There are only 4 connectors that need to be brought to the Arduino to use this unit, so USB cables do the trick. Although there are more pins needed, the wiring can be done at the GPS module end itself (e.g resistor between two pins). I also wanted a minimal USB cable, so I picked up one of those self-retracting flat cable USB dongle thingamabobs and just took out the self-retracting bit. The cable is very flexible as well.
The Verdict:
Adafruit UP501 GPS Module: The UP501 is small, which makes it great for fitting into small packages. Another nice feature is that you only require 4 wires between the controller and the GPS, which makes it a perfect candidate for using readily available USB cables. I managed to get this one before the newer Adafruit Ultimate GPS that comes with a backup battery. As a result, my unit takes up to 45 seconds to as it would cold start every time it is powered up. This is negligible considering I don’t need the GPS functioning the moment I start my car. However, for other projects, this could be an issue. Once the unit acquires its first fix, it provides fast accurate updates. It is also great at acquiring signals indoors.. When testing, I would put it on my window sill to get a fix, then once fixed, I could move the unit to my desk to continue testing.
Adafruit 128×32 OLED Module: I won’t go into detail here as I have already done a review on this unit here. However, I will say that this is a nice, crisp, small display that works very well with small projects.
Looking for an Android remote control for your robot? Check out BTBotControl - I built this to remotely control IPCamBot and view a live feed of a mounted IP Camera.
The Code:
(download)// Select which PWM-capable pins are to be used. int redPin = 9; int greenPin = 3; int bluePin = 10; #include <SoftwareSerial.h> //SoftwareSerial mySerial(rxPin, txPin) //SoftwareSerial nss(2, 3); //yellow wire = RX Blue wire = TX SoftwareSerial nss(0, 2); //yellow wire = RX Blue wire = TX #include <avr/pgmspace.h> #include <SSD1306.h> //for OLED #include <SPI.h> //for OLED #include <string.h> #include <TinyGPS.h> #define OLED_RESET 7 #define OLED_DC 8 #define OLED_CS 6 // SPI slave-select #define OLED_MOSI 11 // hardware SPI MOSI #define OLED_CLK 13 // hardware SPI clock // Connect the GPS Power pin to 3.3V // Connect the GPS Ground pin to ground // Connect the GPS VBAT pin to 3.3V if no battery is used // Connect the GPS TX (transmit) pin to Digital 2 // Connect the GPS RX (receive) pin to Digital 3 // For 3.3V only modules such as the UP501, connect a 10K // resistor between digital 3 and GPS RX and a 10K resistor // from GPS RX to ground. TinyGPS gps; //NewSoftSerial mySerial(rxPin, txPin); //OLED SSD1306 oled(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS); const int GPSActive = 4; // Indicates GPS is getting data const int statusLED = 5; //System hearbeat LED int updateFreq = 1000; //frequency to update GPS polls. Good not to go below 1000 ms. float flat, flon; //lat and long variables used in calculations unsigned long age; //GPS reading age variable float spd; // GPS Speed float minDist = 100000; //preset the minDist to some very high value (in meters) // //int md[] = {600, 180, 35}; //int j = 0; // const float ToRad = PI / 180.0; // for HaverSine function const float R = 6371; // radius earth in Km - for HaverSine function int mappedDist = 0; // mapped variable converted for placement of icons on OLED int spdProRate = 0; // pro rated speed value movement of road graphic on OLED - to mimic speed changes int i = 128; // used for the looping through animations of the OLED int state = LOW; // The input state of the status LED int gpsStatCounter = 0; // setup variables for Tiny GPS void gpsdump(TinyGPS &gps); bool feedgps(); char cVal[32]; int sats; int hdop; String cardinal; //******************************************************************************************************** // use for testing boolean debug = true; //print debugging statements to the serial port. //******************************************************************************************************** // different commands to set the update rate from once a second (1 Hz) to 10 times a second (10Hz) #define PMTK_SET_NMEA_UPDATE_1HZ "$PMTK220,1000*1F" #define PMTK_SET_NMEA_UPDATE_5HZ "$PMTK220,200*2C" #define PMTK_SET_NMEA_UPDATE_10HZ "$PMTK220,100*2F" // turn on only the second sentence (GPRMC) #define PMTK_SET_NMEA_OUTPUT_RMCONLY "$PMTK314,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29" // turn on ALL THE DATA //#define PMTK_SET_NMEA_OUTPUT_ALLDATA "$PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0*28" // to generate your own sentences, check out the MTK command datasheet and use a checksum calculator // such as the awesome http://www.hhhh.org/wiml/proj/nmeaxor.html //Define list of waypoints to search //format WayPt[Lat][Long] //multidimensional array [howmany dn] [howmany across] const float wayPt[175][2] PROGMEM = { {43.25981,-79.88897}, PUT YOUR RED LIGHT CAMERA COORDINATES HERE }; void setup() { Serial.begin(115200); pinMode(statusLED, OUTPUT); //setup the RGB LED pinMode(redPin, OUTPUT); pinMode(greenPin, OUTPUT); pinMode(bluePin, OUTPUT); //set the RGB status LED to off which is HIGH (255) reason: Common-Annode. analogWrite(redPin, 255); analogWrite(greenPin, 255); analogWrite(bluePin, 255); //Serial.println("Adafruit MTK3329 NMEA test!"); nss.begin(9600); //needed to change this to 9600 baud for the UP501 GPS module //********* OLED SETUP STUFF ******************************************************************/ SPI.begin (); //for OLED // OLED: By default, we'll generate the high voltage from the 3.3v line internally! (neat!) oled.ssd1306_init(SSD1306_SWITCHCAPVCC); //********* GPS SETUP STUFF ******************************************************************/ // 9600 NMEA is the default baud rate // uncomment this line to turn on only the "minimum recommended" data for high update rates! nss.println(PMTK_SET_NMEA_OUTPUT_RMCONLY); // uncomment this line to turn on all the available data - for 9600 baud you'll want 1 Hz rate //mySerial.println(PMTK_SET_NMEA_OUTPUT_ALLDATA); // Set the update rate // 1 Hz update rate nss.println(PMTK_SET_NMEA_UPDATE_1HZ); // 5 Hz update rate- for 9600 baud you'll have to set the output to RMC only (see above) //nss.println(PMTK_SET_NMEA_UPDATE_5HZ); // 10 Hz update rate - for 9600 baud you'll have to set the output to RMC only (see above) //nss.println(PMTK_SET_NMEA_UPDATE_10HZ); //********* END GPS SETUP STUFF ******************************************************************/ //setupDisplay(); } void loop() { bool newdata = false; unsigned long start = millis(); blinkLED(); // heartbeat to ensure system is working flat=0; flon=0; spd=0; sats=0; hdop=0; cardinal= ""; // Every xxxx milliseconds we print an update while (millis() - start < updateFreq) { if (feedgps()) newdata = true; } heartbeat(newdata); //shows external heartbeat signal through GPS status LED if (newdata) { feedgps(); // If we don't feed the gps during this long routine, we may drop characters and get checksum errors gps.f_get_position(&flat, &flon, &age); if (debug) { Serial.print("Lat/Long(float): "); Serial.print(flat, 5); Serial.print(", "); Serial.print(flon, 5); Serial.print(" Fix age: "); Serial.print(age); Serial.print("ms."); Serial.print(" (kmph): "); Serial.print(gps.f_speed_kmph(),4); Serial.println(); //Serial.print(" Sats: "); Serial.print(gps.satellites()); Serial.println(); feedgps(); // If we don't feed the gps during this long routine, we may drop characters and get checksum errors Serial.print(" Sats: "); print_int(gps.satellites(), TinyGPS::GPS_INVALID_SATELLITES, 5); Serial.print(" HDOP: "); print_int(gps.hdop(), TinyGPS::GPS_INVALID_HDOP, 5); Serial.print(" Course: "); print_float(gps.f_course(), TinyGPS::GPS_INVALID_F_ANGLE, 7, 2); Serial.print(" Cardinal: "); print_str(gps.f_course() == TinyGPS::GPS_INVALID_F_ANGLE ? "*** " : TinyGPS::cardinal(gps.f_course()), 6); } //compare the latest reading with the array of red light camera points and find which is closest. minDist = getClosestPt(flat, flon); //minDist = md[j]; //if (j == 2){j = 0;}else{j++;} //indicate that GPS is active and receiving data flash(1); //flashes the LED on the project box showing GPS signal if (debug) { Serial.print("Delta = ");Serial.print(minDist, 2);Serial.println(" Meters "); } //Process alerts via various means based on pre-defined distance thresholds. if (minDist < 50) //Warning Alert < 50 meters - in range if red light camera { //red changeWarning(4); } else if (minDist < 200) //Approaching Alert < 200 meters { //yellow changeWarning(3); } else if (minDist <= 600) //Initial Alert - 600 meters { //blue - early warning changeWarning(2); } else { //just show status that GPS is working. changeWarning(1); } //for testing //minDist = 40; //updateDisplay(minDist, 65.00); //end testing //updateDisplay(minDist, gps.f_speed_kmph()); } else { if (debug) {Serial.println("Polling GPS... waiting for fix...");} } oled.clear(); //Distance to next red light oled.drawstring(0, 0, "Dist:"); if (minDist == 100000) { oled.drawstring(30, 0, "?"); } else { ftoa(cVal, minDist/1000, 2); oled.drawstring(30, 0, cVal); } oled.drawstring(55, 0, "km"); //Cardinal Direction of vehicle //strcpy ( cVal, TinyGPS::cardinal(gps.f_course()) ); strcpy ( cVal, gps.f_course() == TinyGPS::GPS_INVALID_F_ANGLE ? "?" : TinyGPS::cardinal(gps.f_course())); oled.drawstring(75, 0, "Dir:"); oled.drawstring(100, 0, cVal); oled.drawline(0, 12, 128, 12, WHITE); //speed spd = gps.f_speed_kmph(); if (spd < 0) {spd=0;} ftoa(cVal, spd, 0); oled.drawstring(0, 2, "Spd:"); oled.drawstring(25, 2, cVal); //satellites itoa(gps.satellites(), cVal, 0); oled.drawstring(65, 2, "Sats:"); oled.drawstring(90, 2, cVal); //latitude ftoa(cVal, flat, 2); oled.drawstring(0, 3, "Lat:"); oled.drawstring(25, 3, cVal); //longitude ftoa(cVal, flon, 2); oled.drawstring(65, 3, "Lng:"); oled.drawstring(90, 3, cVal); //Display all the results oled.display(); //reset minDist for the next round minDist = 100000; //meters } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ char *ftoa(char *a, double f, int precision) { long p[] = {0,10,100,1000,10000,100000,1000000,10000000,100000000}; char *ret = a; long heiltal = (long)f; itoa(heiltal, a, 10); while (*a != '\0') a++; *a++ = '.'; long desimal = abs((long)((f - heiltal) * p[precision])); itoa(desimal, a, 10); return ret; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void setupDisplay() { // Draw animated graphics to OLED - we will use the GPS speed to mimic changes in speed of the lanes. oled.clear(); // clears the screen and buffer //oled.drawbitmap(0, 2, Road, 128, 32, 1); //road - dash lines - draw the animating road //oled.drawbitmap(95, 4, JukeIcon, 34, 40, 1); //keep drawing the car icon in place oled.fillrect(0, 0, 128, 2, WHITE); //keep drawing the top continuous line oled.fillrect(0, 30, 128, 2, WHITE); //keep drawing the bottom continous line oled.display(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void updateDisplay(int minDist, float spd) { if (i==0){i=128;} //we have 128 pixels to loop through for animation of the road dashed lines. reset when it gets to 0. // Draw animated graphics to OLED - we will use the GPS speed to mimic changes in speed of the lanes. //oled.drawbitmap(i, 16, Road, 128, 32, 1); //road - dash lines - draw the animating road //oled.drawbitmap(i-128, 16, Road, 128, 32, 1); //road - dash lines - draw the road ahead to keep contents on the screen //map values based on dist from camera, min dist on ground of 20m, max dist on ground of 500m, min dist in px to car, max dist in px from car mappedDist = map(minDist, 20, 500, 40, 110); //oled.drawbitmap(mappedDist, 10, RedLightCam, 10, 15, 1); //move the red light camera as it approaches //oled.drawbitmap(95, 12, JukeIcon, 34, 40, 1); //keep drawing the car icon in place oled.fillrect(0, 0, 128, 4, WHITE); //keep drawing the top continuous line oled.fillrect(0, 61, 128, 4, WHITE); //keep drawing the bottom continous line oled.display(); oled.clear(); // clears the screen and buffer // convert speed to an int so it can be used in the map funciton. 1.25 km/h = 125 //Serial.println(int(spd*10)); spdProRate = map(int(spd*10), 0, 900, 0, 30); //Serial.println(spdProRate); //i=i-10; i = i - spdProRate; if (i < 0) {i=0;} //mappedDist = 0; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ float getClosestPt(float flat, float flon) { float d; float d2; float md = 10000; //int minCoordIndex; //int coordsProcessed; if (flat != 0 && flon != 0) { for (unsigned int x=0; x<175; x++){ //In array lat is 2nd col, long is 1st col e.g. lat:43.xxxx,long:-79.xxxxx - need to get right ones for calc //flat2 = pgm_read_float_near(&wayPt[x][0]); //flon2 = pgm_read_float_near(&wayPt[x][1]); //if (debug) //{ // Serial.print("Flat2: ");Serial.print(flat2, 5); // Serial.print(" Flon2: ");Serial.println(flon2, 5); //} //d = HaverSine(flat, flon, pgm_read_float_near(&wayPt[x][0]), pgm_read_float_near(&wayPt[x][1])); d = gps.distance_between(flat, flon, pgm_read_float_near(&wayPt[x][0]), pgm_read_float_near(&wayPt[x][1]))/1000 ; if (debug) { //Serial.print("DistanceCalcualted d1: ");Serial.print(d, 4); Serial.print(" d2:"); Serial.print(d2, 4); Serial.println(""); //Serial.print("DistanceCalcualted d1: ");Serial.print(d, 4); Serial.println(""); } if (d < md) { md = d; //minCoordIndex = x; } //coordsProcessed++; } //convert to meters md = md * 1000; //Serial.print(" Processed:");Serial.println(coordsProcessed); /* Serial.println(""); Serial.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); Serial.print(" Index:");Serial.println(minCoordIndex); Serial.print(" Processed:");Serial.println(coordsProcessed); Serial.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); Serial.println(""); */ } //coordsProcessed = 0; return md; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ float HaverSine(float lat1, float lon1, float lat2, float lon2) { //float ToRad = PI / 180.0; //float R = 6371; // radius earth in Km float dLat = (lat2-lat1) * ToRad; float dLon = (lon2-lon1) * ToRad; float a = sin(dLat/2) * sin(dLat/2) + cos(lat1 * ToRad) * cos(lat2 * ToRad) * sin(dLon/2) * sin(dLon/2); float c = 2 * atan2(sqrt(a), sqrt(1-a)); float d = R * c; return d; //distance returned in Kilometers } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ bool feedgps() { while (nss.available()) { if (gps.encode(nss.read())) return true; } return false; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ static void print_float(float val, float invalid, int len, int prec) { char sz[32]; if (val == invalid) { strcpy(sz, "*******"); sz[len] = 0; if (len > 0) sz[len-1] = ' '; for (int i=7; i<len; ++i) sz[i] = ' '; Serial.print(sz); } else { Serial.print(val, prec); int vi = abs((int)val); int flen = prec + (val < 0.0 ? 2 : 1); flen += vi >= 1000 ? 4 : vi >= 100 ? 3 : vi >= 10 ? 2 : 1; for (int i=flen; i<len; ++i) Serial.print(" "); } feedgps(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* void beep(int howmany, int itone) { if (itone == 0){itone=2200;} for (int i=0; i < howmany; i++){ tone(piezo, itone, 10); delay(100); } } */ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ static void print_str(const char *str, int len) { int slen = strlen(str); for (int i=0; i<len; ++i) Serial.print(i<slen ? str[i] : ' '); feedgps(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void flash(int howmany) { for (int i=0; i < howmany; i++){ digitalWrite(GPSActive, HIGH); delay(100); digitalWrite(GPSActive, LOW); delay(100); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void heartbeat(boolean nd){ //show if there is no data - hearbeat of system - shows it is working. Fast blink. if (nd == false){ if (gpsStatCounter == 2) { analogWrite(redPin, 127); analogWrite(greenPin, 255); analogWrite(bluePin, 0); delay(50); analogWrite(redPin, 255); analogWrite(greenPin, 255); analogWrite(bluePin, 255); //delay(50); gpsStatCounter = 0; } else { gpsStatCounter++; } } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void changeWarning(int stateLevel) { switch (stateLevel) { case 1: //green LED - show for status that GPS is getting data analogWrite(greenPin, 155); delay(200); break; case 2: //early warning. analogWrite(bluePin, 155); delay(200); break; case 3: //yellow analogWrite(redPin, 0); analogWrite(greenPin, 190); delay(200); break; case 4: //in and just before red light camera analogWrite(redPin, 155); delay(200); break; default: //all off analogWrite(greenPin, 255); analogWrite(bluePin, 255); analogWrite(redPin, 255); break; } //turn the LEDs back off analogWrite(greenPin, 255); analogWrite(bluePin, 255); analogWrite(redPin, 255); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ static void print_int(unsigned long val, unsigned long invalid, int len) { char sz[32]; if (val == invalid) strcpy(sz, "*******"); else sprintf(sz, "%ld", val); sz[len] = 0; for (int i=strlen(sz); i<len; ++i) sz[i] = ' '; if (len > 0) sz[len-1] = ' '; Serial.print(sz); feedgps(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void blinkLED() { state = !state; digitalWrite(statusLED, state); }