June 2023
Boron Argon Xenon |
Feather Spec. |
Custom Project FeatherWing |
| | Boron Argon Xenon |
Feather Spec. |
Custom Project FeatherWing |
---|---|---|---|---|---|---|
RST | RST | | | --- | --- | --- | |
3V3 | 3V3 | | | --- | --- | --- | |
D20 | ARf | | | --- | --- | --- | |
GND | GND | | | --- | --- | --- | |
A0 D19 | A0 | | | Li+ | Li+ | ||
A1 D18 | A1 | | | EN | EN | ||
A2 D17 | A2 | | | Vusb | Vusb | +5VDC | |
A3 D16 | A3 | | | D8 UART2 RTS |
D13 | DI Bilge Fwd | |
A4 D15 | A4 or D24 (2) | | | D7 LED |
D12 | Pushbutton LED | |
A5 D14 SS | A5 or D25 (2) | | | D6 UART2 CTS |
D11 | DI Bilge Mid | |
D13 SCK |
SCK | | | D5 UART2 RX |
D10 | DI Bilge Aft | |
D12 MOSI |
MOSI | | | D4 UART2 TX MISO |
D9 | DI Eng Run | |
D11 MISO |
MISO | | | D3 SCL1 MOSI UART1_CTS |
D6 | ||
D10 UART1 Rx |
Rx | GPS | | | D2 SDA1 SCK UART1_RTS |
D5 | |
D9 UART1 Tx |
Tx | GPS | | | D1 SCL |
SCL | |
NC | (1) | | | D0 SDA |
SDA |
Color Key: SPI I2C (I2C pullup on FeatherWing, not Feather) GPIO free
The Particle Boron cellular IoT device will publish a JSON string to the Particle Cloud, referencing a Particle webhook. The webhook reformats the data, and then sends it to the Blynk Cloud via a HTTP GET, updating the Blynk datastreams. Note that the device doesn't run Blynk code, and therefore it will never appear as "online" to Blynk.
/*
Project KnotWorkin
Author: Mark Kiehl / Mechatronic Solutions LLC
Date: June 2023
Publish to Blynk every 5 minutes only if:
- Any bilge pump has been activated.
- If the boat's position has changed by more than 122 m / 400 ft
AND the engine is not running (anchor drag).
- The boats position if a GPS fix was obtained.
Blue LED on D7:
Turns on constant during setup, then breathes when called in loop() unless:
mode 4 (fast burst every 1 s) if GPS cannot get a fix.
The GPS red LED blinks at about 1Hz while it's searching for satellites,
and blinks once every 15 seconds when a fix is found.
Hardware:
Custom FeatherWing for 5V power to Vusb and 4x digital input monitoring.
Adafruit GPS FeatherWing
Software:
Adafruit GPS FeatherWing library
Custom code for sending data to Particle Webhook for Blynk.
*/
#include "Particle.h"
const char* firmware_version = "0.0.0";
uint8_t led_mode = 0;
boolean just_started = true;
/////////////////////////////////////////////////////////////////////////
// blinkLEDnoDelay()
unsigned long LEDblinkPeriod = 8;
unsigned long LEDblinkLast = 0;
uint8_t LEDblinkPWM = 0;
bool LEDblinkState = false;
uint8_t LEDlastMode = 0;
void blinkLEDnoDelay(byte pin, byte mode) {
// Blink the LED on 'pin' without using delay() according to
// the 'mode' argument defined below.
// pin must support PWM.
//
// mode:
// 0 = breathing
// 1 = blink slow constantly
// 2 = blink fast constantly
// 3 = slow burst every 1 second
// 4 = fast burst every 1 second
//
// 0=breathing; 1=slow blink; 2=fast blink; 3=slow burst; 4=fast burst
// Required global variables: LEDblinkPeriod, LEDblinkLast, LEDblinkPWM, LEDblinkState, LEDlastMode
if (mode == 0) {
// breathing
LEDblinkPeriod = 8;
if (LEDlastMode != mode) {
LEDblinkPWM = 0;
LEDblinkState = true;
digitalWrite(pin, LOW);
}
if (millis() - LEDblinkLast >= LEDblinkPeriod) {
if (LEDblinkPWM > 254) LEDblinkState = false;
if (LEDblinkPWM < 1) LEDblinkState = true;
if (LEDblinkState) {
LEDblinkPWM++;
} else {
LEDblinkPWM--;
}
analogWrite(pin, LEDblinkPWM);
LEDlastMode = mode;
LEDblinkLast = millis();
}
} else if (mode == 1) {
// blink slow constantly
LEDblinkPeriod = 1000;
if (millis() - LEDblinkLast >= LEDblinkPeriod) {
digitalWrite(pin, LEDblinkState);
LEDblinkState = !LEDblinkState;
LEDlastMode = mode;
LEDblinkLast = millis();
}
} else if (mode == 2) {
// blink fast constantly
LEDblinkPeriod = 100;
if (millis() - LEDblinkLast >= LEDblinkPeriod) {
digitalWrite(pin, LEDblinkState);
LEDblinkState = !LEDblinkState;
LEDlastMode = mode;
LEDblinkLast = millis();
}
} else if (mode == 3) {
// slow burst every 1 second
// Slow 4 blinks (lazy burst) followed by 1 sec pause
if (LEDlastMode != mode) {
LEDblinkPWM = 0;
LEDblinkState = true;
LEDblinkPeriod = 100;
}
if (millis() - LEDblinkLast >= LEDblinkPeriod) {
if (LEDblinkPWM < 7) {
if (LEDblinkPWM == 0) LEDblinkState = true;
digitalWrite(pin, LEDblinkState);
LEDblinkPeriod = 100;
LEDblinkState = !LEDblinkState;
LEDblinkPWM++;
} else {
digitalWrite(pin, LOW);
LEDblinkPWM = 0;
LEDblinkPeriod = 1000;
}
LEDlastMode = mode;
LEDblinkLast = millis();
}
} else if (mode == 4) {
// fast burst every 1 second
// Fast 4 blinks (burst) followed by 1 sec pause
if (LEDlastMode != mode) {
LEDblinkPWM = 0;
LEDblinkState = true;
LEDblinkPeriod = 25;
}
if (millis() - LEDblinkLast >= LEDblinkPeriod) {
if (LEDblinkPWM < 7) {
if (LEDblinkPWM == 0) LEDblinkState = true;
digitalWrite(pin, LEDblinkState);
LEDblinkPeriod = 25;
LEDblinkState = !LEDblinkState;
LEDblinkPWM++;
} else {
digitalWrite(pin, LOW);
LEDblinkPWM = 0;
LEDblinkPeriod = 1000;
}
LEDlastMode = mode;
LEDblinkLast = millis();
}
} // mode
} // blinkLEDnoDelay()
void blinkERR(byte ledPIN){
// S-O-S
const uint8_t S = 150;
const uint16_t O = 300;
for(uint8_t i = 3; i>0; i--){
digitalWrite(ledPIN, HIGH);
delay(S);
digitalWrite(ledPIN, LOW);
delay(S);
}
delay(200);
for(uint8_t i = 3; i>0; i--){
digitalWrite(ledPIN, HIGH);
delay(O);
digitalWrite(ledPIN, LOW);
delay(O);
}
delay(200);
for(uint8_t i = 3; i>0; i--){
digitalWrite(ledPIN, HIGH);
delay(S);
digitalWrite(ledPIN, LOW);
delay(S);
}
delay(200);
} // blinkERR()
/////////////////////////////////////////////////////////////////////////
// digital inputs
// Bundle all of the digital input data into a structure.
const byte DI_COUNT = 4;
// initialize DI_DEFAULT_STATE LOW if pulldown resistor, HIGH if pullup resistor.
// Must use the same LOW / HIGH (pullup / pulldown) for all digital inputs monitored.
// timer_interval and timer_last are used for debounce.
const byte DI_DEFAULT_STATE = HIGH;
struct digital_inputs_t {
uint8_t pin;
uint8_t state;
uint8_t last_state;
uint32_t timer_interval;
uint32_t timer_last;
boolean alarm;
uint32_t state_change_count;
};
digital_inputs_t arr_di[DI_COUNT];
void ProcessDigitalInputs() {
// Publish the arr_di[i].state_change_count ONLY when the change in state is
// from DI_DEFAULT_STATE to !DI_DEFAULT_STATE.
// Look for a change in state (HIGH/LOW) for the digital inputs referenced by arr_di.
// If the change in state is from DI_DEFAULT_STATE to !DI_DEFAULT_STATE, and
// arr_di[i].alarm == false, then increment arr_di[i].state_change_count and
// publish the change (arr_di[i].alarm = true).
for (uint8_t i=0; i<DI_COUNT; i++) {
if (arr_di[i].timer_last > millis()) arr_di[i].timer_last = millis();
arr_di[i].state = digitalRead(arr_di[i].pin);
if (arr_di[i].state != arr_di[i].last_state && millis() - arr_di[i].timer_last > arr_di[i].timer_interval) {
// Change in state for arr_di[i].pin detected.
if (arr_di[i].state != DI_DEFAULT_STATE && arr_di[i].alarm == false) {
led_mode = 3; // 3 = slow burst every 1 second
arr_di[i].state_change_count++; // Only count the change from DI_DEFAULT_STATE to !DI_DEFAULT_STATE. (HIGH = bilge switch off) to LOW = bilge switch on).
arr_di[i].alarm = true; // Causes the arr_di[i].state_change_count to be published by publishTimer()
}
arr_di[i].last_state = arr_di[i].state;
arr_di[i].timer_last = millis();
}
} // for
} // ProcessDigitalInputs()
////////////////////////////////////////////////////////////////
// Adafruit GPS FeatherWing
// https://www.adafruit.com/product/3133
// https://learn.adafruit.com/adafruit-ultimate-gps-featherwing?view=all
// Install library "Adafruit_GPS" from the Particle cloud.
#include <Adafruit_GPS.h>
// Serial1 is also UART1 on the Boron/Argon/Xenon pins D10/Rx and D9/Tx
#define GPSSerial Serial1
// Connect to the GPS on the hardware port
Adafruit_GPS GPS(&GPSSerial);
// Set GPSECHO to 'false' to turn off echoing the GPS data to the Serial console
// Set to 'true' if you want to debug and listen to the raw GPS sentences
#define GPSECHO false
/////////////////////////////////////////////////////////////////////////
// Particle publish to webhook / Blynk only when something happens.
const uint32_t TIMER_INTERVAL_MS = 300000; // Used to limit the frequency of publishing. Use 5 min or 300000 ms
uint32_t last_publish_ms = 0;
float lat = 0.0;
float lon = 0.0;
float knots = 0.0;
float vdc_batt = 0.0;
bool publish_to_blynk = false;
uint8_t loc = 0; // 1 when the GPS position of the boat has changed by more than 122 m or 400 ft.
void publishTimer() {
if (last_publish_ms > millis()) last_publish_ms = millis();
if (millis() - last_publish_ms >= TIMER_INTERVAL_MS) {
// Publish to Blynk if any arr_di[i].alarm == true, on first restart,
// or if the boat's position has changed by more than 122 m or 400 ft.
publish_to_blynk = false;
// Determine if any DI has alarm = true
for (int i=0; i<DI_COUNT; i++) {
if (arr_di[i].alarm == true) {
publish_to_blynk = true;
}
} // for
uint32_t V0 = arr_di[0].state_change_count; // fwd bilge pump
uint32_t V1 = arr_di[1].state_change_count; // mid bilge pump
uint32_t V2 = arr_di[2].state_change_count; // aft bilge pump
uint32_t V3 = 0; // engine running; 1 = true, 0 = false
if (arr_di[3].state != DI_DEFAULT_STATE) {
V3 = 1;
}
loc = 0; // location has not changed (default).
if (GPS.fix) {
Serial.printlnf("GPS UTC %4d-%02d-%02dT%02d:%02d:%02d%+05d, fix qual %d, sat %d, lat %0.5f, lon %0.5f, knots %0.1f, %0.1f deg, altitude %0.1f m, mag var %0.1f deg", GPS.year+2000, GPS.month, GPS.day, GPS.hour, GPS.minute, GPS.seconds, 0, GPS.fixquality, GPS.satellites, GPS.latitudeDegrees, GPS.longitudeDegrees, GPS.speed, GPS.altitude, GPS.angle, GPS.magvariation);
if ((int)lat == 0 || (int)lon == 0) {
lat = GPS.latitudeDegrees;
lon = GPS.longitudeDegrees;
knots = GPS.speed;
}
// Simulate location change
//lat = lat + 0.0012; // more than 400 ft
//Serial.printlnf("Latitude/Longitude: %f, %f", lat, lon);
// FIX: 0 means no 'valid fix', 1 means 'normal precision', and 2 means the position data is further corrected by some differential system. Typically 1 with internal antenna, 2 with external antenna.
// Sat: => 4 typical, but over time in home varies from 4 to 6 with internal antenna. With external antenna, typically 8 to 10.
// Note that knots will float at 0.1 to 1.2 when the GPS is at rest with the internal antenna. With external antenna, value is 0.0 to 0.01 knots.
// Lat/Lon in decimal degrees to 3 decimal places is 111 m / 364 ft, to 4 places = 11.1 m or 36.4 ft.
// A change in GPS latitude/latitude of 0.0011 is a distance of 122 m or 400 ft (a boat swinging around an anchor with 200 ft of anchor rode out).
if (GPS.fixquality > 0 && (fabs(fabs(GPS.latitudeDegrees) - fabs(lat)) > 0.0011 || fabs(fabs(GPS.longitudeDegrees) - fabs(lon)) > 0.0011)) {
double delta_lat_m = fabs(fabs(GPS.latitudeDegrees) - fabs(lat))*10000.0/90.0*1000.0; // distance in m
double delta_lon_m = fabs(fabs(GPS.longitudeDegrees) - fabs(lon))*10000.0/90.0*1000.0;
double delta_lat_ft = fabs(fabs(GPS.latitudeDegrees) - fabs(lat))*10000.0/90.0*3280.4; // distance in ft
double delta_lon_ft = fabs(fabs(GPS.longitudeDegrees) - fabs(lon))*10000.0/90.0*3280.4;
double delta_m = max(delta_lat_m, delta_lon_m);
double delta_ft = max(delta_lat_ft, delta_lon_ft);
if (digitalRead(arr_di[3].pin) == DI_DEFAULT_STATE) {
// The engine is not running & the boat is moving.. Anchor drag!
publish_to_blynk = true;
loc = 1;
Serial.printlnf("The boat has moved a distance of %f m or %f ft since the last time the GPS position was reported, and the engine is NOT running", delta_m, delta_ft);
} else {
Serial.printlnf("The boat has moved a distance of %f m or %f ft since the last time the GPS position was reported, and the engine is running", delta_m, delta_ft);
}
}
if (GPS.fixquality > 0 && just_started == true) {
publish_to_blynk = true;
just_started = false;
}
lat = GPS.latitudeDegrees;
lon = GPS.longitudeDegrees;
knots = GPS.speed;
if (led_mode != 3) led_mode = 0;
} else {
led_mode = 4; // 4 = fast burst every 1 second because no GPS fix.
} // GPS.Fix
if (publish_to_blynk == true) {
//for(uint8_t i = 10; i>0; i--) { digitalWrite(D7, HIGH); delay(10); digitalWrite(D7, LOW); delay(10); }
char data[125]; // See serial output for the actual size in bytes.
snprintf(data, sizeof(data), "{\"V0\":%lu,\"V1\":%lu,\"V2\":%lu,\"V3\":%lu,\"lat\":%f,\"lon\":%f,\"knots\":%f,\"loc\":%u}", V0, V1, V2, V3, lat, lon, knots, loc);
Serial.printlnf("Sending to Blynk: '%s' with size of %u bytes", data, strlen(data));
bool pub_result = Particle.publish("KnotWorkin", data, PRIVATE);
if (pub_result) {
for (int i=0; i<DI_COUNT; i++) {
arr_di[i].alarm = false;
}
publish_to_blynk = false;
if (led_mode != 4) led_mode = 0;
} else {
Serial.println("ERROR: Particle.publish()");
blinkERR(D7);
}
last_publish_ms = millis(); // Limit publish frequency to limit data usage.
} else {
last_publish_ms = millis() - 2000; // Don't check if publish required too frequently.
} // publish_to_blynk
}
} // publishTimer()
void setup() {
pinMode(D7, OUTPUT);
digitalWrite(D7, HIGH);
Serial.begin(9600);
waitFor(Serial.isConnected, 30000);
delay(1000);
Serial.printlnf("Device OS v%s", System.version().c_str());
Serial.printlnf("Free RAM %lu bytes", System.freeMemory());
Serial.printlnf("Firmware version v%s", firmware_version);
// Serial1 on Argon/Boron is the main UART serial the GPS FeatherWing is connected to.
// Below is for testing the GPS only.
//Serial1.begin(9600);
//delay(5000);
// start Adafruit GPS FeatherWing
// Note: If the GPS FeatherWing is not attached, the code continues.
GPS.begin(9600);
// Turn on RMC (recommended minimum) and GGA (fix data) including altitude
GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
// Set the update rate
GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ); // 1 Hz update rate
// Request updates on antenna status, comment out to keep quiet
//GPS.sendCommand(PGCMD_ANTENNA);
delay(1000);
// Ask for firmware version
//GPSSerial.println(PMTK_Q_RELEASE);
// end Adafruit GPS FeatherWing
// Initialize arr_di
arr_di[0].pin = D8; // DI bilge fwd
arr_di[1].pin = D6; // DI bilge mid
arr_di[2].pin = D5; // DI bilge aft
arr_di[3].pin = D4; // DI eng run
for (int i=0; i<DI_COUNT; i++) {
if (DI_DEFAULT_STATE == LOW) {
//pinMode(arr_di[i].pin, INPUT_PULLDOWN);
pinMode(arr_di[i].pin, INPUT);
arr_di[i].state = LOW;
arr_di[i].last_state = LOW;
} else {
//pinMode(arr_di[i].pin, INPUT_PULLUP);
pinMode(arr_di[i].pin, INPUT);
arr_di[i].state = HIGH;
arr_di[i].last_state = HIGH;
}
arr_di[i].timer_interval = 50; // debounce timer 100 ms
arr_di[i].timer_last = millis();
arr_di[i].alarm = false;
arr_di[i].state_change_count = 0;
}
//last_publish_ms = millis();
randomSeed(millis());
digitalWrite(D7, LOW);
} // setup()
void loop() {
ProcessDigitalInputs();
// Below absolutely required here.
if (GPSSerial.available()) {
GPS.read();
if (GPS.parse(GPS.lastNMEA())) {
if (GPS.newNMEAreceived()) {
if (!GPS.parse(GPS.lastNMEA())) {
// sets the newNMEAreceived() flag to false
}
}
} // GPS
}
// debug GPS
// if (Serial1.available()) { char c = Serial1.read(); Serial.write(c); }
publishTimer();
blinkLEDnoDelay(D7, led_mode);
} // loop()
Click do see the Particle integration webhook
{
"token": "your Blynk 32 char device token",
"V0": "{{V0}}",
"V1": "{{V1}}",
"V2": "{{V2}}",
"V3": "{{V3}}",
"V4": "{{lon}},{{lat}}",
"V5": "{{knots}}",
"V6": "{{{PARTICLE_PUBLISHED_AT}}}",
"V7": "{{loc}}"
}
See the
Virtual Pin | Data Type | Comment |
---|---|---|
V0 | Integer | Forward bilge pump on/off count |
V1 | Integer | Mid bilge pump on/off count |
V2 | Integer | Aft bilge pump on/off count |
V3 | Integer | Engine running (1 = yes, 0 = no) count |
V4 | Location | GPS latitude & longitude |
V5 | Double | Boat speed [knots] (from GPS) |
V6 | String | Last date/time data was published |
V7 | Integer | Boat moving (position changed by more than 122 m / 400 ft) since last published data. |
An image of the Blynk app is shown below. The count of times the forward, mid, and aft bilge pumps operate is a more robust approach to monitoring their activity. The 'Eng Run' and 'Anchor Drag' widgets are latching pushbutton switches that display the state of each, and the switch function allows the user to easily reset the Blynk datastream.
Do you need help developing or customizing a IoT product for your needs? Send me an email requesting a free one hour phone / web share consultation.
The information presented on this website is for the author's use only. Use of this information by anyone other than the author is offered as guidelines and non-professional advice only. No liability is assumed by the author or this web site.