In the spring, the lake near us is a tempting place to recreate, but the water temperature isn’t always as welcoming as it may seem. In the early season, there is sometimes a danger of hypothermia from the combination of cool air and water temperature. The rule of thumb is: Don't go in the water if the sum of the water temperature and air temperature is less than 120 degrees Fahrenheit. Even the best trained swimmers are at significant risk in cold weather. Out of water activities like boating or canoeing can also be dangerous when this condition is not met, as there is always a small chance of capsizing and going for an unexpected swim.
GoalsThe goal of this project is to build a weather buoy that can provide an indicator of whether or not it is safe to go out on the water. The buoy will use the Arduino MKR WiFi 1010 included in the Oplà IoT Kit to read a waterproof temperature sensor and the atmospheric sensors on MKR IoT Carrier will retrieve the atmospheric conditions. The IMU onboard the MKR IoT Carrier will be used to determine the current wave conditions.
Safe WaterThe core of the buoy code is quite simple, and it is already formed into pseudo-code for us!
if ( Water Temperature + Air Temperature < 120) {
//
}
That's it! We have two choices for the water temperature, as it is often different at the water's surface and a few feet down. In this case, I've written the script so it uses the average of these two temperatures. The water temperature sensors I am using are generic DS18b20 sensors in a stainless steel case. These are the most common option that appear when you search for "waterproof temperature sensors." These will require the Dallas Temperature library and the One Wire library. I followed the standard wiring of these sensors, with a 4.7 kΩ resistor bridging the Vcc and Data lines. I have two sensors, but because they are DS18b20s, they use the same digital pin of the MKR WIFi 1010. This means that when we want temperature from the sensor, we have to specify which one in the code.
Other FunctionsBesides the water safety, we want to add a few extra functions if we can, especially to take advantage of the sensors already onboard the MKR IoT Carrier. Air temperature, humidity, and pressure are pretty simple so we will add those to our main loop. The MKR IoT Carrier also has an Inertial Measurement Unit (IMU), which has a gyroscope and accelerometer inside. I wanted to use the IMU to take a look at how rough the water is. To get an idea of the wave conditions, I settled on two metrics; wave period and wave intensity.
Wave period is the time between wave peaks. Wave intensity is how strong the wave is. We can't rely on a single data set from the IMU to measure the period and strength of the waves, as we have no way of knowing where in the wave cycle the IMU is measuring. Instead of instantaneous measurements, we can sample the IMU data rapidly for a short time, then analyze it afterwards. This is the approach I am using.
IMU SamplingTo sample the IMU as described above, we will need two parameters. How often do we want to collect the data, and how long do we want each collection to take. In my code, I set up these values in the beginning of the script with variables. This makes it easy to change later if I decide I am unhappy with them.
// How long to wait between IMU collections (seconds)
int collectionInterval = 900; // 15 minutes is 900 seconds
// How long do you want to wait between datapoints (milliseconds)
int imuDelay = 100;
// How many data points to collect during each period
const int collectionTarget = 200; // This should take ~20 seconds overall
After we collect our datapoints, we have to decode them. If the MKR IoT Carrier is mounted parallel to the surface of the water, I found that the Z acceleration can be used as a rough estimate of wave intensity. The sharper the wave front, the higher the vertical acceleration will be.
In the same orientation, the Z gyroscope passes through zero during each wave. We simply count how many times the data goes from positive to negative and divide the collection time (which we measure) by that number. This gives us the approximate average time between wave crests.
void readIMU() {
unsigned long startCollecting = millis(); // Record the start time
int indexG = 0;
int indexA = 0;
int crossings = 0;
float lastGZ = 0;
float highA = 0;
int updateOn = 10;
updateDisplayIMU(0);
// Continue collecting as long as you haven't reached the target
while ((indexG < collectionTarget) || (indexA < collectionTarget)) {
if (carrier.IMUmodule.gyroscopeAvailable()) {
carrier.IMUmodule.readGyroscope(gyroX, gyroY, gyroZ);
if ((lastGZ > 0 && gyroZ < 0) || (lastGZ > 0 && gyroZ < 0)) {
// Has the Z gyroscope crossed zero?
crossings++; // If so, increment the counter
}
lastGZ = gyroZ; // Replace the last number
indexG++; // Increment the index counter
}
if (carrier.IMUmodule.accelerationAvailable()) {
carrier.IMUmodule.readAcceleration(acelX, acelY, acelZ);
if (acelZ > highA) { // Is the Z component the highest recorded?
highA = acelZ; // If so, replace it
}
indexA++; // Increment the index conter
}
if ((indexA == updateOn) || (indexG == updateOn)) {
// If 10 samples have passed
updateDisplayIMU(updateOn);
// update the display with the remaining samples
updateOn = updateOn + 10; // Increment the counter
}
ArduinoCloud.update();
delay(imuDelay);
}
unsigned long stopCollecting = millis();
// Use the collected data to find the wave characteristics
float dT = (stopCollecting - startCollecting)/millisCorrection;
waveIntensity = highA;
if (crossings > 0) { // Avoid dividing by zero
wavePeriod = dT/crossings;
}
else { // Make it an obviously wrong value
wavePeriod = -100;
}
}
IoT ConnectivityThe code I have written periodically contains ArduinoCloud.update();
This function is part of the ArduinoIoTCloud library, and it updates any of the cloud-connected variables that you define during setup of your project in the IOT Cloud. You can see the variables I chose to connect to the cloud at the beginning of the full code below. I set up a simple dashboard to monitor these variables, as shown below.
I set up a cloud-connected boolean (true/false) variable called unitSelector which is connected to the switch on the lower right of the dashboard. By default, the variable is set to true, making the units metric when the buoy powers on or resets. The selector is checked when the code reads the sensors.
void readSensors() {
// Humidity is the same in metric and imperial, so read it first
airHumidity = carrier.Env.readHumidity();
if (unitSelector) { // If reading metric units
airTemp = carrier.Env.readTemperature();
airPressure = carrier.Pressure.readPressure();
deepWaterTemp = tempSensors.getTempCByIndex(deepwaterProbe);
surfaceWaterTemp = tempSensors.getTempCByIndex(surfacewaterProbe);
}
else { // If reading imperial units
airTemp = carrier.Env.readTemperature(FAHRENHEIT);
airPressure = carrier.Pressure.readPressure(PSI);
deepWaterTemp = tempSensors.getTempFByIndex(deepwaterProbe);
surfaceWaterTemp = tempSensors.getTempFByIndex(surfacewaterProbe);
}
// Take the average water temperature
avgWater = (deepWaterTemp + surfaceWaterTemp)/2;
}
The unitSelector also redefines the safe water temperature to the appropriate units (49C or 120F).
Final ThoughtsThis concludes the tutorial. The buoy can be housed in anything from a Tupperware with hot glue to a custom 3D printed enclosure. It's currently winter where I live, so I haven't had a chance to prototype the enclosure yet. Hopefully it will be warm enough soon to give it a thorough run though.
Comments