Contact sales

How to build a traffic-monitoring clock with Particle Logic

Christian Chavez article author avatarChristian ChavezJune 21, 2024
How to build a traffic-monitoring clock with Particle Logic

The Problem

Traffic is the bane of my existence. Every morning I leave for work at the same time yet I never arrive at work at the right time. Some days I have time to spare, arriving so early I don’t know what to do with myself. Other days I have to sprint across the parking lot to get there on time because I spent twenty minutes sitting nose to nose on the the highway, barely moving. 

The Solution

To fix my mornings I need to know when to leave, so I built the Traffic Cube. This project uses Particle’s cloud Logic Functions to create a real-time traffic monitoring cube. Logic Functions run on the cloud and are useful for handling calculations and modifying device behaviors like timing. These functions can be modified live at any time without the need to send a firmware update to our Photon 2.

Our function dynamically calls a TomTom API webhook with custom parameters that returns traffic data to the Photon 2. These conditions are visualized using a SSD1309 display and an 8×8 pixel grid encased in a translucent acrylic cube.

Objectives:

  • Build and configure a monitoring cube using Particle Photon 2, NeoPixel like LEDs, and an SSD1309 display.
  • Use Logic on Particle Cloud to create and manage dynamic webhooks based on real-time device data.
  • Modify device behavior live on the cloud using Logic for real-world product applications.
  • Call TomTom API for live traffic updates and route planning based on conditions set in our Logic Function.

 

Bill of materials:

Particle Photon 2
8×8 LED grid – to indicate traffic delay
SSD1309 OLED display – to show live drive times
Custom Printed Circuit Board (PCB) or breadboard
Laser-cut acrylic cube – for housing the components and diffusing pixel light

Required software and skills:
Particle Cloud

TomTom API

 

Hardware Setup:
Building the Traffic Cube:

This project can be built with just Photon 2 plugged or with any creative combination of hardware to react to data. In our case, the SSD1309 display is controlled using I2C using the Photon 2’s SDA (D0) and SCL (D1) pins. The pixel grid is connected to pin D2. The display shows live traffic information and the pixels represent traffic conditions with color changes.


PCB and Enclosure: I started by building the circuit on a breadboard to test the display, pixel grid, and API calls. I then designed a custom PCB for the Photon 2 with ports to access the I2C and D2 pins. Designing your own PCB can be useful for developing your prototype into a real-world product.



Information on designing your own custom PCBs can be found on the Particle blog. I ordered 5 copies of this board from JLCPCB for a grand total of $31.90 including shipping and they were delivered less than one week after order time.

Photon 2 Firmware Configuration: Reference the the full repository on GitHub at https://github.com/christianc355/trafficStreamLogic

  Setup

 

  • If this is your first time using a Particle device, you can learn more here about getting started with Photon 2.
  • Install the necessary libraries for your hardware using the Particle Web IDE or desktop Workbench. In our case, we install the “SSD1306.h” library for our display, and the “neopixel.h” library for our pixel grid. The “1306” library works great even though our display is technically the “1309”. 
  • Startup display, neopixels or any other attached hardware.

void loop()

  • Create JSON Data utilizing JSONBufferWriter to compile essential data from the device including default geographical coordinates and the current local time from the controller. This JSON formation serves as the primary payload for cloud-based Logic Functions.
  • After creating the JSON payload, use Particle.publish() to send this data to the particle cloud. This triggers the cloud function that will process this data.
  • Use a millis() as a non-blocking timer to execute our publish at a regular interval.
void loop()
{
 lightPixels(pixelPattern);


 if (Particle.connected() && millis() - lastTime > logicCallInterval)
 {
   Serial.printf("\n\nLocal Time Now: %02i:%02i\n\n", Time.hour(), Time.minute()); // print current time


   JSONBufferWriter writer(data, sizeof(data) - 1); //build json data


   writer.beginObject();


   writer.name("coordinates").value(travelLocations);
   writer.name("h").value(Time.hour());
   writer.name("m").value(Time.minute());
   writer.name("s").value(Time.second());
   writer.name("yr").value(Time.year());
   writer.name("mo").value(Time.month());
   writer.name("d").value(Time.weekday());
   writer.name("tz").value(Time.zone());


   writer.endObject();


   Serial.printf("json: %s\n", data);


   Particle.publish("trafficLogic", data); // call Particle Logic function


   lastTime = millis(); // reset our timer
 }
}

Handle Logic Response

  • Once the data is processed in the cloud, the resulting webhook sends data back to the device in JSON format. This data is processed by the handleResponse() function using the JSONObjectIterator. This checks for keys like “trafficDelayInSeconds” and updates device variables accordingly.
  • Our TomTom API will return traffic delay and travel times based on the coordinates provided. Any other parameters set in the Logic Function like interval adjustments or pixel brightness will be handled as well.
  • After deconstructing the JSON response we can use the resulting data to light up the pixels and print information to the OLED.
void handleResponse(const char *event, const char *data)
{
 if (data) // if data is valid
 {
   // parse json data from webhook responses
   JSONValue tomtomData = JSONValue::parseCopy(data);
   if (tomtomData.isValid())
   {
     JSONObjectIterator iterator(tomtomData);
     while (iterator.next())
     {
       if (iterator.name() == "pixelBrightness")
       {
         pixelBrightness = iterator.value().toInt();
       }
       else if (iterator.name() == "logicCallInterval")
       {
         logicCallInterval = iterator.value().toInt();
       }
       else if (iterator.name() == "travelTimeInSeconds")
       {
         travelTimeInSeconds = iterator.value().toInt();
       }
       else if (iterator.name() == "trafficDelayInSeconds")
       {
         trafficDelayInSeconds = iterator.value().toInt();
       }
     }
       pixel.setBrightness(pixelBrightness);
    
    //process and display data below...see github for full code example 


   }
 }
}

Particle Logic and Dynamic Webhook Configuration:

 

Note: To use Logic Functions, you must assign your device to a Product in the Particle Console. Refer to Introduction to Products for additional information in setting up your Product.

 

Accessing Particle Logic:

  • Navigate to the Particle Console and access the Logic tab.
  • Create a new function and choose your product.

 

Implementing Custom Logic:

  • Choose a template . “Reformat JSON Data” is the most useful template to format data for our TomTom API call. 
  • Start with pre-configured JSON interpretation logic and expand it to include time-based conditional checks.
  • In this example we include condition checks for the following:
    • Weekday Mornings: Set the destination to Downtown Los Angeles.
    • Rest of the Day: Switch destination to the beach.
    • Friday Nights: Update the destination to the nearest cinema.
    • Weekends: Adjust for leisure activities like visits to Disneyland.
    • Late nights: Lower pixel brightness and display a goodnight message.
  • This logic dynamically updates the JSON payload and then calls our webhooks using Particle.publish() similar to how you would when writing Particle device firmware. I call one webhook to update hardware settings and another to get live traffic data. The webhooks each trigger responses to the Photon 2 containing JSON data.
//full javascript file available for reference as TrafficLogic.js at https://github.com/christianc355/trafficStreamLogic

import Particle from 'particle:core';


export default function reformat({event})
{
 let data;
 try
 {
   data = JSON.parse(event.eventData);
 }
 catch (err)
 {
   console.error("Invalid JSON", event.eventData);
   throw err;
 }


 // process event data from our Photon 2
 const day = data.d;    // Day of the week, 1 for Sunday, 7 for Saturday
 const hour = data.h;   // Hour in 24-hour format
 const minute = data.m; // Minute


 // Default values updated using conditional logic below


 let pixelBrightness = 100;                                                           // variable to control neopixel brightness of 0-255
 let TomTomAPIKey = "NeVqkDdkpf2GXb5EmGhAuk5mroT*****" let logicCallInterval = 60000; // control time in milliseconds between logic calls on the controller
 let startLon = -118.3089;                                                            // Burbank
 let startLat = 34.1808;
 let destLon = -118.4085; // LAX
 let destLat = 33.9416;
 let routeDescription = "LAX" let targetHour = 13; // default target hour and minute in 24 hour time
 let targetMinute = 20;


 // Weekday schedule
 if (day >= 2 && day <= 6)
 {
   if (hour >= 5 && hour < 11)
   {
     routeDescription = "Downtown Los Angeles";
     destLon = -118.2468; // Downtown LA
     destLat = 34.0522;
     targetHour = 10;
     targetMinute = 30;
     pixelBrightness = 255; // set to max brightness
   }
   else if (hour >= 11 && hour < 22)
   {
     routeDescription = "Santa Monica Beach";
     destLon = -118.4912;
     destLat = 34.0195;
     targetHour = -1; // set to -1 to specify any arrival time
     targetMinute = 0;
   }
 }


 // Saturday schedule
 if (day == = 6)
 {
   if (hour >= 5 && hour < 7)
   { // Disneyland park opening time
     routeDescription = "Disneyland";
     destLon = -117.9189; // Disneyland
     destLat = 33.8121;
     targetHour = 7;
     targetMinute = 30;
   }
   else if (hour >= 8 && hour < 22)
   { // Saturday activity
     routeDescription = "Griffith Observatory";
     destLon = -118.2942;
     destLat = 34.1184;
     targetHour = -1; // no specified arrival time
     targetMinute = -1;
   }
 }


 // format geographical coordinates for TomTom API call
 const coordinates = `${startLat}, ${startLon} : ${destLat}, $ { destLon }
 `;


 const reformattedCoordinates = {
   coordinates : coordinates,
   TomTomAPIKey : TomTomAPIKey
 };


 const hardwareParameters = {
   pixelBrightness : pixelBrightness,     // Example brightness setting
   logicCallInterval : logicCallInterval, // time between logic calls in milliseconds
   routeDescription : routeDescription,   // description message to diplay on OLED
   targetHour : targetHour,               // target arrival time
   targetMinute : targetMinute
 };


 Particle.publish("updateHardware", hardwareParameters, {productId : event.productId});
 Particle.publish("calculateRoute", reformattedCoordinates, {productId : event.productId});
}


// event test data {"h":15,"m":8,"s":26,"yr":2024,"mo":5,"d":1,"tz":-7}

Setting up Dynamic Webhooks

  • Create a custom webhook that builds the updated JSON data from Logic into an API call to TomTom to retrieve live traffic data. We can insert variables from Logic into the webhooks using mustache templates. See Particle documentation on webhooks for more information.

We use {{coordinates}} to pass in geographical data to the TomTom Calculate Route API call. 

{
   "name": "calculateRoute",
   "event": "calculateRoute",
   "responseTopic": "webhookResponse",
   "errorResponseTopic": "",
   "disabled": false,
   "template": "webhook",
   "url": "https://api.tomtom.com/routing/1/calculateRoute/{{{coordinates}}}/json?key={{{TomTomAPIKey}}}&computeBestOrder=true&routeType=fastest&traffic=true",
   "requestType": "GET",
   "noDefaults": true,
   "rejectUnauthorized": true,
   "responseTemplate": "{\n  \"travelTimeInSeconds\": \"{{routes.0.summary.travelTimeInSeconds}}\",\n  \"trafficDelayInSeconds\": \"{{routes.0.summary.trafficDelayInSeconds}}\"\n}"
}
  • For our hardware control webhook we use {{{PARTICLE_EVENT_VALUE}}} to pass the entire data event to the Photon 2. The webhook response template can be used to send JSON data to the controller.
  • We call to console.particle.io because the webhook requires a valid URL to operate and we do not need any API data back from this call.
{
   "name": "updateHardware",
   "event": "updateHardware",
   "responseTopic": "webhookResponse",
   "errorResponseTopic": "",
   "disabled": false,
   "template": "webhook",
   "url": "https://console.particle.io",
   "requestType": "GET",
   "noDefaults": true,
   "rejectUnauthorized": true,
   "responseTemplate": "{{{PARTICLE_EVENT_VALUE}}}"
}

Handling Responses on the Photon 2

  • Use the “handleResponse” function as previously referenced on your Photon 2 to to interpret JSON data received from both webhooks.
  • If certain data like “pixelBrightness” is not received, the existing global value is retained.

 

Practical Applications and Conclusion:

The ability to modify advanced functions and make dynamic webhook calls on the cloud is an essential tool for modern IoT products. In our Traffic Cube scenario, this would give us the ability to easily switch from the TomTom API to let’s say the Google Maps API without disrupting service for users. We can even use the product functionality altogether. For example if we wanted to use our cube to monitor airline traffic or stock prices instead of drive times.

Using the Particle Cloud in this way extends beyond simple traffic data applications to unlimited use cases.  This example is meant to be used as a framework for real-world IoT products where Particle Logic can transform how IoT devices are deployed.

 

 

References:

Logic:

https://docs.particle.io/getting-started/logic-ledger/logic/

Webhooks:

 https://docs.particle.io/reference/cloud-apis/webhooks/


Variable Substitution: https://docs.particle.io/reference/cloud-apis/webhooks/#variable-substitution

JSON Object Iterator:
https://docs.particle.io/reference/device-os/api/json/jsonobjectiterator/#jsonobjectiterator-jsonobjectiterator-const-jsonvalue-amp-value

TomTom Calculate Route API: https://developer.tomtom.com/routing-api/documentation/routing/calculate-route

Comments are not currently available for this post.