Nick Tolk
Published © MIT

Piezo Plinko

A Particle Argon, using data from a pair of piezoelectric elements, locates a falling puck as it falls down a "Plinko"-style pegboard.

IntermediateShowcase (no instructions)8 hours3
Piezo Plinko

Things used in this project

Hardware components

Argon
Particle Argon
×1
Piezo Sensor
ControlEverything.com Piezo Sensor
×2
Flora RGB Neopixel LEDs- Pack of 4
Adafruit Flora RGB Neopixel LEDs- Pack of 4
×1
LED (generic)
LED (generic)
×1

Software apps and online services

Visual Studio Code Extension for Arduino
Microsoft Visual Studio Code Extension for Arduino
Node-RED
Node-RED
Google Maps
Google Maps
Unity
Unity

Story

Read more

Schematics

Argon wiring

Argon wired to piezo elements for capture and Neopixels for display

Argon wiring

Particle Argon wired to 2 piezoelectric disc sensors, LED, and Neopixels

Code

plinkoPiezo

C/C++
Main .ino file for running piezoelectric Plinko
/*
 * Project:     plinkoPiezo
 * Description: Uses a pair of piezo elements attached to a Plinko board to triangulate 
 *              and publish position of falling puck. Coordinates of pegs are charted to
 *              lat/lon coordinates of Albuquerque Plaza.
 * Author:      Nick Tolk
 * Date:        10-APR-2023
 */
// when not "LIVE", events are echoed over Serial rather than being published via MQTT
#define LIVE
#ifdef LIVE
const bool liveRun = true;
#else
SYSTEM_MODE(SEMI_AUTOMATIC)
const bool liveRun = false;
#endif
SYSTEM_THREAD(ENABLED)

#include <math.h>
#include <iostream>
#include <vector>
#include <string>
#include "neopixel.h"

// these are used for image publication to Adafruit dashboard
#include <Adafruit_MQTT.h>
#include "Adafruit_MQTT/Adafruit_MQTT_SPARK.h"
#include "Adafruit_MQTT/Adafruit_MQTT.h"
#include <JsonParserGeneratorRK.h>

#include "credentials.h"

const int ONBOARD_LED = D7;

const int PIXEL_PIN = D2;
const int PIXEL_COUNT = 2;
#define PIXEL_TYPE WS2812B

Adafruit_NeoPixel pixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);

const int PIEZO_PIN_L = A3;   // analog input pins
const int PIEZO_PIN_R = A4;
const int EVENT_LED = D4;     // lit when publishing data

const int PEG_ROWS = 8;   // starting from top (0), even rows have 5 pegs and odd rows have 4

// coordinates on Albuquerque Plaza for plotting positions
const double LAT_TOP = 35.08799050651442;
const double LAT_BOTTOM = 35.08560415806543;
const double LON_RIGHT = -106.65038177579693;
const double LON_LEFT = -106.65203434020583;

// these are all constants tweaked to calibrate to the board and elements being used
// they're for handling input ranges on sensors, and thresholds for deciding whether data is impactful
const int PIEZO_MIN_O = -100;
const int PIEZO_MAX_O = 100;
const float PIEZO_MIN_I = -1.5;
const float PIEZO_MAX_I = 4.0;
const float PIEZO_THRESH_L =  60.0;
const float PIEZO_THRESH_R =  60.0;
const float PIEZO_THRESH_ROW = 70.0;
// us to consider an impact missed or erroneous
const int PIEZO_TIMEOUT = 60*1000;

const int NEW_GAME_T = 500;           // ms to condider a game restarted
const int EVENT_T = 200;              // ms to keep event LED lit when publishing

const int SERIAL_TIMEOUT = 10*1000;   // ms to wait for serial connection - may be absent
const int PUB_DELAY = 200;            // ms to wait between MQTT publishes
unsigned long lastPub;                // for timing

const uint16_t SAMPLES = 4;     // average over several samples as low-pass
uint8_t lIn[SAMPLES];           // left piezo data
uint8_t rIn[SAMPLES];           // right piezo data
system_tick_t tIn[SAMPLES];     // time data

unsigned long lastTick = 0;     // stores us (micros()) ticks for timing
int elapsed;                    // us since lastTick
int dataIndex = 0;

// run every timt through loop() to keep alive
void MQTT_connect();  
bool MQTT_ping();

// creates JSON payload to be published by mPub() thread
void createEventPayLoad(float lat, float lon);

TCPClient TheClient; 
Adafruit_MQTT_SPARK mqtt(&TheClient,AIO_SERVER,AIO_SERVERPORT,AIO_USERNAME,AIO_KEY); 
Adafruit_MQTT_Publish pubFeed = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/latlon");

int piezoL, piezoR; // most recent readings from piezo inputs
long tLeft, tRight; // tLeft and tRight are >= 0 to indicate event time. -1 means nothing currently registered.
int tDiff;          // difference between left and right impact times; used for triangulation
bool eventOn = false; // used to blink onboard LED erradically during Serial startup and indicate events

system_tick_t tNew;     // tracks last impact to decide if a new game has been started
int lr, lrLast, mag;    // time of impact offsets and magnitude of piezo read
double latOut, lonOut;  // values published correlating peg positions to world lat/lon

// microStart and microNow are used to reset timing and record TOI
unsigned long microStart;
long microNow;

std::vector<String> mqttVector;

int piezoI;           // index
float sum;            // used for average calculation
float lAvg, rAvg;
float mLeft, mRight;  // records magnitude at time-of-impact

uint8_t r, g, b;      // for Neopixel feedback

bool playing;     // true when game is in play
int row, col;     // calculated row/column position of last impact

// these clear data arrays
void resetLeft();
void resetRight();

// set Neopixels according to left/right shift from last location
void setLights(int lr, int mag);

void setup() {
  Serial.begin(9600);
  // mPub will publish Strings in mqttVector to "/feeds/plinko"
  new Thread("mPub", mPub);

  pinMode(PIEZO_PIN_L, INPUT); 
  pinMode(PIEZO_PIN_R, INPUT); 
  pinMode(EVENT_LED, OUTPUT);

  pixel.begin();
  pixel.setBrightness (15);
  pixel.setPixelColor(0, 0x00ffff);
  pixel.setPixelColor(1, 0xff00ff);
  pixel.show();

// wait for serial, while blinking onboard LED irregularly to distinguish from system ticks
  pinMode(ONBOARD_LED, OUTPUT); 
  if (!liveRun){
    while(!Serial.available() && millis() - lastTick < SERIAL_TIMEOUT){
      digitalWrite(ONBOARD_LED, eventOn);
      digitalWrite(EVENT_LED, eventOn);
      if (random(2)){             // flip on/off chaotically
        eventOn = !eventOn;
      }
      delay(EVENT_T);
      Serial.begin(9600);
    }
  }

// eventOn is used to track and communicate MQTT traffic
  eventOn = false;
  digitalWrite(ONBOARD_LED, eventOn);
  digitalWrite(EVENT_LED, eventOn);

  if (Serial.available()){
    delay(200);
    Serial.println("Serial is up!");
  }

// make sure wifi is up if we intend to publish
  if (liveRun){
    WiFi.on();
    WiFi.connect();
    while(WiFi.connecting()) {
      Serial.printf(".");
      delay(100);
    }
  }
  Serial.printf("\n\n");

  lastTick = millis();
  microStart = micros();
  tLeft = -1;
  tRight = -1;

  playing = false;
  row = -1;
}

void loop() {
  if (liveRun){
    MQTT_connect();
    MQTT_ping();
  }

// see if we've timed out to new game
  if (playing && millis() - tNew > NEW_GAME_T){
    playing = false;
    row = -1;
    resetLeft();
    resetRight();
  }

// reset event LED if necessary
  if (eventOn && millis() - tNew > EVENT_T){
    eventOn = false;
    digitalWrite(EVENT_LED, eventOn);
  }

// check piezos
  lIn[dataIndex] = analogRead(PIEZO_PIN_L);
  rIn[dataIndex] = analogRead(PIEZO_PIN_R);
  tIn[dataIndex] = millis();

// < 0 indicated no currently registered event
  if (tLeft < 0){
    lAvg = piezoAvg(lIn);
  }
  if (tRight < 0){
    rAvg = piezoAvg(rIn);
  }

  if (!liveRun){
    Serial.printf("\r%6.2f, %6.2f, %f", lAvg, rAvg, (micros()-microStart)/1000000.0);
  }

// if we're not playing, the clock hasn't started
  if (!playing && tLeft < 0 && tRight < 0){
    microStart = micros();
    microNow = 0;
  } else {
    microNow = micros() - microStart;
  }

// register times for events if one of our piezos is above the cutoff
  if ((tLeft < 0) && (lAvg > PIEZO_THRESH_L)){
    mLeft = lAvg - PIEZO_THRESH_L;
    tLeft = microNow;
  }
  if ((tRight < 0) && (rAvg > PIEZO_THRESH_R)){
    mRight = rAvg - PIEZO_THRESH_R;
    tRight = microNow;
  }

// check for registered events on both piezos  
  if (tRight >= 0 && tLeft >= 0){
    tNew = millis();              // keep reset game clock current
    if (!playing){
      playing = true;             // if we weren't playing, we are now
      microStart = micros();      // and we'll reset the clock for the last time this game
      lastTick = microStart;
    }
    if (sqrt(mLeft * mLeft + mRight + mRight) > PIEZO_THRESH_ROW){
      row++;
    }
    if (!eventOn){
      eventOn = true;
      digitalWrite(EVENT_LED, HIGH);
    }

    elapsed = micros() - lastTick;  // time since last registered event
    lastTick = micros();
    tDiff = tLeft - tRight;         // difference to determine position
    lr = (int)round(map(tDiff/1000.0, PIEZO_MIN_I, PIEZO_MAX_I, (float)PIEZO_MIN_O, (float)PIEZO_MAX_O));
    mag = (int)round(sqrt(mLeft * mLeft + mRight * mRight));
    if (row == 0){
      col = map(lr, PIEZO_MIN_O, PIEZO_MAX_O, 0, 4);
    } else {
      if (row % 2){   // 4 pegs on odd rows
        if (lr < lrLast && col > 0){
          col--;
        }
      } else {        // 5 pegs on even rows
        if (lr > lrLast && col < 4){
          col++;
        }
      }
    }
    col = (col > 4) ? 4 : (col < 0) ? 0 : col;

    lrLast = lr;
    latOut = map((float)row, 0.0, (float)PEG_ROWS - 1, (float)LAT_TOP, (float)LAT_BOTTOM);
    if (row % 2){   // 4 pegs on odd rows
      lonOut = map((float)col + 0.5, 0.5, 3.5, LON_LEFT, LON_RIGHT);
    } else {        // 5 pegs on even rows
      lonOut = map((float)col, 0.0, 4.0, LON_LEFT, LON_RIGHT);
    }
    createEventPayLoad(latOut, lonOut);
    if (!liveRun){
      Serial.printf("%s\n", mqttVector.front().c_str());
    }
    setLights(lr, mag);
    // reset impact times and levels
    resetLeft();
    resetRight();
    delay(PIEZO_TIMEOUT / 1000.0);    // pause so we don't double-count
  }

// these check for we have a one-sided event that failed to pair
  if (tLeft >= 0 && (microNow - tLeft) > PIEZO_TIMEOUT){
    resetLeft();
  }
  if (tRight >= 0 && (microNow - tRight) > PIEZO_TIMEOUT){
    resetRight();
  }

  dataIndex = (dataIndex + 1) % SAMPLES;
}

// sets Neopixel colors to reflect detected event locations in real-time
void setLights(int lr, int mag){
  lr = (lr < PIEZO_MIN_O) ? PIEZO_MIN_O : (lr > PIEZO_MAX_O) ? PIEZO_MAX_O : lr;  // bounds check
  r = 3 * mag;
  r = (r > 0xff) ? 0xff : r;
  b = map(lr, PIEZO_MIN_O, PIEZO_MAX_O, 0, 0xff);
  g = map(lr, PIEZO_MIN_O, PIEZO_MAX_O, 0xff, 0);
  pixel.setPixelColor(0, (((r << 8) | g) << 8) | b);
  b = 0xff - b;
  g = 0xff - g;
  pixel.setPixelColor(1, (((r << 8) | g) << 8) | b);

  pixel.show();
}

// returns average of values in *data over SAMPLES elements
float piezoAvg(uint8_t *data){
  sum = 0;
  for (piezoI = 0; piezoI < SAMPLES; piezoI++){
    sum += data[piezoI];
  }
  return(sum / (float)SAMPLES);
}

// sets tLeft and mLeft to 0 and empties lIn[]
void resetLeft(){
  if (!liveRun){
    Serial.printf("RL\n");
  }
  for (piezoI = 0; piezoI < SAMPLES; piezoI++){
    lIn[piezoI] = 0;
  }
  tLeft = -1;
  mLeft = 0;
}

// sets tRight and mRight to 0 and empties rIn[]
void resetRight(){
  if (!liveRun){
//    Serial.printf("RR\n");
  }
  for (piezoI = 0; piezoI < SAMPLES; piezoI++){
    rIn[piezoI] = 0;
  }
  tRight = -1;
  mRight = 0;
}

String strOut;
int tOut;
// runs detached watching for mqttVector to have at least one member, and publishes while honoring the publication throttle delay
void mPub(){
  system_tick_t lastThreadTime = 0;
  while (true){
    if(mqttVector.size() > 0 && millis()- lastPub > PUB_DELAY){
      lastPub = millis();
      strOut = mqttVector.front();
      mqttVector.erase(mqttVector.begin());

      if (liveRun){
        if(mqtt.Update()) {
          pubFeed.publish(strOut);
        }
      } 
    }
    os_thread_delay_until(&lastThreadTime, 10);
  }
}


// Function to connect and reconnect as necessary to the MQTT server.
// Should be called in the loop function and it will take care if connecting.
void MQTT_connect() {
  int8_t ret;
 
  // Return if already connected.
  if (mqtt.connected()) {
    return;
  }
 
  Serial.print("Connecting to MQTT... ");
 
  while ((ret = mqtt.connect()) != 0) { // connect will return 0 for connected
    Serial.printf("Error Code %s\n",mqtt.connectErrorString(ret));
    Serial.printf("Retrying MQTT connection in 5 seconds...\n");
    mqtt.disconnect();
    delay(5000);  // wait 5 seconds and try again
  }
  Serial.printf("MQTT Connected!\n");
}

bool MQTT_ping() {
  static unsigned int last;
  bool pingStatus = 0;

  if ((millis()-last)>120000) {
    Serial.printf("Pinging MQTT \n");
    pingStatus = mqtt.ping();
    if(!pingStatus) {
      Serial.printf("Disconnecting \n");
      mqtt.disconnect();
    }
    last = millis();
  }
  return pingStatus;
}

// crafts JSON packet from arguments, then pushes that onto mqttVector to be handled elsewhere
// previously also created separate packet pushed to tVector for timing
void createEventPayLoad(float lat, float lon) {
  JsonWriterStatic<256> jw;
  {
    JsonWriterAutoObject obj(&jw);

    jw.insertKeyValue ("lat", lat);
    jw.insertKeyValue ("lon", lon);
  }
  mqttVector.push_back(jw.getBuffer());
}

Piezo Plinko

"plinkoPiezo" is used to track a falling puck down the board, then publish peg locations to JSON-formatted lat/lon pairs using MQTT. "plinkoMQTTReplay" will re-publish those packets with 1-second delays to avoid flooding Adafruit.

Credits

Nick Tolk

Nick Tolk

4 projects • 8 followers

Comments

Add projectSign up / Login