GPS speedo design in the making

I guess you mean “protection”. No. I actually wanted the electronics to be uncovered. I thought that wud make it more interesting to some. But as it turned out, the GPS module is hidden. I definitely want the wiring hidden.

The crystal is flush with the bottom of the case so it sits cleanly on a surface. It is meant to sit on the dash but is portable. It could be carried around – on a bicycle, bus, tram or even a plane.

I’ll make a cover plate that will go over the aperture. Easy. Might have to recess the pro mini a bit more than it is.

Great comment. That will make it a better design. Love it.

John.

6 Likes

A bit of clear acrylic might get you the best of both worlds

You could get a piece laser cut, or some mesh might do well enough. Might be worth a bit of adhesive copper tape or even just some Al foil around for some ESD protection?

5 Likes

Good idea. The maker space I use has a laser cutter and the acrylic cut outs I’ve seen are brilliant.

Thanks.

6 Likes

I’ve fitted all the boards and wired them together and added the hood.

It looks ok but soldering the wiring was not easy. I made many mistakes and did much rework.

5 Likes

Looks schmick! :slight_smile: It’s the outside that counts for appearance. Most things I’ve made are good from far but far from good :wink:

Looking at it now though, one thing that might be worth adding later is a brightness control for night driving.

4 Likes

Thanks, Ollie. You’re spot on about the strong light. The white LEDS are amazingly bright and at night too bright when driving. The device can actually be used as a half-decent torch.

They can be dimmed via the sketch. I think I’ll add a small slide switch to select b/w two brightness settings. A light sensor could do the job automatically but I’m trying to make an uncomplicated basic device.

4 Likes

Finally got the gadget finished and working. A few hurdles in getting to here.


4 Likes

Here’s a 50 sec video of the device in action. I’m not set up to record safely while driving so this is using a speed simulated by the sketch.

link to video:
Simulated speed trial

Speed goes from 35 to 43 (kph), stays there for about six seconds, back down to 35, then all the way up to 85.

The overspeed audible alerts occur at between each speed limit and speed limit + 4. So if the current speed limit is 60, the alert will sound at 61, 62 and 63. The speed limits are 40, 50, 60, 70, 80, 90, 100,110 (kph).

7 Likes

I reckon I’ve finished the device. Quite pleased with it. :grinning:

short video:
Final Product

3 Likes

Tinkercad images of the finished assembled device:
Capture
Capture2
Capture3

Image can be viewed as a 3-D rotatable object in Tinkercad. Feel free to copy, edit and print any part.
Tinkercad link

5 Likes

Exploded views:
Capture4
Capture5

Image can be viewed as a 3-D rotatable object in Tinkercad. Feel free to copy, edit and print any part.
Tinkercad link

5 Likes

Is anyone interested in making this device?

3 Likes

Hi John,

My daily use probably wouldn’t need one (walking or riding around the suburbs/city) I can see that this would be very useful for truck drivers or those that do lots of highway driving.

I love all of the consideration that went into the 3D design and would love to see your code(specifically the GPS stuff) as its been a bit of a ‘black box’ for my intents and purposes.

It’s been great to see the project progress.

3 Likes

Thanks, Liam.

It works on a bicycle!

The code wasn’t hard – the 3D print design was the hard part. I’ll post the code for anyone to use or improve.

It was fun to start with the idea and explain the development. Where I ended up is vaguely similar to the initial design. That intrigues me. I think things get simpler as you go but it is a long path to get to simple!

John.

3 Likes

The following post contains the code. Parts of it were developed in an ad hoc fashion so need cleaning up.

The GPS tasks are not difficult but the comms have a problem. Some data is lost – some sentences are stitched together. This doesn’t matter as I only need the first two sentences and they come thru OK. But it’s something I want to resolve.

3 Likes
/*
  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();

}
3 Likes

Sorry, that’s a really ugly presentation of the code.

3 Likes

I have dealt with receiving and decoding NMEA data. All my programming is in assembler, so I’m not sure if the technique works for you. Basically, the serial input is dealt with in a rather complex interrupt routine. It requires two buffers, one receiving the current message and one to deal with the previous. The interrupt routine enters on every character received from the GPS module, basically asking is this a $, a * or something else. $ starts a buffer, in my case * starts a checksum test but that is optional, could be used as ‘end of buffer’. Anything between $ and * goes into the buffer.

When the buffer is full, the state of the mainline when the interrupt occurred is saved. This depends on the architecture, it may be status and any register used by the routine, or it could be more (in the PIC16F1455 it is 8 bytes). The pointer to buffers is swapped so incoming characters go to the ‘other’ buffer and the current buffer is available to be examined. Then interrupts are enabled. The current buffer is examined for useful data, and it is stored for mainline use. Flags are set to indicate there is data. Once the buffer is processed, interrupts are disabled, the saved mainline status is restored, the routine exits with a return from interrupt (which returns to the mainline and enables interrupts).

Ordinarily enabling interrupts in an interrupt routine is suicide. However, in this case the mainline state was saved before enabling and restored at the end. This allowed the buffer data to be examined and extracted (but not processed, that’s the mainline’s job) in a ‘thread’.

There should be plenty of cycles to do the required processing. In my case I am using a software UART to receive the NMEA data which adds another layer of processing - processing bits which then create a thread to deal with an assembled character which then create a thread to deal with a whole message which can set flags for the mainline. What needs considering is the depth of stack. Creating a thread puts one more address on the stack. This is not normally an issue, the above PIC has a 16 deep stack and I think ATmega32 can set any depth.

3 Likes

Hi John,

Awesome project, it was really cool seeing it progress here on the forum!
Those TinkerCAD drawings and comments in the code look amazing :smiley:
As @Liam120347 pointed out I imagine there would be many people that would make great use of one!

PS: I’ve added two sets of 3x back quotes (same key as Tilde ~) ``` around the outside of your code so that it formats itself nicely.
If you’re keen on doing a full write up I’d take a look at our projects tab and the steps required (there’s a backlog at the moment but we hope to get some more live soon!): https://core-electronics.com.au/projects

Liam.

2 Likes

Hi John,

Yeah I’d be keen to make one :smiley: My use case is a bit different when I’m riding but a remix of the project would let me include all the features I’d need.
I’ve fallen into the trap of over-simplifying something to the point where it becomes complex - such a rabbit hole.

2 Likes