loading...
Cover image for Putting My Swimming Pool on the Internet - A Deep Dive Into Building a Pool Bot

Putting My Swimming Pool on the Internet - A Deep Dive Into Building a Pool Bot

j4s0nc profile image Jason C Updated on ・8 min read

This is third article in a multipart series. In Part 1 we talked about the idea for this pool bot, Part 2 covered the hardware behind it.

This article will cover:

Let's put all that sensor data on the Internet!

Connecting Brains

This bot uses an Arduino Nano to read sensor values, and a Particle Photon to connect to the cloud over WiFi. Now let's get them to talk to each other!

The simplest way to make this work is over a serial connection. You may be familiar with using a serial connection to debug your device over usb, but we can also wire the Transmit (TX) and Receive (RX) pins of each devices to send data between them other serial. RxTx.

When wiring this we went to cross the TX & RX wires between devices. So Device on the left transmits/sends (TX) to the device on the right to receive/read (RX), and right's TX to the other RX. We'll also want to give them a common ground wire between them.

One thing to note, the Arduino Nano only has one serial connection and it's shared between the USB port and the TX/RX pins. The Photon has three different serial connections, this will become important when we code the Photon.

  • Serial : USB
  • Serial1: TX/RX Pins
  • Serial2: Pin is shared with onboard LED, can be a pain to use this.

If you're worried about Particle boards only being 3.3v and Arduino being 5v, don't worry! Particle pins can support up to 5v without damaging the board.

Now let's code the Arduino side:

// Arduino 
void setup()
{
  // Serial TX (1) is connected to Photon RX
  // Serial RX (0) is connected to Photon TX
  // Arduino GND is connected to Photon GND
  Serial.begin(9600);
}
void loop()
{
  Serial.print("Arduino Sending Serial Message!");
  delay(1000);
}

Now on the Particle side, we will want to use Serial 1 for the Photons TX/RX pins.

// Particle Photon 
const size_t READ_BUF_SIZE = 256;
char readBuf[READ_BUF_SIZE];
size_t readBufOffset = 0;
void setup() {
  // Serial 1 RX is connected to Arduino TX (1)
  // Serial 1 TX is connected to Arduino RX (0)
  // Photon GND is connected to Arduino GND
  Serial1.begin(9600);  
}

void loop() {

  // Read any data from serial into a buffer
  while(Serial1.available()) {
    if (readBufOffset < READ_BUF_SIZE) {
      char c = Serial1.read();

      if (c != '\n') {
        // Add character to buffer
        readBuf[readBufOffset++] = c;
      }
      else {
        // End of line character found, process line
        readBuf[readBufOffset] = 0;
        processBuffer();
        readBufOffset = 0;
      }
    }
    else {
      //Serial.println("readBuf overflow, emptying buffer");
      readBufOffset = 0;
    }
  }
}

void processBuffer() {
  // We'll talk about the magic here in a little bit! 
  // readBuf would be "Arduino Sending Serial Message!" at this point. 
}

One important note: Both sides of the connection need to be running at the same bps speed! You should notice both have a .begin(9600), this sets the bites per second to 9600 on each device.

Passing Data

Now that we have these devices talking to each other, let's look at passing all this sensor data from the Arduino to the Photon.

I decided to store all the different sensor data into a single structure:

typedef struct
{
  float temp1;
  float temp2;
  float temp3;
  float temp4;
  float heatIndex;
  float humidity;
  int pressure;
  int pumpStatus;
  int waterlevel;
  float ph;
} SensorData;

Inside loop I just write each sensor's value into a struct instead of defining a bunch of variables. At the end of loop I'll write this out to the serial connection.

We could get cute and build out a JSON object or some other complex shape, but remember we're running on a tiny device that is reading these sensors every second and we only setup a 256 character buffer. So it's simple to just dump a comma separated string.

// Arduino 
// Once all the sensor data is assigned to the SensorData struct, we'll send it over serial
void sendSensorData(const SensorData &data)
{
  Serial.print(data.temp1);
  Serial.print(",");
  Serial.print(data.temp2);
  Serial.print(",");
  Serial.print(data.temp3);
  Serial.print(",");
  Serial.print(data.temp4);
  Serial.print(",");
  Serial.print(data.heatIndex);
  Serial.print(",");
  Serial.print(data.humidity);
  Serial.print(",");
  Serial.print(data.pressure);
  Serial.print(",");
  Serial.print(data.pumpStatus);
  Serial.print(",");
  Serial.print(data.waterlevel);
  Serial.print(",");
  Serial.print(data.ph);
  Serial.println();
}

Some of you may be asking, why not just use sprintf? Well, we want to send floats and the Arduino docs say:

"Due to performance reasons %f is not included in the Arduino's implementation of sprintf(). A better option would be to use dtostrf() - you convert the floating point value to a C-style string"

Yea, sure I could go through all that, or just have 20 print statements that do the same thing ;-)

Let's switch over to the Particle Photon side and talk about the magic in the processBuffer method.

// Photon
// This gets called after we see a new line character in the serial data 
// We have defined the same SensorData struct on both devices
void processBuffer() {
    // At this point our buffer should look something like this:
    //readBuf = "69.12,69.24,69.57,71.20,0.00,0.00,14,1,1,7.13"

    // good old c string token logic and string to number conversions  
    char *p = strtok(readBuf, ",");
    latestData.temp1 = atof(p);           p = strtok(NULL,",");
    latestData.temp2 = atof(p);           p = strtok(NULL,",");
    latestData.temp3 = atof(p);           p = strtok(NULL,",");
    latestData.temp4 = atof(p);           p = strtok(NULL,",");
    latestData.heatIndex = atof(p);       p = strtok(NULL,",");
    latestData.humidity = atof(p);        p = strtok(NULL,",");
    latestData.pressure = atoi(p);        p = strtok(NULL,",");
    latestData.pumpStatus = atoi(p);      p = strtok(NULL,",");
    latestData.waterlevel = atoi(p);      p = strtok(NULL,",");
    latestData.ph = atof(p);

    // ... more code, we'll cover in a minute ...
}

At this point the Photon now knows the latest sensor reading passed over serial from the Arduino!

What about these message that are being written about the water level? Serial.println("ALERT Low water! Turning pump OFF");. Remember that from Part 2?

I'm handling that by peeking at the message string and figuring out if it looks like the sensor data. This little helper function can tell us how many commas are in the message:

// Photon
int countChars(char* s, char c)
{
    return *s == '\0' ? 0 : countChars( s + 1, c ) + (*s == c);
}

void processBuffer() {
    // readBuf = "69.12,69.24,69.57,71.20,0.00,0.00,14,1,1,7.13"
    // or maybe readBuf = "ALERT Low water! Turning pump OFF"
    int commas = countChars(readBuf, ',');
    if(commas < 9){
        Particle.publish("MSG", readBuf, PRIVATE);    
        return;
    }

   // good old c string token logic and string to number conversions 
   ...
}

Wondering what that magical Particle.publish function does? read on!

Particle Device Cloud

Before we get too deep, I'd like to take a second and call out how awesome Particle hardware really is! You can pick up a Photon that has wifi built in for $20. These devices can be controlled and flashed over wifi, so you can update code on them without hooking up a usb cable!

Particle also includes access to their online tools like the Web Based IDE and Device Console. Some of Particle benefits (Included for FREE):

  • Free Device Cloud services, up to 100 Wi-Fi devices
  • Remote (over-the-air) firmware updates
  • Standard remote diagnostics with Particle Device Vitals
  • Device-to-cloud encryption and security enabled out-of-the-box

So what does all that mean? Out of the box your Particle devices know how to talk to these services. If you log in to the Particle Console you'll see all your devices. Devices

Clicking on a device will show you detailed information. A cool feature is the signal button (top right), it will turn the onboard LED into "particle nyan" aka a rainbow of colors. This is helpful if you have a few different devices powered on and you need to figure out which is which. Device Details

Now let's get back to pool related things!

Pushing Data To Particle Cloud

Part of the Particle Device OS contains a handful of Cloud Functions baked in.

Let's look at Particle.publish(). This will publish an event to their cloud.

In the processBuffer function we'll transform our SensorData struct into minified JSON and publish it. We will throttle this to one call per 5 seconds. We could add logic to check if important values changes instead, but time based is simple enough.

// in Photon processBuffer function after setting latestData properties

// if you're publishing more than once per second on average, you'll be throttled!
if(millis() - lastPublish > 5000) //Publishes every X milliseconds
{
  // Record when you published
  lastPublish = millis();

  int time = Time.now();
  char sendData[512];
  // Since Particle supports %f in snprintf, we can format all the data in one call.
  snprintf(sendData, sizeof(sendData), "{\"t\":%d,\"t1\":%.2f,\"t2\":%.2f,\"t3\":%.2f,\"t4\":%.2f,\"hi\":%.2f,\"hum\":%.2f,\"pr\":%d,\"ps\":%d,\"lvl\":%d,\"ph\":%.2f}", 
     time,
     latestData.temp1,
     latestData.temp2,
     latestData.temp3,
     latestData.temp4,
     latestData.heatIndex,
     latestData.humidity,
     latestData.pressure,
     latestData.pumpStatus,
     latestData.waterlevel,
     latestData.ph
  );
  lastSentData = latestData;

  //publish JSON string to the cloud! 
  Particle.publish("SensorData", sendData, PRIVATE);
}    

Now we can see this data flowing to the events tab in the Particle web console
Events

At this point my pool is a Thing on the Internet! In the next part of this series we'll cover building backend services to store these events, for now let's keep talking about Particle Cloud awesomeness.

Particle Cloud To Device

If you peeked at the Particle Cloud Documentation you may have seen Particle.variable() and Particle.function() these allow us to do things in the cloud that affect our device.

I want to control my pool pump remotely and be able to manually override it so it's on or off regardless of water level readings. I also want to be able to clear the override. Since the Arduino is the brain controlling the relay to the pump, I just pass the command through serial.

// Photon
void setup() {
    //Connect cloud function to local function 
    Particle.function("sendCommand", sendCommand);
}
int sendCommand(String command)
{
    /* valid commands:
        + Pump override and pump ON
        - Pump override and pump OFF
        ! Clear Pump override
    */
    Particle.publish("MSG", "Received Command: " + command, PRIVATE);  
    Serial1.print(command);
    return 0;
}

On Arduino side, we just read the serial data.

// Arduino 
// this is called as the first step in loop()
void readSerial() {
  while (Serial.available() > 0) {
    char incomingCharacter = (char)Serial.read();
    Serial.print("Received Seriral Command: ");
    Serial.println(incomingCharacter);
    switch (incomingCharacter) {
      case '+':
        pumpOverride = true;
        setPump(HIGH);
        break;

      case '-':
        pumpOverride = true;
        setPump(LOW);
        break;

      case '!':
        pumpOverride = false;
        break;
    }
  }
}

You may notice command is a string on Particle side, but for my needs I only built one character commands, this made the logic a bit simpler.

After flashing this code, you'll see a new box in the Particle Web Console device details page called Functions
particle function

When I enter a value in there and click call, Particle cloud sends it to my Particle Photon device, which calls the local sendCommand function. That function writes the command to serial and the Arduino reads the serial and fires the matching logic.

Later in this series you'll see how we can call this cloud function from our web app.

Up Next: Saving Particle Events to Azure Table Storage

Discussion

pic
Editor guide