/*
Speed Nanny
===========
GPS speedometer and overspeed alert
Feb 22 2022
Uses GPS output to determine speed.
Displays speed on a TM1637 4-digit, 7-segment LED display.
Raises audible alert when speed is just above
current speed limit
A piezo buzzer is used for the audible alarm.
Speed limit is intially set to 40.
When in the over speed alert, user may:
. remain in the alert span -- the alert will keep sounding
. accelerate beyond the alert span -- alert will be silenced
and speed limit incremented to next 10kph, e.g., 40->50, 50-60
. decelerate below the speed limit -- alert will be silenced
and speed limit remains unchanged
Speed limit will not go below 30 and not above 110.
Program does not use a GPS library such as TinyGPS++
so as to minimise code size.
Only three parameters are needed from the GPS output:
UTC time, data is valid flag, and speed in kph.
These parameters come from fields in two NMEA sentences
outputted from the GPS -- GPRMC (UTC time and valid flag)
and GPVTG (speed).
The Serial facility of Arduino is used
to receive the GPS output (the GPS TX is connected
to the Arduino RX; the GPS RX is not connected)
It has been difficult to come up with a scheme that reliably
receives the RMC and VGT sentences , i.e.,
complete and correct at each GPS output cycle (1Hz).
The scheme succeeds at that but other sentences are missed --
the GSV sentences following the first GSV sentence.
This can be accepted as we don't need any data
from the GSV sentences.
As the GPS is connected to the hardware serial port, the
GPS TX to Arduino RX connection must be disconnected
when uploading a sketch. The raw GPS output can be
viewed in the IDE by connecting the GPS TX to the TX
of an Arduino board with a USB port (not a Pro Mini)
and grounding the Arduino's RST pin.
No commands are sent to the GPS module which operates
at its default settings, hence the GPS RX pin is not connected.
The speed can be simulated rather than taking it from the GPS.
This is done by commenting out the statement "SimulateSpeed()",
recompiling and disconnecting the GPS. The procedure SimulateSpeed() can be edited
to give a desired speed profile. It would be possible to
modify the sketch to allow selection of simulation by the user
while the program is running, instead of recompiling.
*/
#include <TM1637Display.h>
#define pinBZR 10
#define pinCLK 3
#define pinDIO 4
#define brightnessNormal 4
#define GPStimeout 1000
TM1637Display display(pinCLK, pinDIO);
boolean newSentenceRead, overSpeed, haveFix, haveTime,
validFlag, haveRMC, haveVTG, haveSpeed, noGPSdata;
int speedLimit, speedLimitInitial, alertSpan, alertLevel, sentenceID, code,
speedForDisplay, hourLocalasInt, minuteLocalasInt, timeLocalasInt;
unsigned long noGPSdataMark;
float speed;
String currentSentence, currentVTGsentence, currentRMCsentence, speedAsString,
currentSentenceID, validField, timeUTCField, speedField;
const uint8_t
dataAllDots[] = {0x80, 0x80, 0x80, 0x80},
dataDot0[] = {0x80, 0x00, 0x00, 0x00},
dataDot1[] = {0x00, 0x80, 0x00, 0x00},
dataDot2[] = {0x00, 0x00, 0x80, 0x00},
dataDot3[] = {0x00, 0x00, 0x00, 0x80}
;
const int RunningDotDelay = 100;
//****************************SETUP*************************************
void setup() {
//initialise serial port
Serial.begin(9600);
Serial.setTimeout(10);
// while(true){
//// if (Serial.available()) {
//// Serial.write(Serial.read());
//// }
// String s = (Serial.readStringUntil('\n'));
// if (s != "") {
// s = s + '\n';
// Serial.print(s);
// }
// }
//initialise serial hardware
pinMode(pinBZR, OUTPUT);
display.setBrightness(brightnessNormal);
//initialise variables
speedLimitInitial = 40;
speedLimit = speedLimitInitial;
alertSpan = 3;
float number = 123.45;
//make intro message & test hardware
Serial.println("Speed Nanny, Mar 077" ); //to monitor
Serial.println(number); //to monitor
Serial.println(123.49); //to monitor
display.showNumberDecEx(1234, 0b00100000, false, 4, 0);
display.showNumberDec(8888);
// alertBuzzer();
delay(500);
display.clear();
digitalWrite(pinBZR, LOW);
}
//*****************************LOOP********************************
void loop() {
if (Serial.available() > 0) { //any data from GPS?
noGPSdata = false;
ReadNextSentence(); //yes--get next sentence from GPS
if (haveRMC && haveVTG) //do we have both RMC & VTG?
ExtractDataAndProcess(); //yes--process data and control display & buzzer
}
else ManageSimMode(); //no return from simulation mode
//simulation mode can only be ended by board reset
}
//repeat the above indefinitely
//******************************
void ManageSimMode() {
//if we had GPS data last loop, then start timeout timer
if (!noGPSdata) { //had data last loop?
noGPSdata = true; //yes, so set flag
noGPSdataMark = (millis()); //& start timer
if (noGPSdataMark < 0) noGPSdataMark = -noGPSdataMark;
}
else {
//no GPS data last loop, so continue timer
//& test for timeout
if ((millis() - noGPSdataMark) > GPStimeout) //timeout?
SimulateSpeed(); //yes, so simulate till board reset
}
}
//******************************
void ReadNextSentence() {
String sentence;
//only arrive here if there is serial data available
//read next sentence
currentSentence = (Serial.readStringUntil('\n')); //discards the <NL>,
//retains the <CR>
currentSentence = currentSentence + '\n'; //restore the <NL>
Serial.print(currentSentence);
//check if it is RMC or VTG
//if so, store it
if (currentSentence.startsWith("$GPRMC")) {
currentRMCsentence = currentSentence;
haveRMC = true;
}
else if (currentSentence.startsWith("$GPVTG")) {
currentVTGsentence = currentSentence;
haveVTG = true;
}
}
//******************************
void ClearData() {
//initialise variables
//call before reading each GPS output cycle
validField = "null"; timeUTCField = "null"; speedField = "null";
validFlag = false;
speed = 0;
// timeUTCField = "0000";
// currentVTGsentence = "n/a"; currentRMCsentence = "n/a";
}
//******************************
void ExtractDataAndProcess() {
GetValidFlag();
Get_timeUTC();
if (haveTime) {
GetSpeed();
ManageDisplayContent();
}
if (haveSpeed) {
determineAlertLevel();
actionAlert();
}
haveRMC = false;
haveVTG = false;
timeUTCField = "0000";
currentVTGsentence = "n/a"; currentRMCsentence = "n/a";
// Serial.print("currentRMCsentence:");
// Serial.println(currentRMCsentence);
// Serial.print("currentVTGsentence:");
// Serial.println(currentVTGsentence);
ClearData();
}
//******************************
void GetSpeed() {
//the valid flag field is the 8th in the VTG sentence;
//need to determine positions of 7th & 8th commas
int posAlpha, posBeta = -1;
String speedWithoutDecimalPoint;
for (int i = 1; i <= 8; i++) {
posAlpha = posBeta;
posBeta = currentVTGsentence.indexOf(',', posAlpha + 1);
}
//get speed as a string, will be in kph with three decimal places
speedField = currentVTGsentence.substring(posAlpha + 1, posBeta);
if (speedField != "")
{
haveSpeed = true;
speedAsString = speedField;
//convert from string to numeric
speed = speedField.toFloat(); //produces a float number to no more
//than 2 decimal places
int index = speedAsString.indexOf('.'); //find the period
speedAsString.remove(index); //remove from 2nd dec place to end
speedForDisplay = speedAsString.toInt(); //convert to integer
}
}
//******************************
void GetValidFlag() {
//the valid flag field is the 3rd in the RMC sentence;
//need to determine positions of 2nd & 3rd commas
int posAlpha, posBeta = -1;
for (int i = 1; i <= 3; i++) {
posAlpha = posBeta;
posBeta = currentRMCsentence.indexOf(',', posAlpha + 1);
}
validField = currentRMCsentence.substring(posAlpha + 1, posBeta);
if (validField == "A") validFlag = 1; else validFlag = 0;
}
//******************************
void Get_timeUTC() {
//the UTC time field is the 2nd in the RMC sentence;
//need to determine positions of 1st & 2nd commas
int posAlpha, posBeta = -1;
String hourLocalAsStr, minuteLocalAsStr;
for (int i = 1; i <= 2; i++) {
posAlpha = posBeta;
posBeta = currentRMCsentence.indexOf(',', posAlpha + 1);
}
timeUTCField = currentRMCsentence.substring(posAlpha + 1, posBeta);
if (timeUTCField != "") {
haveTime = true;
//determine local time
hourLocalAsStr = timeUTCField;
hourLocalAsStr = hourLocalAsStr.substring(0, 2);
minuteLocalAsStr = timeUTCField;
minuteLocalAsStr = minuteLocalAsStr.substring(2, 4);
hourLocalasInt = hourLocalAsStr.toInt() + 11;
minuteLocalasInt = minuteLocalAsStr.toInt();
if (hourLocalasInt > 12) hourLocalasInt = hourLocalasInt - 12;
timeLocalasInt = (hourLocalasInt * 100) + minuteLocalasInt;
}
else {
haveTime = false;
DisplayRunningDot();
}
}
//******************************
void determineAlertLevel() {
//speed will be either below/equal or above speed limit
if (speed <= (speedLimit + 2)) { //below/equal
if (speed <= speedLimit - 10) alertLevel = 3; //more than 10kph below
else alertLevel = 0; //just below
}
//above limit
else {
if (speed > (speedLimit + 2) + alertSpan) alertLevel = 2; //above the delta band
else alertLevel = 1; //just above limit
}
if (speed > 110) alertLevel = 1; //always raise alert if faster than 110
}
//******************************
void actionAlert() {
switch (alertLevel) {
case 0:
//below speed limit: turn buzzer off
digitalWrite(pinBZR, LOW);
break;
case 1:
//above speed limit & below sp limit+delta: turn buzzer on & flash display
// display.setBrightness(5);
alertBuzzer();
break;
case 2:
//above sp limit+delta: move it up to next sp limit
if (speedLimit < 110) speedLimit = speedLimit + 10;
digitalWrite(pinBZR, LOW);
break;
case 3:
//below sp limit-10: move to next sp limit downwards
if (speedLimit > speedLimitInitial) speedLimit = speedLimit - 10;
digitalWrite(pinBZR, LOW);
break;
}
}
//******************************
void alertBuzzer() {
int frequency = 2000, duration = 5;
for (int i = 0; i <= 3; i++) {
tone(pinBZR, frequency, duration);
delay(50);
}
}
//******************************
void DisplayEights() {
uint8_t data[] = { 0xff, 0xff, 0xff, 0xff };
display.setSegments(data);
delay(200);
display.clear();
}
//******************************
void SimulateSpeed() {
int interval = 600;
//generate a speed value, changing it every set time interval
//from 35 to 44
for (speedForDisplay = 35; speedForDisplay <= 44; speedForDisplay++) {
speedForDisplay = speedForDisplay;
DisplaySpeed();
speed = speedForDisplay;
determineAlertLevel();
actionAlert();
delay(interval);
}
//stay at 42 for 5 secs
for (int i = 1; i <= 8; i++) {
DisplaySpeed();
speed = speedForDisplay;
determineAlertLevel();
actionAlert();
delay(interval);
}
//from 41 to 35
for (speedForDisplay = 42; speedForDisplay >= 35; speedForDisplay--) {
DisplaySpeed();
speed = speedForDisplay;
determineAlertLevel();
actionAlert();
delay(interval);
}
//from 35 to 85
for (speedForDisplay = 35; speedForDisplay <= 85; speedForDisplay++) {
DisplaySpeed();
speed = speedForDisplay;
determineAlertLevel();
actionAlert();
delay(interval);
}
}
void DisplaySpeed() {
//display the speed on the TM1637 display
// display.showNumberDecEx(speedForDisplay, 0b00100000, false, 4, 0);
// display.showNumberDecEx(speedForDisplay, 0b00000000, false, 4, 0);
display.showNumberDec(speedForDisplay);
}
void DisplayTime() {
// display.showNumberDec(timeLocalasInt);
display.showNumberDecEx(timeLocalasInt, 0b01000000, false, 4, 0);
delay(500);
}
void DisplayRunningDot() {
display.clear();
display.setSegments(dataDot0);
delay(RunningDotDelay);
display.setSegments(dataDot1);
delay(RunningDotDelay);
display.setSegments(dataDot2);
delay(RunningDotDelay);
display.setSegments(dataDot3);
delay(RunningDotDelay);
display.setSegments(dataDot2);
delay(RunningDotDelay);
display.setSegments(dataDot1);
delay(RunningDotDelay);
display.setSegments(dataDot0);
delay(RunningDotDelay);
display.clear();
}
void ManageDisplayContent() {
//display running dot, time or speed
if (haveSpeed) DisplaySpeed();
else if (haveTime) {
DisplayTime();
delay(100);
DisplayRunningDot();
delay(100);
}
else
DisplayRunningDot();
}