// Modified from ROCKBLOCK_MENGES_REV5 and barebones_ROCKBLOCK_REV3G /* RockBLOCK modem code for use with and Adafruit Trinket (Arduino clone) https://www.sparkfun.com/products/13745 https://www.adafruit.com/product/2010 https://www.adafruit.com/product/1137 https://www.adafruit.com/product/2900 Irridium SBD libraries available here: https://github.com/mikalhart/IridiumSBD/releases/tag/v1.0 Please acknowlege Hans Huth and Sean Keane if you use/modify this code */ // Libraries #include #include #include // for OLED display #include // for OLED display // Display Settings #define OLEDScanner 11 // determines power state of OLED #define I2C_ADDRESS 0x3C SSD1306AsciiAvrI2c oled; // Rockblock settings #define STATUS 13 // onboard pin to determine if modem engaged. #define ROCKBLOCK_RX_PIN A1 // Recieve data pin from Rockblock (seial data from RockBLOCK) #define ROCKBLOCK_TX_PIN A2 // 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; // Resests the loop timer back to 0 so it can be compared with telemInterval each loop unsigned long loopStartTime = 0; // Used to register start time at top of loop() unsigned long telemInterval = 1800000; // status reporting interval (24hours where 1000 = 1 second) **Can be modified remotely unsigned long pollingInterval = 60000; // (60 secs) interval that rangeFind takes readings for change detect **Can be modifed remotely SoftwareSerial ssIridium(ROCKBLOCK_RX_PIN, ROCKBLOCK_TX_PIN); // type Arduino Stream IridiumSBD isbd(ssIridium, ROCKBLOCK_SLEEP_PIN); // this is my RockBLOCK // Rangefinder pins/variables #define RangeTrig 6 // Attach to pin 4 on range sensor #define RangePin A3 // Attach to pin 3 on range sensor // Ultrasonic variables int8_t arraysize = 9; // quanitity of values for median (odd numbers) uint16_t rangevalue[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0}; // group array for 9 ultrasonic values to calculate median/mode uint16_t mode; // calculated mode distance (value that occurs most often) (cm or volts) uint16_t modeOld; // used to store old rangeFind values if needed (i.e. if there is a change) (cm or volts) int change; // used to measure change. INITALLY SET TO 10 SO THE LOOP WILL RUN WHEN INITALLY SET UP // GETS SET TO ACTUAL CHANGE IN mode AND modeOld DURING FIRST LOOP. int changeCond = 10; // Conditional change value to start transmission, can be changed through message retrival. **Can be modifed remotly String myHeight; // string for adding stage field to myUrl String myChange; // string for adding change field to myURL // 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 while ultraonic measures a new value // 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. byte Setup = 1; // Allows the loop to run immediatly for setup //Variables to modify conditions through SMS long varChange; // Variable for saving recieved message data and saving it to global variables. int n = 25; // Variable for setting adjustSendReceivetimeout or how long a message sending attempt will last. 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); Serial.println("REM Setup"); // OLED Button pins pinMode(OLEDScanner, INPUT); // LED on 13 for status on modem communication pinMode(STATUS, OUTPUT); // Sleep pin on modem pinMode(ROCKBLOCK_SLEEP_PIN, OUTPUT); // Ultrasonic pins pinMode(RangeTrig, OUTPUT); // Pin for triggering sensor pinMode(RangePin, INPUT); // Pin for reading sensor (NOTE: analog is input by default) digitalWrite(RangeTrig, LOW); // Pin on sensor is pulled high internally, // sensor will stop ranging when low (power savings) // Setup the RockBLOCK isbd.adjustSendReceiveTimeout(n); // This deterimnes how long RockBLOCK will try to send message before timeout // Here, set to 25 seconds per attempt for timeout isbd.setPowerProfile(0); // Since we are running off battery in field, set to 0 // Available power settings: // 1 = low current (90 mA USB; 60 secs between transmit retries) // 0 = high current (high current battery; 20 secs between transmit retries) rangeFind(); modeOld = mode; // Set old rangefinder value to current in the event transmission doesn't work if (digitalRead(OLEDScanner) == 1) { // Check that the OLED is on oledReset(); oled.println(F("REM SETUP:")); oled.println(F("Lets test connection")); oled.print(F("Stage Level= ")); oled.print(mode); oled.print(F(" cm")); delay(2000); // Wait seconds so the user can read the message } } void loop() { Serial.println(F("In Loop")); if (digitalRead(OLEDScanner) == 1) { //Check to see if OLED is on before sending data oledReset(); oled.print(F("Stage Level=")); oled.print(mode); oled.println(F(" cm")); if(Setup == 1) { // Setup is equal to 1 the first run through the loop. It will be set to 1 if the first message is sent oled.println(F("Sending Message to")); oled.println(F("Test Connection")); } else { // This message prints after setup is complete to remind the user to shut off the OLED before leaving oled.println(F("Power off OLED")); oled.println(F("before leaving site")); } delay(5000); // Allow the user to read this message } loopStartTime = millis(); // Setting loop start time. // millis() returns the number of milliseconds since the start of current program. rangeFind(); // Determines rangefinder value change = mode-modeOld; // Calculate change in rangeFind height based on last reading (cm) (Will run initally because modeOld is initally 0.) Serial.print("Interval is "); Serial.println(telemInterval); Serial.print("Change condition = "); Serial.println(changeCond); Serial.print("Polling Interval = "); Serial.println(pollingInterval); // Uncomment (abs(change) >= changeCond) to have message sent if changeCond is exceeded if (/*abs(change) >= changeCond ||*/ ((loopStartTime-resetLoopTime) >= telemInterval) || Setup == 1) { // If I made it here, I'm setting things up for first telemetry test // OR my interval for readings has been exceeded - time to report status // OR my change condition has been exceeded (assuming respective code has been uncommented) resetLoopTime = loopStartTime; // reset the timer if (digitalRead(OLEDScanner) == 1) { oledReset(); //Start the OLED oled.println(F("In the Loop")); oled.println(F("Gathering Data")); delay(2000); } if(timeout == true) // if message failed to send during last attempt, increase the attempt to send interval. { n +=25; // If message doesnt send add 25 seconds to timeout. if(n >= 300) { n = 300; // never allow n to get over the default value of 300 seconds or 5 mins } isbd.adjustSendReceiveTimeout(n); } Serial.println(F("Transmit")); // Conditions met - start the transmission sequence // Start the serial port ssIridium.begin(ROCKBLOCK_BAUD); // Start talking to RockBLOCK Serial.println("Beginning to talk to the RockBLOCK..."); ssIridium.listen(); if (digitalRead(OLEDScanner) == 1) { oledReset(); oled.println(F("Talking to RockBlock")); oled.println(F("This may take a min")); } // Try sending a text if awoken okay if (isbd.begin() == ISBD_SUCCESS) { digitalWrite(STATUS, HIGH); // modem awoke okay isbd.useMSSTMWorkaround(false); //isbd.begin() causes this to be set true. // To prevent lockout keep it set to false. if (digitalRead(OLEDScanner) == 1) { oledReset(); oled.print(F("RockBlock Turned on")); delay(2000); } // Monitoring functions myVcc = readVcc(); // returns REM battery voltage // varialbes to String myChange = String(change); myHeight = String(mode); // battery myVolts = String(myVcc); // build post string myUrl = "," + myVolts + "," + myHeight + "," + myChange + ",0"; // 0 required as terminator byte for RockBLOCK servers if (digitalRead(OLEDScanner) == 1) { oledReset(); oled.println(F("The String is:")); oled.println(F("volt, cm, change(cm)")); oled.println(myUrl); delay(5000); } myUrl.toCharArray(url,256); if(timeout == true) { messageRetry(); // Function for resending previous unsent message (only attempts resends one time) } /* Note whenever a message is sent the avaliable incoming messages are also downloaded. * If no message is in the inbox the buffer size (or sizeof(buffer)) = 0. * handleMessage() will only be called if a message was sucessfully downloaded. */ // 35 corresponds to # so any data sent must start with "#" (e.g. #time8640000) if(buffer[0] == 35) { // Sending messages also downloads available messages Serial.print("telemInterval = "); Serial.println(telemInterval); Serial.print("Change cond = "); Serial.println(changeCond); Serial.print("Polling Interval = "); Serial.println(pollingInterval); for(int kk=1;kk<201; kk++) { Serial.println(buffer[kk]); } handleMessage(); // Function for converting recieved messages Serial.print("telemInterval = "); Serial.println(telemInterval); Serial.print("Reading rate = "); Serial.println(pollingInterval); Serial.print("Change cond = "); Serial.println(changeCond); } // Okay - let's send the most recent data if (digitalRead(OLEDScanner) == 1) { oledReset(); oled.println(F("Trying to send")); oled.println(F("field data for a")); oled.println(F("channel update")); } timeout= false; // Reset timeout variable (if a previous timeout message was not sent do not try it again). adaptiveRetry(); // function for sending/recieving data to server including retries /* Again since a message was attempted to be sent it also checks if a message is in the inbox. * If a message was recieved the convert it and save the command. */ if(buffer[0] == 35) { Serial.print("telemInterval = "); Serial.println(telemInterval); Serial.print("Change cond = "); Serial.println(changeCond); Serial.print("Reading rate = "); Serial.println(pollingInterval); Serial.print("buffer = "); for(int kk=1;kk<201; kk++){Serial.println(buffer[kk]);} handleMessage(); Serial.print("telemInterval = "); Serial.println(telemInterval); Serial.print("Change cond = "); Serial.println(changeCond); Serial.print("Reading rate = "); Serial.println(pollingInterval); } // End incoming message handling if (timeout == true){ if (digitalRead(OLEDScanner) == 1) { oledReset(); oled.println(F("Message timeouted")); oled.println(F("saving message")); oled.println(F("for retry later")); delay(5000); } memcpy(url_old, url, 256); // Copies unsent message for retry next loop. (Erases old url_old, if one was saved). } else if (digitalRead(OLEDScanner) == 1 && timeout == false) { // Confirm message was sent oledReset(); oled.println(F("Message Sent")); oled.println(F("Powering down modem")); } // All done - go to sleep isbd.sleep(); ssIridium.end(); digitalWrite(STATUS, LOW); // modem asleep modeOld = mode; // save old rangeFind value if ((Setup == 1 && timeout == false) && digitalRead(OLEDScanner) == 1) { // If the first run through the loop is succesful, prompt user to turn off OLED and set Setup = 0 oled.clear(); oled.println(F("Message Sent")); oled.println(F("Setup complete")); oled.println(F("Turn off OLED")); oled.println(F("With on/off switch")); Setup = 0; delay(15000); } else if ((Setup == 1 && timeout == true) && digitalRead(OLEDScanner) == 1) { // If the message wasn't sent during setup, the attempt will be tried in a few seconds, but prompt user to move antenna oled.clear(); oled.println(F("Couldnt send message")); oled.println(F("Move Antenna")); oled.println(F("Restart in 30 sec")); delay(30000); } if(Setup == 1 && timeout == false) // make sure Setup is set to zero irrespective of state of OLED Setup = 0; } } else { // there hasn't been a signficant change OR I am still within timer interval Serial.println(F("Waiting for condition")); delay(pollingInterval); // check condition after polling interval if(digitalRead(OLEDScanner) == 1) { oled.clear(); oled.println(F("Waiting for condition")); delay(15000); } } } // end loop ///// FUNCTIONS ///// void rangeFind(){ int16_t pulse; // number of pulses from sensor int i=0; digitalWrite(RangeTrig, HIGH); while( i < arraysize ) { pulse = analogRead(RangePin); // read in time for pin to transition rangevalue[i]=pulse; // pulses to centimeters (use 147 for inches) if( rangevalue[i] < 725 && rangevalue[i] >= 10 ) // value in range i++; delay(10); // wait between samples } isort(rangevalue,arraysize); // sort samples mode = getMyMode(rangevalue,arraysize); // get mode digitalWrite(RangeTrig, LOW); //print_range(); //Debuggin Function } // Sorting function (Author: Bill Gentles, Nov. 12, 2010) void isort(uint16_t *a, int8_t n){ for (int i = 1; i < n; ++i) { uint16_t j = a[i]; int k; for (k = i - 1; (k >= 0) && (j < a[k]); k--) { a[k + 1] = a[k]; } a[k + 1] = j; } } // Mode function, returning the mode or median. uint16_t getMyMode(uint16_t *x,int n){ int i = 0; int count = 0; int maxCount = 0; uint16_t mode = 0; int bimodal; int prevCount = 0; while(i<(n-1)){ prevCount=count; count=0; while( x[i]==x[i+1] ) { count++; i++; } if( count > prevCount & count > maxCount) { mode=x[i]; maxCount=count; bimodal=0; } if( count == 0 ) { i++; } if( count == maxCount ) { //If the dataset has 2 or more modes. bimodal=1; } if( mode==0 || bimodal==1 ) { // Return the median if there is no mode. mode=x[(n/2)]; } return mode; } } 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++) { // consider adding OLED output here Serial.print(F("Attempt# ")); Serial.print(i+1); delay(5000); 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.")); Serial.println(F(" ")); Serial.println(F("----------------------")); Serial.println(F(" ")); // 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(){ // If I'm here, timeout was set to true in prior iteration given failure after multiple tries, // so attempt to resend previous unsent post. if (digitalRead(OLEDScanner) == 1){ oledReset(); oled.clear(); oled.println(F("Sending old message")); } //Attempt to download any message while sending. err = isbd.sendReceiveSBDText(url_old,buffer,bufferSize); if (err != 0) { if (digitalRead(OLEDScanner) == 1){ oledReset(); oled.println(F("Old message not sent")); oled.println(F("trying one last time")); } isbd.sendReceiveSBDText(url_old,buffer,bufferSize); delay(15000); // Allow 15 seconds before thingspeak posts. } else { delay(15000); } } /* * handleMessage() works by taking the buffer that the inbound message was saved in and initally reading the first four characters' * ASCII values. * These first four ASCII values are added together to form a command code. All the subsequent characters are read * 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 for time add up to 116 + 105 + 109 + 101 = 431. * The subsequent values [6,0,0,0,0,0] get converted from their ASCII values and saved as one number '600000'. This then changes the * global Variable telemInterval to one hour (telemInterval = 600000); */ void handleMessage(){ long varChange = 0; // variable to change values inside program int commandCode = 0; // variable to determine which command was recieved // If no message was recieved buffer size is 0 // 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<(byte)bufferSize; ++i) { Serial.print(F("Decimal = ")); Serial.println(buffer[i],DEC); // displays message in decimal (ASCII) format Serial.print(F("HEX = ")); Serial.println(buffer[i], HEX); // displays message in hex format Serial.print(F("Actual Message = ")); if (isprint(buffer[i])) { Serial.print(("(")); Serial.write(buffer[i]); Serial.println((")")); Serial.print(("(")); Serial.print(buffer[i]); Serial.print((")")); } Serial.println((" ")); } Serial.println(); // data handling to store recieved data in variable if((byte)bufferSize!=0){ for(byte i=1; i<5; ++i) { // adds first four ASCII values together to form command code commandCode += buffer[i]; oled.print(F("Command Code =")); Serial.print("command code ="); Serial.println(commandCode); } for(byte i=5; i<(byte)bufferSize; ++i) { // adds subsequent characters together to get varChange value. byte ASCII_CONVERSION = buffer[i] - 48; // Convert ASCII numbers to acutal numbers varChange = varChange *10 + ASCII_CONVERSION; // Algorithm to get individual numbers as one value. oled.print(F("Variable Change = ")); Serial.print("VarChange = "); Serial.println(varChange); } } // Determine which code was sent and change respective variable. if(commandCode == 431) { // time: sum of ASCII values = 431 Serial.print("right before interval is changed interval = "); Serial.println(telemInterval); telemInterval = varChange; // Change the conditional interval time to send a message Serial.print("in interval change new interval = "); Serial.println(telemInterval); } if(commandCode == 416) { // chng: sum of ASCII values = 416 // Must send in value 10x desired value (ex. sent: 10 actual 1.0) Serial.print("change condition is "); Serial.println(changeCond); changeCond = varChange / 10.0; // Change the conditional change value to send a message Serial.print("change condition was just changed to "); Serial.println(changeCond); } if(commandCode == 430) { // rari: sum of ASCII values = 430 pollingInterval = varChange; // Change the rate the rangeFind takes readings } if(commandCode == 418) { // isbd: sum of ASCII values = 418 n = varChange; isbd.adjustSendReceiveTimeout(n); // Adjust how long RockBLOCK will try to send message before timeout } } void oledReset() { oled.begin(&Adafruit128x32, I2C_ADDRESS); oled.setFont(System5x7); oled.clear(); oled.setCursor(0,0); } 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 } // diagnostic functions #if DIAGNOSTICS void ISBDConsoleCallback(IridiumSBD *device, char c) { Serial.write(c); } void ISBDDiagsCallback(IridiumSBD *device, char c) { Serial.write(c); } #endif