Last active
August 29, 2015 13:57
-
-
Save richard-to/9738497 to your computer and use it in GitHub Desktop.
Some Arduino sample code for SD card, LinkSprite Sim900 shield, and EEPROM
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <SD.h> | |
#include <String.h> | |
const int CS_PIN = 10; | |
char BACKUP_FILE[] = "backup.txt"; | |
char PENDING_FILE[] = "pending.txt"; | |
void setup() | |
{ | |
pinMode(CS_PIN, OUTPUT); | |
Serial.begin(9600); | |
if (!SD.begin(CS_PIN)) | |
{ | |
Serial.println("Error: Cannot connect to SD card. Make sure pin 10 is connected to CS."); | |
} | |
} | |
void loop() | |
{ | |
if (Serial.available()) | |
{ | |
switch(Serial.read()) | |
{ | |
case 'b': | |
sd_readBackupData(); | |
break; | |
case 'r': | |
sd_readPendingData(); | |
break; | |
case 'p': | |
sd_processPendingData("300,300,300"); | |
break; | |
case 'd': | |
sd_deleteAllFiles(); | |
break; | |
} | |
} | |
} | |
void sd_processPendingData(String data) | |
{ | |
sd_writeFile(PENDING_FILE, data); | |
String output = sd_readFile(PENDING_FILE); | |
int countLines = 0; | |
int index = 0; | |
int length = output.length(); | |
while(index < length) | |
{ | |
if (output[index] == '\n') | |
{ | |
++countLines; | |
} | |
++index; | |
} | |
Serial.print("Pending lines: "); | |
Serial.println(countLines); | |
if (countLines == 4) | |
{ | |
Serial.println("Backing up data..."); | |
output.replace("\n", ";"); | |
sd_writeBackupData(output); | |
sd_clearFile(PENDING_FILE); | |
Serial.println("Backing up data...done"); | |
} | |
} | |
void sd_writeBackupData(String data) | |
{ | |
sd_writeFile(BACKUP_FILE, data); | |
} | |
void sd_readBackupData() | |
{ | |
sd_readFile(BACKUP_FILE); | |
} | |
void sd_readPendingData() | |
{ | |
sd_readFile(PENDING_FILE); | |
} | |
void sd_deleteAllFiles() | |
{ | |
sd_deleteFile(BACKUP_FILE); | |
sd_deleteFile(PENDING_FILE); | |
} | |
void sd_writeFile(char *filename, String data) | |
{ | |
Serial.println("CMD: Write data to SD card."); | |
File fileHandle = SD.open(filename, FILE_WRITE); | |
if (fileHandle) | |
{ | |
Serial.println("Writing data..."); | |
fileHandle.println(data); | |
fileHandle.close(); | |
Serial.println("Writing data...done."); | |
} | |
else | |
{ | |
Serial.println("Error: Could not open file on SD card to write."); | |
} | |
} | |
void sd_clearFile(char *filename) | |
{ | |
Serial.println("CMD: Clear file contents."); | |
File fileHandle = SD.open(filename, (O_WRITE | O_CREAT | O_TRUNC)); | |
if (fileHandle) | |
{ | |
Serial.println("Clearing file contents...done"); | |
fileHandle.close(); | |
} | |
else | |
{ | |
Serial.println("Error: Could not open file on SD card to write."); | |
} | |
} | |
String sd_readFile(char *filename) | |
{ | |
char c; | |
String buffer = ""; | |
Serial.println("CMD: Read data from SD card."); | |
File fileHandle = SD.open(filename); | |
if (fileHandle) | |
{ | |
Serial.println("Reading data..."); | |
while (fileHandle.available()) | |
{ | |
c = fileHandle.read(); | |
buffer += c; | |
} | |
Serial.println(buffer); | |
fileHandle.close(); | |
Serial.println("Reading data...done."); | |
} | |
else | |
{ | |
Serial.println("Error: Could not open file on SD card to read."); | |
} | |
return buffer; | |
} | |
void sd_deleteFile(char *filename) | |
{ | |
Serial.println("CMD: Delete data from SD card."); | |
File fileHandle = SD.open(filename); | |
if (fileHandle) | |
{ | |
fileHandle.close(); | |
if (SD.remove(filename)) | |
{ | |
Serial.println("Deleting data...done."); | |
} | |
else | |
{ | |
Serial.println("Error: Could not delete file."); | |
} | |
} | |
else | |
{ | |
Serial.println("Error: Could not open file. Does it exist?"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <LowPower.h> | |
#include <math.h> | |
#include <SdFat.h> | |
#include <SPI.h> | |
#include <String.h> | |
#include <Time.h> | |
// Analog Pins | |
#define THERMISTOR_PIN 5 | |
#define ULTRASONIC_PIN 6 | |
// Digital Pins | |
#define MOSFET_RTC_PIN 3 | |
#define MOSFET_SD_PIN 4 | |
#define MOSFET_US_PIN 5 | |
#define MOSFET_GSM_PIN 6 | |
#define MOSFET_THERM_PIN 7 | |
#define RTC_CS_PIN 8 | |
#define GSM_PWRKEY 9 | |
#define SD_CS_PIN 10 | |
// Settings | |
// ---------------------------------- | |
#define SEND_DATA_AFTER_X_READINGS 4 | |
#define SLEEP_CYCLES 4 | |
#define NUM_THERM_READINGS 5 | |
#define THERM_READING_DELAY 20 | |
#define NUM_DISTANCE_READINGS 3 | |
#define DISTANCE_READING_DELAY 200 | |
#define DATA_DELIM ';' | |
#define BACKUP_FILENAME "backup.txt" | |
#define UNSENT_FILENAME "unsent.txt" | |
#define ERROR_FILENAME "error.txt" | |
#define PHONE_NUMBER "+16505033896" | |
#define GSM_TIMEOUT 250 | |
#define SMS_TIMEOUT 100 | |
#define ERROR_GSM "GSM Failed" | |
#define ERROR_SMS "SMS Failed" | |
// Custom Datatypes | |
// ------------------------------------- | |
typedef struct { | |
int distance; | |
int temperature; | |
time_t timestamp; | |
} SensorReading; | |
// Global Variables | |
// ------------------------------------- | |
int numCachedReadings = 0; | |
SensorReading sensorReadings[SEND_DATA_AFTER_X_READINGS]; | |
SdFat sd; | |
SdFile fh; | |
void setup() | |
{ | |
pinMode(SD_CS_PIN, OUTPUT); | |
pinMode(MOSFET_THERM_PIN, OUTPUT); | |
} | |
void loop() | |
{ | |
// 1. Wake up. | |
// ----------- | |
// Enter idle state for 8 s with the rest of peripherals turned off. | |
// 98 sleep cycles will take a reading approximately every 13 minutes. | |
for (int i = 0; i < SLEEP_CYCLES; ++i) | |
{ | |
LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF); | |
} | |
// 2. Turn on thermistor. | |
// ---------------------- | |
digitalWrite(MOSFET_THERM_PIN, HIGH); | |
// 3. Take 5 thermistor readings. (one every 20ms) | |
// ----------------------------------------------- | |
int thermReadings[NUM_THERM_READINGS]; | |
for (int i = 0; i < NUM_THERM_READINGS; ++i) | |
{ | |
thermReadings[i] = analogRead(THERMISTOR_PIN); | |
delay(THERM_READING_DELAY); | |
} | |
// 4. Turn off thermistor. | |
// ----------------------- | |
digitalWrite(MOSFET_THERM_PIN, LOW); | |
delay(500); | |
// 5. Average 5 thermistor readings. | |
// --------------------------------- | |
double sumTherm = 0; | |
for (int i = 0; i < NUM_THERM_READINGS; ++i) | |
{ | |
sumTherm += thermReadings[i]; | |
} | |
double avgTherm = sumTherm / NUM_THERM_READINGS; | |
avgTherm = 1023 / avgTherm - 1; | |
double R = 10000 / avgTherm; | |
// 6. Convert average thermistor reading into temperature. | |
// ------------------------------------------------------- | |
// Steinhart-Hart, modified: | |
double avgTemperature = ( 3950.0 / (log( R / (10000.0 * exp( -3950.0 / 298.13 ) ) ) ) ) - 273.13; | |
// 7. Turn on ultrasonic sensor (MOSFET). | |
// -------------------------------------- | |
digitalWrite(MOSFET_US_PIN, HIGH); | |
// Calibration time | |
delay(3000); | |
// 8. Take 3 distance readings. (One every 200ms) | |
// ---------------------------------------------- | |
int distanceReadings[NUM_DISTANCE_READINGS]; | |
for (int i = 0; i < NUM_DISTANCE_READINGS; ++i) | |
{ | |
distanceReadings[i] = analogRead(ULTRASONIC_PIN); | |
delay(DISTANCE_READING_DELAY); | |
} | |
// 9. Turn off ultrasonic sensor (MOSFET). | |
// --------------------------------------- | |
digitalWrite(MOSFET_US_PIN, LOW); | |
delay(500); | |
// 10. Average 3 distance measurements. | |
// ------------------------------------ | |
double sumDistance = 0.0; | |
for (int i = 0; i < NUM_DISTANCE_READINGS; ++i) | |
{ | |
sumDistance += distanceReadings[i]; | |
} | |
double avgDistance = sumDistance / NUM_DISTANCE_READINGS; | |
// 11. Use average temperature to calculate actual distance. | |
// --------------------------------------------------------- | |
double adjustedDistance = ( ( 331.1 + .6 * avgTemperature ) / 344.5 ) * avgDistance; | |
int roundedDistance = round(adjustedDistance); | |
int roundedTemperature = round(avgTemperature); | |
// 12. Get time from RTC Shield. | |
// ----------------------------- | |
digitalWrite(MOSFET_RTC_PIN, HIGH); | |
delay(500); | |
rtc_init(); | |
time_t unixTime = rtc_readTimeDate(); | |
digitalWrite(MOSFET_RTC_PIN, LOW); | |
delay(500); | |
// 13. Combine time, distance, and temperature into a single string. | |
// ----------------------------------------------------------------- | |
String dataString = String(unixTime) + " " + String(roundedDistance) + " " + String(roundedTemperature); | |
// Cache distance and time in global array variable | |
sensorReadings[numCachedReadings].distance = roundedDistance; | |
sensorReadings[numCachedReadings].temperature = roundedTemperature; | |
sensorReadings[numCachedReadings].timestamp = unixTime; | |
numCachedReadings += 1; | |
// 14. Turn on SD Card. | |
// 15. Save data string to text file on SD card: "1399506809 1000 100" (roughly 20 characters) | |
// ------------------------------------------------------------------------------------------- | |
//Turn on SD MOSFET. | |
digitalWrite(MOSFET_SD_PIN, HIGH); | |
// Try to turn on SD card. Should only need to be called once. | |
sd.begin(SD_CS_PIN, SPI_HALF_SPEED); | |
sd_writeToFile(BACKUP_FILENAME, dataString); | |
delay(1000); | |
// Turn off SD MOSFET. | |
digitalWrite(MOSFET_SD_PIN, LOW); | |
delay(500); | |
// 16. Are there 4 unsent data strings? | |
// 17. Yes. Send 4 unsent data strings in one SMS. Go to 18. | |
// ------------------------------------- | |
if (numCachedReadings == SEND_DATA_AFTER_X_READINGS) | |
{ | |
// 18. Prepare text message | |
// --------------------- | |
String textMessage = String(sensorReadings[0].timestamp) + " " + String(sensorReadings[0].distance) + " " + String(sensorReadings[0].temperature); | |
time_t startTime = sensorReadings[0].timestamp; | |
int minutesElapsed = 0; | |
// Format data to send as text message | |
for (int i = 1; i < numCachedReadings; ++i) | |
{ | |
minutesElapsed = (sensorReadings[i].timestamp - startTime) / 60; | |
textMessage += String(DATA_DELIM) + String(minutesElapsed) + " " + String(sensorReadings[i].distance) + " " + String(sensorReadings[i].temperature); | |
} | |
// 19. Turn on GSM Shied. | |
// --------------------- | |
//Turn on GSM MOSFET. | |
digitalWrite(MOSFET_GSM_PIN, HIGH); | |
delay(1000); | |
// GSM baud rate. Start communication link with GSM shield. | |
Serial.begin(19200); | |
delay(100); | |
// Turn on GSM power pin. | |
digitalWrite(GSM_PWRKEY, HIGH); | |
delay(1000); | |
// Turn off GSM power pin. | |
digitalWrite(GSM_PWRKEY, LOW); | |
delay(1000); | |
// 20. Send text message if GSM Ready | |
if (gsm_ready(GSM_TIMEOUT) == true) | |
{ | |
gsm_sendTextMessage(textMessage); | |
if (gsm_smsSent(SMS_TIMEOUT) == false) | |
{ | |
// Log SMS failure | |
digitalWrite(MOSFET_SD_PIN, HIGH); | |
delay(1000); | |
sd.begin(SD_CS_PIN, SPI_HALF_SPEED); | |
sd_writeToFile(ERROR_FILENAME, String(unixTime) + ": " + ERROR_SMS); | |
sd_writeToFile(UNSENT_FILENAME, textMessage); | |
digitalWrite(MOSFET_SD_PIN, LOW); | |
delay(1000); | |
} | |
} | |
else | |
{ | |
// Log GSM failure | |
digitalWrite(MOSFET_SD_PIN, HIGH); | |
delay(1000); | |
sd.begin(SD_CS_PIN, SPI_HALF_SPEED); | |
sd_writeToFile(ERROR_FILENAME, String(unixTime) + ": " + ERROR_GSM); | |
sd_writeToFile(UNSENT_FILENAME, textMessage); | |
digitalWrite(MOSFET_SD_PIN, LOW); | |
delay(1000); | |
} | |
// Reset number of cached readings | |
numCachedReadings = 0; | |
// 20. Turn off GSM. | |
// ----------------- | |
// Turn on GSM power pin. | |
digitalWrite(GSM_PWRKEY, HIGH); | |
delay(1000); | |
// Turn off GSM power pin. | |
digitalWrite(GSM_PWRKEY, LOW); | |
delay(1000); | |
// Turn off GSM MOSFET. | |
digitalWrite(MOSFET_GSM_PIN, LOW); | |
delay(1000); | |
} | |
} | |
void gsm_sendTextMessage(String message) | |
{ | |
// Because we want to send the SMS in text mode | |
Serial.print("AT+CMGF=1\r"); | |
delay(500); | |
// Send sms message, be careful need to add a country code before the cellphone number | |
Serial.println("AT + CMGS = \"" + String(PHONE_NUMBER) + "\""); | |
delay(500); | |
// The content of the message | |
Serial.println(message); | |
delay(500); | |
// The ASCII code of the ctrl+z is 26 | |
Serial.println((char)26); | |
delay(500); | |
} | |
// Clears the GSM buffer to make sure we don't unexpected characters | |
void gsm_clearBuffer() | |
{ | |
char c; | |
while (Serial.available() > 0) | |
{ | |
c = Serial.read(); | |
} | |
} | |
// Polls GSM buffer for "Call Ready" signal. | |
boolean gsm_ready(int timeout) | |
{ | |
char gsmReady[] = "Call Ready"; | |
char gsmReadyLength = 10; | |
int matched = 0; | |
char c; | |
while (timeout > 0) | |
{ | |
while (Serial.available()) | |
{ | |
c = Serial.read(); | |
if (c == gsmReady[matched]) | |
{ | |
matched += 1; | |
if (matched == gsmReadyLength) | |
{ | |
return true; | |
} | |
} else { | |
matched = 0; | |
} | |
} | |
timeout -= 1; | |
delay(100); | |
} | |
return false; | |
} | |
// Polls GSM buffer for two "OK" messages or one "ERROR" to | |
// check if SMS was sent successfully. | |
boolean gsm_smsSent(int timeout) | |
{ | |
char smsOK[] = "OK"; | |
int smsOKLength = 2; | |
int numOKNeeded = 2; | |
char smsError[] = "ERROR"; | |
int smsErrorLength = 5; | |
char c; | |
int matchedError = 0; | |
int matchedOK = 0; | |
int foundOK = 0; | |
while (timeout > 0) | |
{ | |
while (Serial.available()) | |
{ | |
c = Serial.read(); | |
if (c == smsError[matchedError]) | |
{ | |
matchedError += 1; | |
matchedOK = 0; | |
if (matchedError == smsErrorLength) | |
{ | |
return false; | |
} | |
} | |
else if (c == smsOK[matchedOK]) | |
{ | |
matchedOK += 1; | |
matchedError = 0; | |
if (matchedOK == smsOKLength) | |
{ | |
foundOK += 1; | |
} | |
if (foundOK == smsOKLength) | |
{ | |
return true; | |
} | |
} else { | |
matchedOK = 0; | |
matchedError = 0; | |
} | |
} | |
timeout -= 1; | |
delay(100); | |
} | |
return false; | |
} | |
// Saves data to specified file on SD Card | |
// | |
// Returns true if file written successfully, otherwise returns false. | |
// | |
boolean sd_writeToFile(char *filename, String data) | |
{ | |
if (fh.open(filename, O_RDWR | O_CREAT | O_AT_END)) | |
{ | |
fh.println(data); | |
fh.close(); | |
return true; | |
} | |
else | |
{ | |
return false; | |
} | |
} | |
// Reads file from sd card | |
// TODO(richard-to): Need to test this implementation. | |
// Not currently used in sketch. | |
String sd_readFile(char *filename) | |
{ | |
char c; | |
String buffer = ""; | |
if (fh.open(filename, O_READ)) | |
{ | |
while (fh.available()) | |
{ | |
c = fh.read(); | |
buffer += c; | |
} | |
fh.close(); | |
} | |
return buffer; | |
} | |
// Initializes RTC Clock | |
int rtc_init() | |
{ | |
pinMode(RTC_CS_PIN, OUTPUT); | |
SPI.begin(); | |
SPI.setBitOrder(MSBFIRST); | |
SPI.setDataMode(SPI_MODE3); | |
digitalWrite(RTC_CS_PIN, LOW); | |
SPI.transfer(0x8E); | |
SPI.transfer(0x60); | |
digitalWrite(RTC_CS_PIN, HIGH); | |
delay(10); | |
} | |
// Sets time on RTC CLOCK | |
// | |
// Notes: | |
// * Year is calculated as `current year - 1970`. So 2014 would be 44 and not 14. | |
// * Time is in UTC | |
// * day(1-31), month(1-12), year(0-99), hour(0-23), minute(0-59), second(0-59) | |
// | |
int rtc_setTimeDate(int d, int mo, int y, int h, int mi, int s) | |
{ | |
int timedate[7] = {s, mi, h, 0, d, mo, y}; | |
for (int i = 0; i <= 6; ++i) | |
{ | |
if (i == 3) | |
{ | |
i++; | |
} | |
int b = timedate[i] / 10; | |
int a = timedate[i] - b * 10; | |
if (i == 2) | |
{ | |
if (b == 2) | |
{ | |
b = B00000010; | |
} | |
else if (b == 1) | |
{ | |
b = B00000001; | |
} | |
} | |
timedate[i] = a + (b << 4); | |
digitalWrite(RTC_CS_PIN, LOW); | |
SPI.transfer(i+0x80); | |
SPI.transfer(timedate[i]); | |
digitalWrite(RTC_CS_PIN, HIGH); | |
} | |
} | |
// Gets unix timestamp from RTC clock | |
time_t rtc_readTimeDate() | |
{ | |
int timedate[7]; | |
for (int i = 0; i <= 6; ++i) | |
{ | |
if (i == 3) | |
{ | |
i++; | |
} | |
digitalWrite(RTC_CS_PIN, LOW); | |
SPI.transfer(i + 0x00); | |
unsigned int n = SPI.transfer(0x00); | |
digitalWrite(RTC_CS_PIN, HIGH); | |
int a = n & B00001111; | |
if (i == 2) | |
{ | |
int b = (n & B00110000) >> 4; | |
if (b == B00000010) | |
{ | |
b = 20; | |
} else if (b == B00000001) | |
{ | |
b = 10; | |
} | |
timedate[i] = a + b; | |
} | |
else if (i == 4) | |
{ | |
int b = (n & B00110000) >> 4; | |
timedate[i] = a + b * 10; | |
} | |
else if (i == 5) | |
{ | |
int b = (n & B00010000) >> 4; | |
timedate[i] = a + b * 10; | |
} | |
else if (i == 6) | |
{ | |
int b = (n & B11110000) >> 4; | |
timedate[i] = a + b * 10; | |
} | |
else | |
{ | |
int b = (n & B01110000) >> 4; | |
timedate[i] = a + b * 10; | |
} | |
} | |
TimeElements tm; | |
tm.Year = timedate[6]; | |
tm.Month = timedate[5]; | |
tm.Day = timedate[4]; | |
tm.Hour = timedate[2]; | |
tm.Minute = timedate[1]; | |
tm.Second = timedate[0]; | |
return makeTime(tm); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// TODO(richard-to): Table ID and API key should be stored as user parameters. | |
// This can be found in your Google Fusion tables url. Look for "docid" query parameter | |
var tableId = "Table-ID-Here"; | |
// Access key is needed for read permissions https://developers.google.com/fusiontables/docs/v1/using#auth | |
var apiKey = "API-Key-Here"; | |
// Main Google scripts web app entry point | |
function doGet() { | |
// Start with 1 day for example purposes | |
var startDate = '5/12/2014'; | |
var endDate = '5/13/2014'; | |
// Gets data from Google Fusion tables | |
var data = queryFusionDataWithDateRange(tableId, apiKey, startDate, endDate); | |
// Populates data table that will be used to build charts | |
var table = populateDataTable(data); | |
// Present our data as a scatter chart | |
var scatterChart = buildScatterChartView(); | |
// Create a control to filter specific sensors | |
var sensorFilter = Charts.newCategoryFilter() | |
.setFilterColumnLabel("Sensor") | |
.setLabel("Select Sensors") | |
.build(); | |
var app = UiApp.createApplication(); | |
// Create dashboard to layout controls and charts | |
var dashboard = buildDashboard(app, table, [sensorFilter], [scatterChart]); | |
// Create a dropdown menu to change chart types | |
var chartSelectorLabel = app.createInlineLabel("Select chart"); | |
var chartSelector = app.createListBox(false).setName('chart_selector'); | |
chartSelector.addItem('Data table').addItem('Scatter plot').setSelectedIndex(1); | |
var chartSelectorPanel = app.createHorizontalPanel(); | |
chartSelectorPanel.add(chartSelectorLabel).add(chartSelector).setSpacing(20); | |
// Create controls to filter by date | |
var startDateBox = app.createDateBox().setId("start_datebox").setValue(new Date(startDate)); | |
var startDateLabel = app.createInlineLabel("Start"); | |
var endDateBox = app.createDateBox().setId("end_datebox").setValue(new Date(endDate)); | |
var endDateLabel = app.createInlineLabel("End"); | |
var dateFilterButton = app.createButton("Filter by date"); | |
var datePanel = app.createHorizontalPanel(); | |
datePanel.add(startDateLabel).add(startDateBox) | |
.add(endDateLabel).add(endDateBox) | |
.add(dateFilterButton).setSpacing(20); | |
// When filter date button pressed or chart selected, send chart type, start date, end date to update charts | |
var updateChartHandler = app.createServerHandler("updateChartHandler") | |
.addCallbackElement(startDateBox).addCallbackElement(endDateBox).addCallbackElement(chartSelector); | |
dateFilterButton.addClickHandler(updateChartHandler); | |
chartSelector.addChangeHandler(updateChartHandler); | |
// Add chart selector panel and dashboard to app to make them visible | |
app.add(chartSelectorPanel); | |
app.add(datePanel); | |
app.add(dashboard); | |
return app; | |
} | |
// This code is executed when use clicks "Filter date button" or selects a different chart type | |
// TODO(richard-to): Need to clean up code so code isn't duplicated from `doGet` | |
function updateChartHandler(eventInfo) { | |
var app = UiApp.getActiveApplication(); | |
var chartType = eventInfo.parameter.chart_selector; | |
var startDate = eventInfo.parameter.start_datebox; | |
var endDate = eventInfo.parameter.end_datebox; | |
var formattedStartDate = null; | |
var formattedEndDate = null; | |
if (startDate) | |
{ | |
formattedStartDate = toFusionDate(startDate); | |
} | |
if (endDate) | |
{ | |
endDate.setDate(endDate.getDate() + 1); | |
formattedEndDate = toFusionDate(endDate); | |
} | |
// Remove old charts | |
app.remove(app.getElementById('dashboard')); | |
// Gets data from Google Fusion tables | |
var data = queryFusionDataWithDateRange(tableId, apiKey, formattedStartDate, formattedEndDate); | |
// Populates data table that will be used to build charts | |
var table = populateDataTable(data); | |
// Present our data as a scatter chart or data table depending on chart type | |
var chartView = null; | |
if (chartType == 'Scatter plot') | |
{ | |
chartView = buildScatterChartView(); | |
} | |
else | |
{ | |
chartView = buildDataTableChartView(); | |
} | |
// Create a control to filter specific sensors | |
var sensorFilter = Charts.newCategoryFilter() | |
.setFilterColumnLabel("Sensor") | |
.setLabel("Select Sensors") | |
.build(); | |
// Create dashboard to layout controls and charts | |
var dashboard = buildDashboard(app, table, [sensorFilter], [chartView]); | |
// Add updated charts back to app | |
app.add(dashboard); | |
return app; | |
} | |
// Converts Javascript Date object to a formatted date usable for Fusion table queries | |
function toFusionDate(date) | |
{ | |
return (date.getMonth() + 1) + "/" + date.getDate() + "/" + date.getFullYear(); | |
} | |
function populateDataTable(data) | |
{ | |
var table = Charts.newDataTable() | |
.addColumn(Charts.ColumnType.DATE, "Date") | |
.addColumn(Charts.ColumnType.NUMBER, "Distance") | |
.addColumn(Charts.ColumnType.NUMBER, "Temperature") | |
.addColumn(Charts.ColumnType.STRING, "Sensor"); | |
var rows = data.rows; | |
for (var i = 0; i < rows.length; ++i) | |
{ | |
table.addRow([new Date(rows[i][0]), parseInt(rows[i][1]), parseInt(rows[i][2]), rows[i][3]]); | |
} | |
table.build(); | |
return table; | |
} | |
function buildDataTableChartView() | |
{ | |
return Charts.newTableChart() | |
.setDimensions(1024, 500) | |
.build(); | |
} | |
function buildScatterChartView() | |
{ | |
// Present our data as a scatter chart | |
return Charts.newScatterChart() | |
.setTitle('Distance and Temperature') | |
.setXAxisTitle('Date Time') | |
.setYAxisTitle('Distance/Chart') | |
.setDimensions(1024, 500) | |
.setLegendPosition(Charts.Position.NONE) | |
.setPointStyle(Charts.PointStyle.TINY) | |
.setDataViewDefinition(Charts.newDataViewDefinition().setColumns([0,1,2])) | |
.build(); | |
} | |
// Create Dashboard to layout controls and charts | |
function buildDashboard(app, table, controls, charts) | |
{ | |
var dashboard = Charts.newDashboardPanel() | |
.setDataTable(table) | |
.bind(controls, charts) | |
.build(); | |
var controlPanel = app.createHorizontalPanel(); | |
for (var i = 0; i < controls.length; ++i) | |
{ | |
controlPanel.add(controls[i]); | |
} | |
var verticalPanel = app.createVerticalPanel().setSpacing(20); | |
verticalPanel.add(controlPanel); | |
for (var i = 0; i < charts.length; ++i) | |
{ | |
verticalPanel.add(app.createHorizontalPanel().add(charts[i])); | |
} | |
dashboard.add(verticalPanel); | |
dashboard.setId('dashboard'); | |
return dashboard; | |
} | |
// Get data from fusion tables with option to filter by date range | |
function queryFusionDataWithDateRange(tableId, apiKey, startDate, endDate) | |
{ | |
var query = "SELECT 'Date', Distance, Temperature, Sensor FROM " + tableId; | |
if (startDate || endDate) | |
{ | |
query += " WHERE"; | |
} | |
if (startDate) | |
{ | |
query += " 'Date' >= '" + startDate + "'"; | |
} | |
if (startDate && endDate) | |
{ | |
query += " AND"; | |
} | |
if (endDate) | |
{ | |
query += " 'Date' <= '" + endDate + "'"; | |
} | |
return fetchFusionData(apiKey, encodeURIComponent(query)); | |
} | |
// Performs the HTTP GET request to get fusion data in JSON format | |
function fetchFusionData(apiKey, query) { | |
var URL = "https://www.googleapis.com/fusiontables/v1/query?key=" + apiKey + "&sql=" + query; | |
var response = UrlFetchApp.fetch(URL, {method: "get",}); | |
return JSON.parse(response.getContentText()); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <SoftwareSerial.h> | |
#include <String.h> | |
SoftwareSerial Sim900(7, 8); | |
#define TIMEOUT 250 | |
#define GSM_READY_MSG "Call Ready" | |
#define GSM_READY_MSG_LEN 10 | |
void setup() | |
{ | |
Serial.begin(9600); | |
Sim900.begin(19200); | |
} | |
void loop() | |
{ | |
if (Serial.available()) | |
{ | |
switch(Serial.read()) | |
{ | |
case 'm': | |
gsm_clearBuffer(); | |
gsm_sendTextMessage(); | |
if (gsm_smsSent(TIMEOUT)) | |
{ | |
Serial.println("SMS Sent!"); | |
} | |
else | |
{ | |
Serial.println("SMS Not Sent!"); | |
} | |
break; | |
case 'c': | |
gsm_command(); | |
break; | |
case 't': | |
gsm_checkTime(); | |
break; | |
case 'p': | |
gsm_clearBuffer(); | |
gsm_power(); | |
if (gsm_isReady(GSM_READY_MSG, GSM_READY_MSG_LEN, TIMEOUT)) | |
{ | |
Serial.println("GSM Ready!"); | |
} | |
else | |
{ | |
Serial.println("GSM Not Ready!"); | |
} | |
break; | |
} | |
} | |
if (Sim900.available()) | |
{ | |
Serial.write(Sim900.read()); | |
} | |
} | |
void gsm_power() | |
{ | |
Serial.println("Toggle GSM Power"); | |
digitalWrite(9, HIGH); | |
delay(1000); | |
digitalWrite(9, LOW); | |
} | |
void gsm_clearBuffer() | |
{ | |
char c; | |
while (Sim900.available() > 0) | |
{ | |
c = Sim900.read(); | |
} | |
} | |
boolean gsm_isReady(char keyword[], int length, int timeout) | |
{ | |
char c; | |
int matched = 0; | |
while (timeout > 0) | |
{ | |
while (Sim900.available()) | |
{ | |
c = Sim900.read(); | |
if (c == keyword[matched]) | |
{ | |
matched += 1; | |
if (matched == length) | |
{ | |
return true; | |
} | |
} else { | |
matched = 0; | |
} | |
} | |
timeout -= 1; | |
delay(100); | |
} | |
return false; | |
} | |
boolean gsm_smsSent(int timeout) | |
{ | |
char smsOK[] = "OK"; | |
int smsOKLength = 2; | |
int numOKNeeded = 2; | |
char smsError[] = "ERROR"; | |
int smsErrorLength = 5; | |
char c; | |
int matchedError = 0; | |
int matchedOK = 0; | |
int foundOK = 0; | |
while (timeout > 0) | |
{ | |
while (Sim900.available()) | |
{ | |
c = Sim900.read(); | |
if (c == smsError[matchedError]) | |
{ | |
matchedError += 1; | |
matchedOK = 0; | |
if (matchedError == smsErrorLength) | |
{ | |
return false; | |
} | |
} | |
else if (c == smsOK[matchedOK]) | |
{ | |
matchedOK += 1; | |
matchedError = 0; | |
if (matchedOK == smsOKLength) | |
{ | |
foundOK += 1; | |
} | |
if (foundOK == smsOKLength) | |
{ | |
return true; | |
} | |
} else { | |
matchedOK = 0; | |
matchedError = 0; | |
} | |
} | |
timeout -= 1; | |
delay(100); | |
} | |
return false; | |
} | |
void gsm_sendTextMessage() | |
{ | |
Sim900.print("AT+CMGF=1\r"); | |
delay(100); | |
Sim900.println("AT + CMGS = \"+19076029802\""); | |
delay(100); | |
Sim900.println("A test message"); | |
delay(100); | |
Sim900.println((char)26); | |
delay(100); | |
} | |
void gsm_checkTime() | |
{ | |
String time = gsm_getTime(); | |
Serial.println(time); | |
} | |
String gsm_getTime() | |
{ | |
char c; | |
boolean foundTime = false; | |
String time = ""; | |
Sim900.println("AT+CCLK?"); | |
delay(100); | |
while (Sim900.available() != 0) | |
{ | |
c = Sim900.read(); | |
if (c == '"' && foundTime == false) | |
{ | |
foundTime = true; | |
} | |
else if (c == '"' && foundTime == true) | |
{ | |
foundTime = false; | |
} | |
else if (foundTime == true) | |
{ | |
time += c; | |
} | |
} | |
return time; | |
} | |
void gsm_command() | |
{ | |
Serial.println("Enter AT command: "); | |
char buffer[100]; | |
int inputLength = 0; | |
while (!Serial.available()) { | |
delay(100); | |
} | |
inputLength = Serial.readBytesUntil('\n', buffer, 99); | |
buffer[inputLength] = '\0'; | |
Sim900.println(buffer); | |
delay(100); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <LowPower.h> | |
#include <math.h> | |
#include <SD.h> | |
#include <SPI.h> | |
#include <String.h> | |
#include <Time.h> | |
// Digital Pins | |
#define RTC_CS_PIN 8 | |
void setup() | |
{ | |
Serial.begin(9600); | |
rtc_init(); | |
// day(1-31), month(1-12), year(0-99), hour(0-23), minute(0-59), second(0-59) | |
rtc_setTimeDate(7, 5, 44, 13, 21, 0); | |
} | |
void loop() | |
{ | |
Serial.println(rtc_readTimeDate()); | |
delay(1000); | |
} | |
// Initializes RTC Clock | |
int rtc_init() | |
{ | |
pinMode(RTC_CS_PIN, OUTPUT); | |
SPI.begin(); | |
SPI.setBitOrder(MSBFIRST); | |
SPI.setDataMode(SPI_MODE3); | |
digitalWrite(RTC_CS_PIN, LOW); | |
SPI.transfer(0x8E); | |
SPI.transfer(0x60); | |
digitalWrite(RTC_CS_PIN, HIGH); | |
delay(10); | |
} | |
// Sets time on RTC CLOCK | |
// | |
// Notes: | |
// * Year is calculated as `current year - 1970`. So 2014 would be 44 and not 14. | |
// * Time is in UTC | |
// | |
// d = day, mo = month, y = year, h = hours, mi = minutes, s = seconds | |
// | |
int rtc_setTimeDate(int d, int mo, int y, int h, int mi, int s) | |
{ | |
int timedate[7] = {s, mi, h, 0, d, mo, y}; | |
for (int i = 0; i <= 6; ++i) | |
{ | |
if (i == 3) | |
{ | |
i++; | |
} | |
int b = timedate[i] / 10; | |
int a = timedate[i] - b * 10; | |
if (i == 2) | |
{ | |
if (b == 2) | |
{ | |
b = B00000010; | |
} | |
else if (b == 1) | |
{ | |
b = B00000001; | |
} | |
} | |
timedate[i] = a + (b << 4); | |
digitalWrite(RTC_CS_PIN, LOW); | |
SPI.transfer(i+0x80); | |
SPI.transfer(timedate[i]); | |
digitalWrite(RTC_CS_PIN, HIGH); | |
} | |
} | |
// Gets unix timestamp from RTC clock | |
time_t rtc_readTimeDate() | |
{ | |
int timedate[7]; | |
for (int i = 0; i <= 6; ++i) | |
{ | |
if (i == 3) | |
{ | |
i++; | |
} | |
digitalWrite(RTC_CS_PIN, LOW); | |
SPI.transfer(i + 0x00); | |
unsigned int n = SPI.transfer(0x00); | |
digitalWrite(RTC_CS_PIN, HIGH); | |
int a = n & B00001111; | |
if (i == 2) | |
{ | |
int b = (n & B00110000) >> 4; | |
if (b == B00000010) | |
{ | |
b = 20; | |
} else if (b == B00000001) | |
{ | |
b = 10; | |
} | |
timedate[i] = a + b; | |
} | |
else if (i == 4) | |
{ | |
int b = (n & B00110000) >> 4; | |
timedate[i] = a + b * 10; | |
} | |
else if (i == 5) | |
{ | |
int b = (n & B00010000) >> 4; | |
timedate[i] = a + b * 10; | |
} | |
else if (i == 6) | |
{ | |
int b = (n & B11110000) >> 4; | |
timedate[i] = a + b * 10; | |
} | |
else | |
{ | |
int b = (n & B01110000) >> 4; | |
timedate[i] = a + b * 10; | |
} | |
} | |
TimeElements tm; | |
tm.Year = timedate[6]; | |
tm.Month = timedate[5]; | |
tm.Day = timedate[4]; | |
tm.Hour = timedate[2]; | |
tm.Minute = timedate[1]; | |
tm.Second = timedate[0]; | |
return makeTime(tm); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "LowPower.h" | |
#include <SD.h> | |
#include <String.h> | |
// Digital Pins | |
const int SD_CS_PIN = 10; | |
char BACKUP_FILENAME[] = "backup.txt"; | |
void setup() | |
{ | |
pinMode(SD_CS_PIN, OUTPUT); | |
SD.begin(SD_CS_PIN); | |
Serial.begin(9600); | |
} | |
void loop() | |
{ | |
for (int i = 0; i < 2; i++) | |
{ | |
LowPower.idle(SLEEP_8S, ADC_OFF, TIMER2_OFF, TIMER1_OFF, TIMER0_OFF, SPI_OFF, USART0_ON,TWI_OFF); | |
} | |
delay(5000); | |
Serial.println("READY"); | |
test_write(); | |
} | |
void test_write() | |
{ | |
String dataString = "TEST"; | |
if (sd_writeToFile(BACKUP_FILENAME, dataString)) { | |
Serial.println("SUCESS"); | |
} | |
else | |
{ | |
Serial.println("FAILED"); | |
} | |
} | |
// Saves data to specified file on SD Card | |
// | |
// Returns true if file written successfully, otherwise returns false. | |
// | |
boolean sd_writeToFile(char *filename, String data) | |
{ | |
File fileHandle = SD.open(filename, FILE_WRITE); | |
if (fileHandle) | |
{ | |
fileHandle.println(data); | |
fileHandle.close(); | |
return true; | |
} | |
else | |
{ | |
return false; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <SdFat.h> | |
#include <LowPower.h> | |
// Replace SS with the chip slect pin for your SD. | |
const uint8_t sdChipSelect = SS; | |
// SD file system. | |
SdFat sd; | |
// Log file. | |
SdFile file; | |
void setup() | |
{ | |
// Make sure to turn on mosfet to enable sd | |
digitalWrite(3, HIGH); | |
delay(100); | |
sd.begin(sdChipSelect); | |
file.open("DATA.CSV", O_CREAT | O_WRITE | O_APPEND); | |
} | |
uint32_t n = 0; | |
void loop() | |
{ | |
digitalWrite(3, LOW); | |
delay(100); | |
for (int i = 0; i < 2; ++i) | |
{ | |
LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF); | |
} | |
digitalWrite(3, HIGH); | |
delay(100); | |
file.println(n++); | |
file.sync(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Fetches the list of existing Sheets from the ScriptDb instance. | |
// | |
// Params: | |
// db: ScriptDb instance | |
// | |
// Returns: | |
// A hash table with a Sheet metadata associated with name | |
// of the the Sheet. | |
function fetchSheetMetaFromDb(db) | |
{ | |
var data = {}; | |
var result = db.query({}); | |
while (result.hasNext()) { | |
var item = result.next(); | |
data[item.name] = item; | |
} | |
return data; | |
} | |
// Gets the sheet object to add sensor readings to. | |
// | |
// If a sheet does not exist for the specific sensor (identified | |
// by phone number, then a new sheet is created and saved to the ScriptDb | |
// instance. | |
// | |
// Params: | |
// sheet_name: The name of the Sheet. Should be unique since sensor is id'd by phone number | |
// sheet_meta: Metadata for sheet object. Just name and url right now. | |
// db: ScriptDb instance | |
// | |
// Returns: | |
// Sheet object | |
// | |
// Notes: | |
// | |
// If the url points to a sheet that does not exist, then the sheet metadata will | |
// be deleted from ScriptDb instance. | |
// | |
// Beware that moving the Sheet to the trash means that the sheet is still valid. The | |
// The sheet will need to be removed completely | |
// | |
// TODO: | |
// | |
// If a sheet is moved to the trash, it should not be considered an active sheet. | |
function getSheetObjectByName(sheet_name, sheet_meta, db) | |
{ | |
var sheet = undefined; | |
if (sheet_meta[sheet_name] !== undefined) { | |
try { | |
sheet = SpreadsheetApp.openByUrl(sheet_meta[sheet_name].url); | |
} catch(e) { | |
db.remove(sheet_meta[sheet_name]); | |
sheet = undefined; | |
sheet_meta[sheet_name] = undefined; | |
} | |
} | |
if (sheet === undefined) { | |
sheet = SpreadsheetApp.create(sheet_name); | |
var item = { | |
name: sheet_name, | |
url: sheet.getUrl() | |
}; | |
sheet_meta[sheet_name] = db.save(item); | |
} | |
return sheet; | |
} | |
// Gets starred messages from Gmail and adds parsed data to Sheet | |
// | |
// Params: | |
// sheet_meta: A hash table with existing sheets | |
// db: ScriptDb instance | |
// | |
// TODO: | |
// | |
// - Need to keep an eye out for errors due to the automatic line breaks. | |
// | |
// Notes: | |
// | |
// - Gmail throttles the script if too many actions are taken (ie, fetch/read/star/unstar msgs) | |
// Should not be a problem at a rate of one email every five hours. | |
// - Ideally there should be a way to prevent cases where a message is processed | |
// but not unstarred due to a Gmail error. This would lead to a message being processed twice. | |
// However the probability of that happening is very low so long as you are not processing | |
// greater than 100 messages, in which case Gmail will throttle the script or AppScript max execution | |
// time will be reached. | |
function processStarredThreads(sheet_meta, db) { | |
var threads = GmailApp.getStarredThreads(); | |
for (var t in threads) { | |
var thread = threads[t]; | |
var subject = thread.getFirstMessageSubject(); | |
var msgs = thread.getMessages(); | |
var msg_count = thread.getMessageCount(); | |
var sheet = getSheetObjectByName(subject, sheet_meta, db); | |
var active_sheet = sheet.setActiveSheet(sheet.getSheets()[0]); | |
// Loop through thread of messages. Each thread contains messages | |
// from a single sensor up to 100 messages. | |
for (var i = 0; i < msg_count; ++i) { | |
// Starred messages have not been processed yet. | |
if (msgs[i].isStarred()) { | |
// Get the body text from the email message. | |
var text = msgs[i].getPlainBody(); | |
// Break the message into separate lines | |
// The emails seem to add an automatic line | |
// break at about 73-75 characters. | |
var lines = text.split("\r\n"); | |
var data = ""; | |
// The data we need will be on 2-3 lines if | |
// we maxed out most of the text message length. | |
// | |
// Hence we need to combine the lines again. | |
for (var j = 0; j < lines.length; ++j) { | |
if (lines[j].length > 0) { | |
data += lines[j]; | |
} else { | |
break; | |
} | |
} | |
// For some reason a double space is present in the | |
// recombined message data. This extra space occurs exactly | |
// at the line break. | |
// | |
// Here we split the sensor readings into individual | |
// readings of - timestamp distance temperature. | |
data = data.trim().replace(" ", " ").split(';'); | |
var readings = []; | |
for (var j = 0; j < data.length; ++j) { | |
readings.push(data[j].split(' ')); | |
} | |
// The first timestamp should be a full unix timestamp. | |
// We will need this to calculate the other timestamps. | |
// The other timestamps consist of minutes after the first | |
// reading in the message. | |
var startTime = parseInt(readings[0][0]); | |
sheet.appendRow([startTime, readings[0][1], readings[0][2],data.toString()]); | |
for (var j = 1; j < readings.length; ++j) { | |
sheet.appendRow([ | |
startTime + (60 * parseInt(readings[j][0])), | |
readings[j][1], | |
readings[j][2] | |
]); | |
} | |
// Unstar the message now that we've added the reading to the Sheet. | |
msgs[i].unstar(); | |
} | |
// Every fifteen readings, let's pause for one second. | |
// Not sure if this helps avoid the Gmail throttling though. | |
if (i % 15 == 0) { | |
Utilities.sleep(1000); | |
} | |
} | |
} | |
} | |
// Main program to execute. | |
function main_program() | |
{ | |
var sheet; | |
var db = ScriptDb.getMyDb(); | |
var sheet_meta = fetchSheetMetaFromDb(db); | |
processStarredThreads(sheet_meta, db); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment