/* This is barebones satelite telemetry code to be used as a starter for a remote sensing project. This version incorporates code for flow sensing and autosampler triggering This code is licensed under Creative Commons, but please acknowledge Hans Huth and Sean Keane if you use/modify this is in your own projects ;) A special thanks to Mikal Hart for authoring the IridiumSBD library: http://arduiniana.org/libraries/iridiumsbd/ Also, thanks to Adafruit for the excellent documentation supporting their products. RockBLOCK modem: https://www.sparkfun.com/products/13745 Background: Initial code prepared by Hans Huth for temperature/humidity monitoring at Three Brothers Stock Tank using an AM2315 temperature/humidity sensor https://www.adafruit.com/product/1293 See minute 9:00 https://www.youtube.com/watch?v=Hvt8sU2gckk 07/31/2018 - Initial code suffered from data drops given lack of error handling for lost signals (July, 2017). For further details, see minute 14:28 here: https://www.youtube.com/watch?v=Hvt8sU2gckk 12/17/18 - code was improved by Sean Keane with adaptive retry to avoid data loss AND also incoporates autosampler - currently deployed and working 12/27/18 - barebones_ROCKBLOCK_REV3B - additional comments and adaptive retry code simplified by Hans Huth 01/09/19 - barebones_ROCKBLOCK_REV3C - additional comments/updates from meeting with Sean on 01/08/19 by Hans Huth - tested successfully 01/09/19 - 01/10/19 barebones_ROCLBLOCK_REV3D - this is REV3C ammended with code from Sean's ROCKBLOCK_AUTOSAMPLER_BAREBONES and reflecting our conversations 01/13/19 barebones_ROCKBLOCK_REV3E - sleepInterval and associated comments removed from 3D - not useful at this time Also got rid of changeCond since it really didn't seem to do anything, and #define value since not used and reserved word anyway Also changed "#define read 3" to read as "#define intPIN" since "read" is a reserved word; replaced "read" with "intPIN" throughout code. intPIN is the interrupt pin for whatever board I am working with. loopStartTime now a global variable rather than being defined at start of loop. set byte progPinStatus = 1 in global declaration 01/14/19 barebones_ROCKBLOCK_REV3F - need to rename variables and reverse logic where appropriate to help staff trace code more easily for future mods (i.e. I can't have AllDone = 1 meaning program is still waiting - rename as progWaiting so logic makes sense) INTERVAL now telemInterval; Reading_Rate now pollingInterval; AllDone now progWaiting; (Addresses reverse logic on pin where HIGH means program still waiting to trigger) complete now progPinStatus; compCode now completeCode; Setup variable logic is now reversed (Don't set variable Setup to 0 if on first run, now is 1) Changed ProgramStarted to progStarted Changed progStarted to byte rather than boolean to avoid issues on URL value (I want 1 or 0 vs. true or false posted to web) Also, this will ensure I'm consistant in conditionsls (using 0,1 throughout instead of 0,1, coupled with true, false in same conditional) 02/23/19 barebones_ROCKBLOCK_REV3G - based on initial tests posted here: https://thingspeak.com/channels/663532 I moved programComplete() function from first action in loop() to the while loop responsible for checking things during polling interval. This ensure we break out and post immediately rather than waiting for polling interval to pass once program completes. */ // Libraries #include #include // Rockblock settings #define STATUS 13 // onboard pin to determine if modem engaged. #define ROCKBLOCK_RX_PIN A2 // Recieve data pin from Rockblock (seial data from RockBLOCK) #define ROCKBLOCK_TX_PIN A3 // Transmit data pin to Rockblock (serial data to RockBLOCK) #define ROCKBLOCK_SLEEP_PIN 4 // on/off pin for power savings #define ROCKBLOCK_BAUD 19200 // serial modem communication baud rate #define CONSOLE_BAUD 115200 // serial terminal communications baud rate #define DIAGNOSTICS true // Set "true" to see serial diagnostics // Timer Variables unsigned long resetLoopTime = 0; // Used to reset the loop timer back to 0 for comparison with telemInterval unsigned long loopStartTime = 0; // Used to register start time at top of loop() unsigned long telemInterval = 3600000; // Telemetry reporting interval (where 1000 = 1 second; 10 minutes) : 1 hour unsigned long pollingInterval = 300000; // loop() polling interval: 5 minutes // Modem communication SoftwareSerial ssIridium(ROCKBLOCK_RX_PIN, ROCKBLOCK_TX_PIN); // Type Arduino Stream IridiumSBD isbd(ssIridium, ROCKBLOCK_SLEEP_PIN); // this is my RockBLOCK // String and character array for posting to RockBLOCK server String myUrl; // String where all fields will be captured and posted char url[256]; // for passing to modem stream char url_old[256]; // If telemetry failed, this array stores the old value for telemetry retry // Message Retrival Variables uint8_t buffer[200]; // buffer for saving messages. size_t bufferSize = sizeof(buffer); // Determines the size of the buffer. This gets set to 0 if no message is recieved. // or if the message takes to long to download. // Variables for battery voltage long myVcc = 0.00; String myVolts = ""; // Variables to determine message status bool timeout = false; // If telemetry times out, this becomes true. int err; // Error code status for Iridium processes. long varChange; // Variable for saving recieved message data int n = 25; // Variable for setting adjustSendReceivetimeout (how long a message sending attempt will last) // NEW: Auto Sampler Pins and Variables // Autosampler wire color meaning :(Blue -> GND) (Green -> Program Completed) (Black -> Program Triggered) #define ProgComp 10 // Attach to green wire on half cable connected to autosampler. // AutoSampler status pin (green wire) will be pulled low // for 90 seconds after the program cycle is complete. // This will change back to 1 (HIGH) after 90 seconds expired. #define ProgramTrigger 9 // Attach to black wire which talks to the auxiliary input pin on autosampler // This pin will be set HIGH by default, and pulled LOW to trigger. #define intPIN 3 // Trinket interrupt pin is 3 - change depending on board you are using byte Setup = 1; // Allows the loop to run immediately for setup. byte progPinStatus = 1; // Status of autosampler pin on green wire to determine if program completed (0) or not (1). byte progWaiting = 1; // progWaiting becomes 0 (false) when progPinStatus pulled low (happens when AS program completes). // in this sketch, this will never be changed back to 1 until board is reset. // This variable is important since green pin can go back to 1 after 90 seconds. volatile byte progStarted = 0; // Variable to determine if the program was started due to flow - set in interrupt // field codes for posting to server String startCode; // string for adding trigger value to myURL (same as progStarted: 1 means started) String completeCode; // string for adding progPinStatus code to myURL (same as progWaiting: 0 means no longer waiting) // ****** END Library and global variable declarations void setup() { // Start the serial port at 115200 baud rate so that I can // see output on the a terminal from modem initiation Serial.begin(CONSOLE_BAUD); delay(1000); Serial.println(F("Begin REM setup.")); // LED on 13 for reporting status on modem communication pinMode(STATUS, OUTPUT); // Sleep pin on modem pinMode(ROCKBLOCK_SLEEP_PIN, OUTPUT); // NEW CODE for autosampler update // Auto Sampler Pins // Auto Sampler Pins pinMode(ProgramTrigger, OUTPUT); // Pin for triggering autosampler via the black wire on the half cable pinMode(ProgComp, INPUT); // Pin for detecting autosampler program status via the green wire on the half cable digitalWrite(ProgramTrigger,HIGH); // When we set this pin low, the autosampler will begin its program. //Interrupt pin setup. If the sensor signal rises, start the autosampler program via programBegin() attachInterrupt(digitalPinToInterrupt(intPIN), programBegin, RISING); // END NEW CODE // Setup the RockBLOCK isbd.adjustSendReceiveTimeout(n); // This determines how long RockBLOCK will try to send message before timeout. // n = 25 seconds per attempt before system times out as per global variable setting. // If transmission times out, will delay according to setPowerProfile() so that // modem power (capacitor) has an opportunity to recharge. isbd.setPowerProfile(1); // Use high power (0) when running off high-capacity lipo. // Currently set to (1) for testing / development via laptop power. /* PowerProfile() background: The RockBLOCK module uses a “super capacitor” to supply power to the Iridium 9602. As the capacitor is depleted through repeated transmission retries, the host device’s power bus replenishes it. Under certain low power conditions it is important that the library not retry too quickly, as this can drain the capacitor and render the 9602 inoperative. Available power settings: isbd.setPowerProfile(1); for USB "low current" applications; 90 mA; 60 secs between transmit retries isbd.setPowerProfile(0); for battery "high current" applications; 90 mA; 20 secs between transmit retries */ Serial.println(F("REM setup complete.")); } // END Setup() void loop() { Serial.println(F("In Loop")); delay(500); if(Setup == 1) { // Unit was just powered on Serial.println(F("REM was just powered on.")); Serial.println(F("Modem will test connection.")); } delay(500); loopStartTime = millis(); // Setting loop start time. // millis() returns the number of milliseconds since the start of current program. // Note that the interrupt function sets progStarted = 1. if (progStarted == 1 || ((loopStartTime-resetLoopTime) >= telemInterval) || progWaiting == 0 || Setup == 1) { // If I made it here: // Program has just been started (progStarted == 1) OR // telemetry interval has been exceeded (loopStartTime-resetLoopTime) >= telemInterval) AND/OR // autosampler triggered and program has been completed (progWaiting == 0) AND/OR // this is my first run (Setup == 1) resetLoopTime = loopStartTime; // reset the timer Serial.println(F("Monitoring interval exceeded")); Serial.println(F("AND/OR autosampler was triggered")); Serial.println(F("AND/OR program was completed,")); Serial.println(F("AND/OR just powered on. As such,")); Serial.println(F("we are ready to transmit.")); delay(1000); if(timeout == true) // "timeout" is updated in adaptiveRetry(); // If I'm here, then last telemetry attempt failed. // In response, increase the timeout interval. { n +=25; // add 25 seconds to timeout if(n >= 300){ // limit n to 300 seconds or 5 mins n = 300; } isbd.adjustSendReceiveTimeout(n); } Serial.println(F("Start Transmission Sequence")); // Start the serial port ssIridium.begin(ROCKBLOCK_BAUD); // Start talking to RockBLOCK Serial.println(F("Connecting to RockBLOCK...")); ssIridium.listen(); Serial.println(F("Waking RockBlock.")); Serial.println(F("This may take a minute.")); if (isbd.begin() == ISBD_SUCCESS) { // Awoken okay, so try sending a text digitalWrite(STATUS, HIGH); // modem awoke okay- set led on pin 13 high for visual confirmation // isbd.begin() causes following be set true. // To prevent lockout, set to false. isbd.useMSSTMWorkaround(false); Serial.print(F("RockBlock Turned on")); // Call Sensor Functions myVcc = readVcc(); // returns REM battery voltage // Send variables to String myVolts = String(myVcc); // NEW CODE // updating startCode and completeCode variables for transmitting startCode = String(progStarted); // progStarted will be 0 or 1 stored in the String startCode. // Sean's logic is okay here: 0 = false, 1 = true completeCode = String(progWaiting); // Build post string - NEW CODE myUrl = "," + myVolts + "," + completeCode + "," + startCode + ",0"; // 0 required as terminator byte for RockBLOCK servers Serial.println(F("The String is:")); Serial.println(myUrl); Serial.println(F("Volts, Complete Code, Start Code")); myUrl.toCharArray(url,256); // Convert URL to character array. if(timeout == true){ // Last adaptiveRetry() failed, so call function to upload pending data messageRetry(); /* Note that whenever a message is sent, the modem tries to accept any incoming messages. If no messages are avaliable, buffer will remain unchanged and bufferSize will be set to 0. Since we plan on receiving messages, messageRetry() and adaptiveRetry() both try to handle any incoming messages that start with a "#" (ascii value 35) and end with a "!" (ascii value of 33). For example #time120000! will change telemInterval to 120000 miliseconds. When attempting to send a message, modem also checks for messages in the inbox. If a message was recieved, modem will convert it and save the command. */ } timeout = false; // Reset timeout variable. // Next, let's send the most recent data Serial.println(F("Update channel with latest field data")); adaptiveRetry(); // Function for sending/receiving data to server including retries. // Status will be tracked via "timeout" set in adaptiveRetry(); if (timeout == true) { Serial.println(F("Message timed out;")); Serial.println(F("saving message")); Serial.println(F("for retry later.")); delay(5000); memcpy(url_old, url, 256); // Copies unsent message for retry next loop. // This erases old url_old, if one was saved. } else { // timeout is false, so inform message was sent Serial.println(F("Message Sent")); } Serial.println(F("Powering down modem")); isbd.sleep(); ssIridium.end(); digitalWrite(STATUS, LOW); // modem asleep } // end conditional "if (isbd.begin() == ISBD_SUCCESS)" else{ Serial.println(F("Modem not powering on;")); Serial.println(F("check connections!")); } // NEW CODE if(startCode.toInt() == 1 && timeout == false){ // Message was sent with progStarted code as 0 (program initiated). // So, the autosampler program has begun and we alerted the handler. progStarted = 0; // Change back to false since we already know the program has started. // Our loop() will start again when the completeCode is 0. } if(completeCode.toInt() == 0 && timeout == false){ // progPinStatus code is 0 (autosampler program completed) and message was sent (timeout == false) // loop() will run until this is satisfied. delay(telemInterval); // Delay for a day before restarting loop // When loop() restarts after delay, progWaiting will still be 0 so a message // will be sent immediately. This enables a reminder to be sent daily until // the program is reset. } // NEW CODE END // Report out on the first loop run if (Setup == 1 && timeout == false) { Serial.println(F("Setup Test message sent.")); Serial.println(F("Setup complete.")); delay(15000); Setup = 0; // setup is complete - global variable set to 1 } else if (Setup == 1 && timeout == true) { // If the first message wasn't sent due to timeout, "Setup" will still be set to one. // As such, the program will restart at the top of the loop and again try to send a message // as per first conditional. Let the user know status before doing so and make a recommendation. Serial.println(F("Couldn't send message;")); Serial.println(F("Shut it down and try moving antennae.")); delay(30000); // buys me time to unplug Arduino or modem and reposition } // All done! /* Again, if I am here here, either * Autosampler program is complete (progWaiting = 0) * or the reporting interval has been exceeded, * or the autosampler program is all complete */ } // end conditional: // "if (progStarted == 1 || ((loopStartTime-resetLoopTime) >= telemInterval) || progWaiting == 0 || Setup == 1)" else { /* * This is not my first loop (Setup was set to 0), * or autosampler program hasn't started or completed, * or I am still waiting for telemetry interval to expire. */ Serial.println(F("Telemetry interval hasn't yet passed.")); unsigned long WaitForCondition = loopStartTime; // Reset waiting for condition timer // The following while loop will delay on polling interval unless // : progStarted == 1 from interrupt triggering, OR // : progWaiting == 0 from AS program completing // If neither condition happens during polling interval, sketch will just return to top of loop while((loopStartTime - WaitForCondition) <= pollingInterval) { // Delay next loop() until polling interval is exceeded loopStartTime = millis(); programComplete(); // Poll autosampler complete pin if (progStarted == 1 || progWaiting == 0) { break; // This will ensure immediate message sent after an interrupt event or AS program condition change instead of delaying on reporting interval. // If polling once per hour, this is a big deal since I dont want to wait for polling interval to expire before doing something. delay(30000); // rest for 30 seconds } } } } // END loop() // Add Sensor and other functions here // Always include function for reading/reporting battery voltage long readVcc() { // Function provided by: // https://provideyourown.com/2012/secret-arduino-voltmeter-measure-battery-voltage/ // Read 1.1V reference against AVcc // set the reference to Vcc and the measurement to the internal 1.1V reference #if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1); #elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__) ADMUX = _BV(MUX5) | _BV(MUX0); #elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) ADMUX = _BV(MUX3) | _BV(MUX2); #else ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1); #endif delay(2); // Wait for Vref to settle ADCSRA |= _BV(ADSC); // Start conversion while (bit_is_set(ADCSRA,ADSC)); // measuring uint8_t low = ADCL; // must read ADCL first - it then locks ADCH uint8_t high = ADCH; // unlocks both long result = (high<<8) | low; result = 1125300L / result; // Calculate Vcc (in mV); 1125300 = 1.1*1023*1000 return result; // Vcc in millivolts } // Autosampler / sensor code void programBegin() { // Presence of fluid given change of state on interrupt pin - funciton called! Serial.println(F(", flow detected - triggering autosampler")); // pulse trigger pin connected to pulse pin on autosampler. digitalWrite(ProgramTrigger,LOW); // assuming working with HACH sampler; may need to pull high if another sampler. progStarted = 1; // variable to alert program that the auto sampler is getting triggered detachInterrupt(digitalPinToInterrupt(intPIN)); // Once we made it here once were no longer need the interrupt pin. // Detaching it will prevent it from cycling. } void programComplete() // Determine if Autosampler program is complete { progPinStatus = digitalRead(ProgComp); // AutoSampler status pin (green wire) will be pulled low // for 90 seconds after the program cycle is complete. // This will change back to 1 (HIGH) after 90 seconds expired. if (progPinStatus == 0) // status of autosampler pin on green wire { progWaiting = 0; // If the autosampler finishes it program while the RockBlock was doing other things, // note it so we can alert the handler. Once this is changed to 0 it is never changed // back to 1 until the autosampler and Arduino can be reset. } } // ****** RockBLOCK modem functions void adaptiveRetry() { // When a new message is sent, the modem attempts to recieve a message. // If one is available, it gets saved to buffer and the size gets saved to buffersize. // buffersize has shown to be unreliable: /* A message might send without a 0 code, so another sendRecieveSBD session is started. In this case, buffersize gets set to 0 even though buffer has a messaged saved in it. To counteract this issue, all messages sent must start with a "#" and end with a "!" or the program will not register it. */ memset(buffer, 0, sizeof(buffer)); // Reset the buffer before a new message comes in // or it will be added to the next open space in the buffer. // Details regarding this function for handling bad transmissions are avaialble here: // https://docs.rockblock.rock7.com/docs/adaptive-retry for(int i=0;i<5;i++) { Serial.print(F("Attempt# ")); Serial.print(i+1); err = isbd.sendReceiveSBDText(url,buffer,bufferSize); Serial.print(F("Error Code = ")); Serial.println(err); // check status of sent message if (err != 0) { // Latest attempt didn't work so echo status Serial.println(F("sendSBDText failed.")); Serial.println(F("Delaying before retry.")); // Try again after specified delay if(i==0) delay(random(0, 5000)); // 1st failure- trying second after random time 0-5 seconds else if(i<=2) delay(random(0, 30000)); // 2nd, 3rd failure- trying third, fourth after random time 0-30 seconds else if(i==3) delay(random(120000, 300000)); // 4th failure- trying fifth after random time between 120-300 seconds else if(i==4) { // If I'm here, the first five tries failed timeout = true; Serial.print(i+1); Serial.println(F(" attempts failed;")); Serial.println(F("message not sent.")); break; } } else // Message transmitted during first else, so // notificaiton will be provided in main loop. break; } // for loop closed // Since buffer would have been changed above if a message was recieved, // check that the first character was a # (Ascii value = 35). if(buffer[0]==35){ handleMessage(); // for incoming messages } } void messageRetry(){ memset(buffer, 0, sizeof(buffer)); // Same as above reset the buffer before receiving a message. // If I'm here, timeout was set to true in prior iteration given failure after multiple tries, // so let's attempt to resend previous unsent post. Serial.println(F("Attempting to send old message")); //Attempt to download any message while sending. err = isbd.sendReceiveSBDText(url_old,buffer,bufferSize); if (err != 0) { Serial.println(F("Old message not sent")); Serial.println(F("trying one last time")); isbd.sendReceiveSBDText(url_old,buffer,bufferSize); delay(15000); // Allow 15 seconds before thingspeak posts. } else { // Old message sent succesfully delay(15000); // Allow 15 seconds between thingspeak posts } if(buffer[0] == 35){ //Same as before - if the first character was a #, then try to handle the message. handleMessage(); } } void handleMessage(){ /* From Sean: handleMessage() works by taking the buffer that the inbound message was saved in and initally reading the first character's ASCII value. If the value is 35 which is a '#' symbol the program will try to handle the message. The next four ASCII values are added together to form a command code. All the subsequent characters are read (up untill the '!' or ASCII value 33) and they are turned into one value and saved into varChange. Using the unique command code a global variable is chosen and the value of varChange is saved inside to be used in the next loop cycles. Example: to change global variable for reporting interval (telemInterval) send in message #time600000! . The first four ASCII values, after the #, for time add up to 116 + 105 + 109 + 101 = 431 = 't' + 'i' + 'm' + 'e'. The subsequent values [6,0,0,0,0,0] get converted from their ASCII values and saved as one number '600000'. Finally the ! tells the function to stop reading. This then changes the global Variable telemInterval to one hour (telemInterval = 600000). We dont use bufferSize to initiate this function because it can be saved wrong for two reasons. One is a message might send with a error code other than 0. This causes another message send cycle to be started and buffersize will then be 0 because the message was already downloaded. Also if downloading the message takes too long the library will cut the function short before it can save buffersize to the correct value. This means buffer can be changed while bufferSize stays 0. This makes the initator and terminator characters (#,! respectfully) are necessary to recieve messages reliably. */ long varChange = 0; // variable to change values inside program int commandCode = 0; // variable to determine which command was recieved Serial.println(F("IN HANDLE MESSAGE checking for ! as terminator")); bool OkToProceed = false; // Is there a ! in the message? for(byte j=0; j<200; ++j){ // Check all the characters in the buffer untill we reach a '!', which has a ASCII value of 33. int scan = buffer[j]; if(scan == 33){ OkToProceed = true; // If im here we detected a ! in the message and we can proceed. break; // So break out of this for loop. } } if(OkToProceed == true){ // If no message was recieved buffer size is 0 and buffer is empty. // if message is succesfully downloaded, convert to desired format // Debugging code to read message through serial terminal Serial.println(F("Message received!")); Serial.print(F("Inbound message size is ")); Serial.println(bufferSize); for (byte i=0; i