├── .gitattributes ├── .gitignore ├── README.md ├── Wimp_Weather_Station.ino ├── agent.nut └── device.nut /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Wimp Weather Station 2 | ========================= 3 | 4 | [![Wimp Weather Station](https://cdn.sparkfun.com/r/600-600/assets/learn_tutorials/2/1/7/Setup-4.jpg)](https://cdn.sparkfun.com/assets/learn_tutorials/2/1/7/Setup-4.jpg) 5 | 6 | [*A wireless weather station based on the Electric Imp*](https://learn.sparkfun.com/tutorials/weather-station-wirelessly-connected-to-wudnerground) 7 | 8 | The Wimp is a personal weather station that uses the [weather shield](https://www.sparkfun.com/products/12081) along with an [Electric Imp](https://www.sparkfun.com/products/11395) to push live weather data up to [Wunderground](http://www.wunderground.com/). You can help increase the accuracy and prediction of weather by adding a weather meter to your house! But why buy an off-the-shelf system when you can build you own? For around $250 you can build a cutting edge open source station that you have complete control over! All you need is a pile of parts and access to a Wifi network. 9 | 10 | Read the tutorial on how to setup your own [weather station wirelessly connected to Wunderground](https://learn.sparkfun.com/tutorials/weather-station-wirelessly-connected-to-wudnerground). 11 | 12 | Repository Contents 13 | ------------------- 14 | 15 | The sketch to be loaded onto the Arduino is called *Wimp_Weather_Station.ino*. The Squirrel code that goes on the electric imp is called *agent.nut* and *device.nut* for the two parts of Imp code. 16 | 17 | License Information 18 | ------------------- 19 | The hardware is released under [Creative Commons ShareAlike 4.0 International](https://creativecommons.org/licenses/by-sa/4.0/). 20 | The code is beerware; if you see me (or any other SparkFun employee) at the local, and you've found our code helpful, please buy us a round! 21 | 22 | Distributed as-is; no warranty is given. 23 | -------------------------------------------------------------------------------- /Wimp_Weather_Station.ino: -------------------------------------------------------------------------------- 1 | /* 2 | Weather Station using the Electric Imp 3 | By: Nathan Seidle 4 | SparkFun Electronics 5 | Date: October 4th, 2013 6 | License: This code is public domain but you buy me a beer if you use this and we meet someday (Beerware license). 7 | 8 | Much of this is based on Mike Grusin's USB Weather Board code. 9 | 10 | This code reads all the various sensors (wind speed, direction, rain gauge, humidty, pressure, light, batt_lvl) 11 | and sends it to the imp, which then forwards that data to an Imp Agent on the cloud that does some processing then 12 | bounces the weather data to Wunderground. 13 | 14 | The Imp Shield has Card Detect tied to pin A0. We use A0 for wind direction. You will need to cut the trace on the Imp shield. 15 | 16 | Current: 17 | 130 for 2 seconds while transmitting 18 | ~30mA during sleep 19 | 20 | Todo: 21 | Reset after 45 days to avoid millis roll over problems 22 | 23 | What was the wind direction and speed gust for the last 10 minutes? 24 | Is the 3.3V pin tied on the weather shield or elsewhere? 25 | */ 26 | 27 | #include //We need watch dog for this program 28 | #include //I2C needed for sensors 29 | #include "MPL3115A2.h" //Pressure sensor 30 | #include "HTU21D.h" //Humidity sensor 31 | 32 | //#define ENABLE_LIGHTNING 33 | 34 | //SoftwareSerial imp(8, 9); // RX, TX into Imp pin 7 35 | 36 | MPL3115A2 myPressure; //Create an instance of the pressure sensor 37 | HTU21D myHumidity; //Create an instance of the humidity sensor 38 | 39 | //Hardware pin definitions 40 | //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 41 | // digital I/O pins 42 | const byte WSPEED = 3; 43 | const byte RAIN = 2; 44 | const byte STAT1 = 7; 45 | 46 | #ifdef ENABLE_LIGHTNING 47 | const byte LIGHTNING_IRQ = 4; //Not really an interrupt pin, we will catch it in software 48 | const byte slaveSelectPin = 10; //SS for AS3935 49 | #endif 50 | 51 | // analog I/O pins 52 | const byte WDIR = A0; 53 | const byte LIGHT = A1; 54 | const byte BATT = A2; 55 | const byte REFERENCE_3V3 = A3; 56 | //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 57 | 58 | #ifdef ENABLE_LIGHTNING 59 | #include "AS3935.h" //Lighting dtector 60 | #include //Needed for lighting sensor 61 | 62 | byte SPItransfer(byte sendByte); 63 | 64 | AS3935 AS3935(SPItransfer, slaveSelectPin, LIGHTNING_IRQ); 65 | #endif 66 | 67 | //Global Variables 68 | //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 69 | long lastSecond; //The millis counter to see when a second rolls by 70 | unsigned int minutesSinceLastReset; //Used to reset variables after 24 hours. Imp should tell us when it's midnight, this is backup. 71 | byte seconds; //When it hits 60, increase the current minute 72 | byte seconds_2m; //Keeps track of the "wind speed/dir avg" over last 2 minutes array of data 73 | byte minutes; //Keeps track of where we are in various arrays of data 74 | byte minutes_10m; //Keeps track of where we are in wind gust/dir over last 10 minutes array of data 75 | 76 | long lastWindCheck = 0; 77 | volatile long lastWindIRQ = 0; 78 | volatile byte windClicks = 0; 79 | 80 | #ifdef ENABLE_LIGHTNING 81 | byte lightning_distance = 0; 82 | #endif 83 | 84 | //We need to keep track of the following variables: 85 | //Wind speed/dir each update (no storage) 86 | //Wind gust/dir over the day (no storage) 87 | //Wind speed/dir, avg over 2 minutes (store 1 per second) 88 | //Wind gust/dir over last 10 minutes (store 1 per minute) 89 | //Rain over the past hour (store 1 per minute) 90 | //Total rain over date (store one per day) 91 | 92 | byte windspdavg[120]; //120 bytes to keep track of 2 minute average 93 | #define WIND_DIR_AVG_SIZE 120 94 | int winddiravg[WIND_DIR_AVG_SIZE]; //120 ints to keep track of 2 minute average 95 | float windgust_10m[10]; //10 floats to keep track of largest gust in the last 10 minutes 96 | int windgustdirection_10m[10]; //10 ints to keep track of 10 minute max 97 | volatile float rainHour[60]; //60 floating numbers to keep track of 60 minutes of rain 98 | 99 | //These are all the weather values that wunderground expects: 100 | int winddir; // [0-360 instantaneous wind direction] 101 | float windspeedmph; // [mph instantaneous wind speed] 102 | float windgustmph; // [mph current wind gust, using software specific time period] 103 | int windgustdir; // [0-360 using software specific time period] 104 | float windspdmph_avg2m; // [mph 2 minute average wind speed mph] 105 | int winddir_avg2m; // [0-360 2 minute average wind direction] 106 | float windgustmph_10m; // [mph past 10 minutes wind gust mph ] 107 | int windgustdir_10m; // [0-360 past 10 minutes wind gust direction] 108 | float humidity; // [%] 109 | float tempf; // [temperature F] 110 | float rainin; // [rain inches over the past hour)] -- the accumulated rainfall in the past 60 min 111 | volatile float dailyrainin; // [rain inches so far today in local time] 112 | //float baromin = 30.03;// [barom in] - It's hard to calculate baromin locally, do this in the agent 113 | float pressure; 114 | //float dewptf; // [dewpoint F] - It's hard to calculate dewpoint locally, do this in the agent 115 | 116 | //These are not wunderground values, they are just for us 117 | float batt_lvl = 11.8; 118 | float light_lvl = 0.72; 119 | 120 | // volatiles are subject to modification by IRQs 121 | volatile unsigned long raintime, rainlast, raininterval, rain; 122 | 123 | //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 124 | 125 | //Interrupt routines (these are called by the hardware interrupts, not by the main code) 126 | //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 127 | void rainIRQ() 128 | // Count rain gauge bucket tips as they occur 129 | // Activated by the magnet and reed switch in the rain gauge, attached to input D2 130 | { 131 | raintime = millis(); // grab current time 132 | raininterval = raintime - rainlast; // calculate interval between this and last event 133 | 134 | if (raininterval > 10) // ignore switch-bounce glitches less than 10mS after initial edge 135 | { 136 | dailyrainin += 0.011; //Each dump is 0.011" of water 137 | rainHour[minutes] += 0.011; //Increase this minute's amount of rain 138 | 139 | rainlast = raintime; // set up for next event 140 | } 141 | } 142 | 143 | void wspeedIRQ() 144 | // Activated by the magnet in the anemometer (2 ticks per rotation), attached to input D3 145 | { 146 | if (millis() - lastWindIRQ > 10) // Ignore switch-bounce glitches less than 10ms (142MPH max reading) after the reed switch closes 147 | { 148 | lastWindIRQ = millis(); //Grab the current time 149 | windClicks++; //There is 1.492MPH for each click per second. 150 | } 151 | } 152 | 153 | void setup() 154 | { 155 | wdt_reset(); //Pet the dog 156 | wdt_disable(); //We don't want the watchdog during init 157 | 158 | Serial.begin(9600); 159 | 160 | pinMode(WSPEED, INPUT_PULLUP); // input from wind meters windspeed sensor 161 | pinMode(RAIN, INPUT_PULLUP); // input from wind meters rain gauge sensor 162 | 163 | pinMode(WDIR, INPUT); 164 | pinMode(LIGHT, INPUT); 165 | pinMode(BATT, INPUT); 166 | pinMode(REFERENCE_3V3, INPUT); 167 | 168 | pinMode(STAT1, OUTPUT); 169 | 170 | midnightReset(); //Reset rain totals 171 | 172 | //Configure the pressure sensor 173 | myPressure.begin(); // Get sensor online 174 | myPressure.setModeBarometer(); // Measure pressure in Pascals from 20 to 110 kPa 175 | myPressure.setOversampleRate(128); // Set Oversample to the recommended 128 176 | myPressure.enableEventFlags(); // Enable all three pressure and temp event flags 177 | myPressure.setModeActive(); // Go to active mode and start measuring! 178 | 179 | //Configure the humidity sensor 180 | myHumidity.begin(); 181 | 182 | #ifdef ENABLE_LIGHTNING 183 | startLightning(); //Init the lighting sensor 184 | #endif 185 | 186 | seconds = 0; 187 | lastSecond = millis(); 188 | 189 | // attach external interrupt pins to IRQ functions 190 | attachInterrupt(0, rainIRQ, FALLING); 191 | attachInterrupt(1, wspeedIRQ, FALLING); 192 | 193 | // turn on interrupts 194 | interrupts(); 195 | 196 | Serial.println("Wimp Weather Station online!"); 197 | reportWeather(); 198 | 199 | // wdt_enable(WDTO_1S); //Unleash the beast 200 | } 201 | 202 | void loop() 203 | { 204 | wdt_reset(); //Pet the dog 205 | 206 | //Keep track of which minute it is 207 | if(millis() - lastSecond >= 1000) 208 | { 209 | lastSecond += 1000; 210 | 211 | //Take a speed and direction reading every second for 2 minute average 212 | if(++seconds_2m > 119) seconds_2m = 0; 213 | 214 | //Calc the wind speed and direction every second for 120 second to get 2 minute average 215 | windspeedmph = get_wind_speed(); 216 | winddir = get_wind_direction(); 217 | windspdavg[seconds_2m] = (int)windspeedmph; 218 | winddiravg[seconds_2m] = winddir; 219 | //if(seconds_2m % 10 == 0) displayArrays(); 220 | 221 | //Check to see if this is a gust for the minute 222 | if(windspeedmph > windgust_10m[minutes_10m]) 223 | { 224 | windgust_10m[minutes_10m] = windspeedmph; 225 | windgustdirection_10m[minutes_10m] = winddir; 226 | } 227 | 228 | //Check to see if this is a gust for the day 229 | //Resets at midnight each night 230 | if(windspeedmph > windgustmph) 231 | { 232 | windgustmph = windspeedmph; 233 | windgustdir = winddir; 234 | } 235 | 236 | //Blink stat LED briefly to show we are alive 237 | digitalWrite(STAT1, HIGH); 238 | //reportWeather(); //Print the current readings. Takes 172ms. 239 | delay(25); 240 | digitalWrite(STAT1, LOW); 241 | 242 | //If we roll over 60 seconds then update the arrays for rain and windgust 243 | if(++seconds > 59) 244 | { 245 | seconds = 0; 246 | 247 | if(++minutes > 59) minutes = 0; 248 | if(++minutes_10m > 9) minutes_10m = 0; 249 | 250 | rainHour[minutes] = 0; //Zero out this minute's rainfall amount 251 | windgust_10m[minutes_10m] = 0; //Zero out this minute's gust 252 | 253 | minutesSinceLastReset++; //It's been another minute since last night's midnight reset 254 | } 255 | } 256 | 257 | //Check to see if there's been lighting 258 | #ifdef ENABLE_LIGHTNING 259 | if(digitalRead(LIGHTNING_IRQ) == HIGH) 260 | { 261 | //We've got something! 262 | lightning_distance = readLightning(); 263 | } 264 | #endif 265 | 266 | 267 | //Wait for the imp to ping us with the ! character 268 | if(Serial.available()) 269 | { 270 | byte incoming = Serial.read(); 271 | if(incoming == '!') 272 | { 273 | reportWeather(); //Send all the current readings out the imp and to its agent for posting to wunderground. Takes 196ms 274 | //Serial.print("Pinged!"); 275 | 276 | #ifdef ENABLE_LIGHTNING 277 | //Give imp time to transmit then read any erroneous lightning strike 278 | delay(1000); //Give the Imp time to transmit 279 | readLightning(); //Clear any readings and forget it 280 | #endif 281 | 282 | } 283 | else if(incoming == '@') //Special character from Imp indicating midnight local time 284 | { 285 | midnightReset(); //Reset a bunch of variables like rain and daily total rain 286 | //Serial.print("Midnight reset"); 287 | } 288 | else if(incoming == '#') //Special character from Imp indicating a hardware reset 289 | { 290 | //Serial.print("Watchdog reset"); 291 | delay(5000); //This will cause the system to reset because we don't pet the dog 292 | } 293 | } 294 | 295 | //If we go for more than 24 hours without a midnight reset then force a reset 296 | //24 hours * 60 mins/hr = 1,440 minutes + 10 extra minutes. We hope that Imp is doing it. 297 | if(minutesSinceLastReset > (1440 + 10)) 298 | { 299 | midnightReset(); //Reset a bunch of variables like rain and daily total rain 300 | //Serial.print("Emergency midnight reset"); 301 | } 302 | 303 | delay(100); //Update every 100ms. No need to go any faster. 304 | } 305 | 306 | //Prints the various arrays for debugging 307 | void displayArrays() 308 | { 309 | //Windgusts in this hour 310 | Serial.println(); 311 | Serial.print(minutes); 312 | Serial.print(":"); 313 | Serial.println(seconds); 314 | 315 | Serial.print("Windgust last 10 minutes:"); 316 | for(int i = 0 ; i < 10 ; i++) 317 | { 318 | if(i % 10 == 0) Serial.println(); 319 | Serial.print(" "); 320 | Serial.print(windgust_10m[i]); 321 | } 322 | 323 | //Wind speed avg for past 2 minutes 324 | /*Serial.println(); 325 | Serial.print("Wind 2 min avg:"); 326 | for(int i = 0 ; i < 120 ; i++) 327 | { 328 | if(i % 30 == 0) Serial.println(); 329 | Serial.print(" "); 330 | Serial.print(windspdavg[i]); 331 | }*/ 332 | 333 | //Rain for last hour 334 | Serial.println(); 335 | Serial.print("Rain hour:"); 336 | for(int i = 0 ; i < 60 ; i++) 337 | { 338 | if(i % 30 == 0) Serial.println(); 339 | Serial.print(" "); 340 | Serial.print(rainHour[i]); 341 | } 342 | 343 | } 344 | 345 | //When the imp tells us it's midnight, reset the total amount of rain and gusts 346 | void midnightReset() 347 | { 348 | dailyrainin = 0; //Reset daily amount of rain 349 | 350 | windgustmph = 0; //Zero out the windgust for the day 351 | windgustdir = 0; //Zero out the gust direction for the day 352 | 353 | minutes = 0; //Reset minute tracker 354 | seconds = 0; 355 | lastSecond = millis(); //Reset variable used to track minutes 356 | 357 | minutesSinceLastReset = 0; //Zero out the backup midnight reset variable 358 | } 359 | 360 | //Calculates each of the variables that wunderground is expecting 361 | void calcWeather() 362 | { 363 | //current winddir, current windspeed, windgustmph, and windgustdir are calculated every 100ms throughout the day 364 | 365 | //Calc windspdmph_avg2m 366 | float temp = 0; 367 | for(int i = 0 ; i < 120 ; i++) 368 | temp += windspdavg[i]; 369 | temp /= 120.0; 370 | windspdmph_avg2m = temp; 371 | 372 | //Calc winddir_avg2m, Wind Direction 373 | //You can't just take the average. Google "mean of circular quantities" for more info 374 | //We will use the Mitsuta method because it doesn't require trig functions 375 | //And because it sounds cool. 376 | //Based on: http://abelian.org/vlf/bearings.html 377 | //Based on: http://stackoverflow.com/questions/1813483/averaging-angles-again 378 | long sum = winddiravg[0]; 379 | int D = winddiravg[0]; 380 | for(int i = 1 ; i < WIND_DIR_AVG_SIZE ; i++) 381 | { 382 | int delta = winddiravg[i] - D; 383 | 384 | if(delta < -180) 385 | D += delta + 360; 386 | else if(delta > 180) 387 | D += delta - 360; 388 | else 389 | D += delta; 390 | 391 | sum += D; 392 | } 393 | winddir_avg2m = sum / WIND_DIR_AVG_SIZE; 394 | if(winddir_avg2m >= 360) winddir_avg2m -= 360; 395 | if(winddir_avg2m < 0) winddir_avg2m += 360; 396 | 397 | 398 | //Calc windgustmph_10m 399 | //Calc windgustdir_10m 400 | //Find the largest windgust in the last 10 minutes 401 | windgustmph_10m = 0; 402 | windgustdir_10m = 0; 403 | //Step through the 10 minutes 404 | for(int i = 0; i < 10 ; i++) 405 | { 406 | if(windgust_10m[i] > windgustmph_10m) 407 | { 408 | windgustmph_10m = windgust_10m[i]; 409 | windgustdir_10m = windgustdirection_10m[i]; 410 | } 411 | } 412 | 413 | //Calc humidity 414 | humidity = myHumidity.readHumidity(); 415 | //float temp_h = myHumidity.readTemperature(); 416 | //Serial.print(" TempH:"); 417 | //Serial.print(temp_h, 2); 418 | 419 | //Calc tempf from pressure sensor 420 | tempf = myPressure.readTempF(); 421 | //Serial.print(" TempP:"); 422 | //Serial.print(tempf, 2); 423 | 424 | //Total rainfall for the day is calculated within the interrupt 425 | //Calculate amount of rainfall for the last 60 minutes 426 | rainin = 0; 427 | for(int i = 0 ; i < 60 ; i++) 428 | rainin += rainHour[i]; 429 | 430 | //Calc pressure 431 | pressure = myPressure.readPressure(); 432 | 433 | //Calc dewptf 434 | 435 | //Calc light level 436 | light_lvl = get_light_level(); 437 | 438 | //Calc battery level 439 | batt_lvl = get_battery_level(); 440 | 441 | //Lightning is checked in the main loop 442 | } 443 | 444 | //Returns the voltage of the light sensor based on the 3.3V rail 445 | //This allows us to ignore what VCC might be (an Arduino plugged into USB has VCC of 4.5 to 5.2V) 446 | float get_light_level() 447 | { 448 | float operatingVoltage = averageAnalogRead(REFERENCE_3V3); 449 | 450 | float lightSensor = averageAnalogRead(LIGHT); 451 | 452 | operatingVoltage = 3.3 / operatingVoltage; //The reference voltage is 3.3V 453 | 454 | lightSensor *= operatingVoltage; 455 | 456 | return(lightSensor); 457 | } 458 | 459 | //Returns the voltage of the raw pin based on the 3.3V rail 460 | //The battery can ranges from 4.2V down to around 3.3V 461 | //This function allows us to ignore what VCC might be (an Arduino plugged into USB has VCC of 4.5 to 5.2V) 462 | //The weather shield has a pin called RAW (VIN) fed through through two 5% resistors and connected to A2 (BATT): 463 | //3.9K on the high side (R1), and 1K on the low side (R2) 464 | float get_battery_level() 465 | { 466 | float operatingVoltage = averageAnalogRead(REFERENCE_3V3); 467 | 468 | float rawVoltage = averageAnalogRead(BATT); 469 | 470 | operatingVoltage = 3.30 / operatingVoltage; //The reference voltage is 3.3V 471 | 472 | rawVoltage *= operatingVoltage; //Convert the 0 to 1023 int to actual voltage on BATT pin 473 | 474 | rawVoltage *= 4.90; //(3.9k+1k)/1k - multiply BATT voltage by the voltage divider to get actual system voltage 475 | 476 | return(rawVoltage); 477 | } 478 | 479 | //Returns the instataneous wind speed 480 | float get_wind_speed() 481 | { 482 | float deltaTime = millis() - lastWindCheck; //750ms 483 | 484 | deltaTime /= 1000.0; //Covert to seconds 485 | 486 | float windSpeed = (float)windClicks / deltaTime; //3 / 0.750s = 4 487 | 488 | windClicks = 0; //Reset and start watching for new wind 489 | lastWindCheck = millis(); 490 | 491 | windSpeed *= 1.492; //4 * 1.492 = 5.968MPH 492 | 493 | /* Serial.println(); 494 | Serial.print("Windspeed:"); 495 | Serial.println(windSpeed);*/ 496 | 497 | return(windSpeed); 498 | } 499 | 500 | int get_wind_direction() 501 | // read the wind direction sensor, return heading in degrees 502 | { 503 | unsigned int adc; 504 | 505 | adc = averageAnalogRead(WDIR); // get the current reading from the sensor 506 | 507 | // The following table is ADC readings for the wind direction sensor output, sorted from low to high. 508 | // Each threshold is the midpoint between adjacent headings. The output is degrees for that ADC reading. 509 | // Note that these are not in compass degree order! See Weather Meters datasheet for more information. 510 | 511 | if (adc < 380) return (113); 512 | if (adc < 393) return (68); 513 | if (adc < 414) return (90); 514 | if (adc < 456) return (158); 515 | if (adc < 508) return (135); 516 | if (adc < 551) return (203); 517 | if (adc < 615) return (180); 518 | if (adc < 680) return (23); 519 | if (adc < 746) return (45); 520 | if (adc < 801) return (248); 521 | if (adc < 833) return (225); 522 | if (adc < 878) return (338); 523 | if (adc < 913) return (0); 524 | if (adc < 940) return (293); 525 | if (adc < 967) return (315); 526 | if (adc < 990) return (270); 527 | return (-1); // error, disconnected? 528 | } 529 | 530 | //Reports the weather string to the Imp 531 | void reportWeather() 532 | { 533 | calcWeather(); //Go calc all the various sensors 534 | 535 | Serial.print("$,winddir="); 536 | Serial.print(winddir); 537 | Serial.print(",windspeedmph="); 538 | Serial.print(windspeedmph, 1); 539 | Serial.print(",windgustmph="); 540 | Serial.print(windgustmph, 1); 541 | Serial.print(",windgustdir="); 542 | Serial.print(windgustdir); 543 | Serial.print(",windspdmph_avg2m="); 544 | Serial.print(windspdmph_avg2m, 1); 545 | Serial.print(",winddir_avg2m="); 546 | Serial.print(winddir_avg2m); 547 | Serial.print(",windgustmph_10m="); 548 | Serial.print(windgustmph_10m, 1); 549 | Serial.print(",windgustdir_10m="); 550 | Serial.print(windgustdir_10m); 551 | Serial.print(",humidity="); 552 | Serial.print(humidity, 1); 553 | Serial.print(",tempf="); 554 | Serial.print(tempf, 1); 555 | Serial.print(",rainin="); 556 | Serial.print(rainin, 2); 557 | Serial.print(",dailyrainin="); 558 | Serial.print(dailyrainin, 2); 559 | Serial.print(","); //Don't print pressure= because the agent will be doing calcs on the number 560 | Serial.print(pressure, 2); 561 | Serial.print(",batt_lvl="); 562 | Serial.print(batt_lvl, 2); 563 | Serial.print(",light_lvl="); 564 | Serial.print(light_lvl, 2); 565 | 566 | #ifdef LIGHTNING_ENABLED 567 | Serial.print(",lightning_distance="); 568 | Serial.print(lightning_distance); 569 | #endif 570 | 571 | Serial.print(","); 572 | Serial.println("#,"); 573 | 574 | //Test string 575 | //Serial.println("$,winddir=270,windspeedmph=0.0,windgustmph=0.0,windgustdir=0,windspdmph_avg2m=0.0,winddir_avg2m=12,windgustmph_10m=0.0,windgustdir_10m=0,humidity=998.0,tempf=-1766.2,rainin=0.00,dailyrainin=0.00,-999.00,batt_lvl=16.11,light_lvl=3.32,#,"); 576 | } 577 | 578 | //Takes an average of readings on a given pin 579 | //Returns the average 580 | int averageAnalogRead(int pinToRead) 581 | { 582 | byte numberOfReadings = 8; 583 | unsigned int runningValue = 0; 584 | 585 | for(int x = 0 ; x < numberOfReadings ; x++) 586 | runningValue += analogRead(pinToRead); 587 | runningValue /= numberOfReadings; 588 | 589 | return(runningValue); 590 | } 591 | 592 | //The following is for the AS3935 lightning sensor 593 | #ifdef ENABLE_LIGHTNING 594 | byte readLightning(void) 595 | { 596 | byte distance = 0; 597 | 598 | //Check to see if we have lightning! 599 | if(digitalRead(LIGHTNING_IRQ) == HIGH) 600 | { 601 | // first step is to find out what caused interrupt 602 | // as soon as we read interrupt cause register, irq pin goes low 603 | int irqSource = AS3935.interruptSource(); 604 | 605 | // returned value is bitmap field, bit 0 - noise level too high, bit 2 - disturber detected, and finally bit 3 - lightning! 606 | if (irqSource & 0b0001) 607 | { 608 | //Serial.println("Noise level too high, try adjusting noise floor"); 609 | } 610 | 611 | if (irqSource & 0b0100) 612 | { 613 | //Serial.println("Disturber detected"); 614 | distance = 64; 615 | } 616 | 617 | if (irqSource & 0b1000) 618 | { 619 | // need to find how far that lightning stroke, function returns approximate distance in kilometers, 620 | // where value 1 represents storm in detector's near victinity, and 63 - very distant, out of range stroke 621 | // everything in between is just distance in kilometers 622 | distance = AS3935.lightningDistanceKm(); 623 | 624 | //Serial.print("Lightning: "); 625 | //Serial.print(lightning_distance, DEC); 626 | //Serial.println(" km"); 627 | 628 | //The AS3935 remembers the nearest strike distance. For example 15km away then 10, then overhead all following 629 | //distances (10, 20, 30) will instead output as 'Storm overhead, watch out!'. Resetting the chip erases this. 630 | lightning_init(); 631 | } 632 | } 633 | 634 | return(distance); 635 | } 636 | 637 | void startLightning(void) 638 | { 639 | pinMode(slaveSelectPin, OUTPUT); // set the slaveSelectPin as an output: 640 | 641 | pinMode(LIGHTNING_IRQ, INPUT_PULLUP); //Set IRQ pin as input 642 | 643 | SPI.begin(); //Start SPI 644 | 645 | SPI.setDataMode(SPI_MODE1); // NB! chip uses SPI MODE1 646 | 647 | SPI.setClockDivider(SPI_CLOCK_DIV16); //Uno 16MHz / 16 = 1MHz 648 | 649 | SPI.setBitOrder(MSBFIRST); // and chip is MSB first 650 | 651 | lightning_init(); //Setup the values for the sensor 652 | 653 | Serial.println("Lightning sensor online"); 654 | } 655 | 656 | void lightning_init() 657 | { 658 | AS3935.reset(); // reset all internal register values to defaults 659 | 660 | // if lightning detector can not tune tank circuit to required tolerance, 661 | // calibration function will return false 662 | if(!AS3935.calibrate()) 663 | { 664 | Serial.println("Tuning out of range, check your wiring, your sensor and make sure physics laws have not changed!"); 665 | } 666 | 667 | AS3935.setOutdoors(); //The weather station is outdoors 668 | 669 | AS3935.enableDisturbers(); //We want to know if a man-made event happens 670 | AS3935.setNoiseFloor(3); //See table 16 of the AS3935 datasheet. 4-6 works. This was found through experimentation. 671 | 672 | //printAS3935Registers(); 673 | } 674 | 675 | /*void printAS3935Registers() 676 | { 677 | int noiseFloor = AS3935.getNoiseFloor(); 678 | int spikeRejection = AS3935.getSpikeRejection(); 679 | int watchdogThreshold = AS3935.getWatchdogThreshold(); 680 | Serial.print("Noise floor is: "); 681 | Serial.println(noiseFloor, DEC); 682 | Serial.print("Spike rejection is: "); 683 | Serial.println(spikeRejection, DEC); 684 | Serial.print("Watchdog threshold is: "); 685 | Serial.println(watchdogThreshold, DEC); 686 | }*/ 687 | 688 | byte SPItransfer(byte sendByte) 689 | { 690 | return SPI.transfer(sendByte); 691 | } 692 | #endif 693 | -------------------------------------------------------------------------------- /agent.nut: -------------------------------------------------------------------------------- 1 | // This agent gathers data from the device and pushes to Wunderground 2 | // Talks to wunderground rapid fire server (updates of up to once every 10 sec) 3 | // by: Nathan Seidle 4 | // SparkFun Electronics 5 | // date: October 4, 2013 6 | // license: BeerWare 7 | // Please use, reuse, and modify this code as you need. 8 | // We hope it saves you some time, or helps you learn something! 9 | // If you find it handy, and we meet some day, you can buy me a beer or iced tea in return. 10 | 11 | // Example incoming serial string from device: 12 | // $,winddir=270,windspeedmph=0.0,windgustmph=0.0,windgustdir=0,windspdmph_avg2m=0.0,winddir_avg2m=12,windgustmph_10m=0.0,windgustdir_10m=0,humidity=998.0,tempf=-1766.2,rainin=0.00,dailyrainin=0.00,pressure=-999.00,batt_lvl=16.11,light_lvl=3.32,# 13 | 14 | local STATION_ID = "KCOBOULD95"; 15 | local STATION_PW = "password"; //Note that you must only use alphanumerics in your password. Http post won't work otherwise. 16 | 17 | local sparkfun_publicKey = "dZ4EVmE8yGCRGx5XRX1W"; 18 | local sparkfun_privateKey = "privatekey"; 19 | 20 | local LOCAL_ALTITUDE_METERS = 1638; //Accurate for the roof on my house 21 | 22 | local midnightReset = false; //Keeps track of a once per day cumulative rain reset 23 | 24 | local local_hour_offset = 7; //Mountain time is 7 hours off GMT 25 | 26 | const MAX_PROGRAM_SIZE = 0x20000; 27 | const ARDUINO_BLOB_SIZE = 128; 28 | program <- null; 29 | 30 | //------------------------------------------------------------------------------------------------------------------------------ 31 | html <- @" 32 | 33 | 34 |
35 | Program the ATmega328 via the Imp.

36 | Step 1: Select an Intel HEX file to upload:
37 | Step 2: to upload the file.
38 | Step 3: Check out your Arduino
39 |
40 | 41 | 42 | 43 | "; 44 | 45 | //------------------------------------------------------------------------------------------------------------------------------ 46 | // Parses a HTTP POST in multipart/form-data format 47 | function parse_hexpost(req, res) { 48 | local boundary = req.headers["content-type"].slice(30); 49 | local bindex = req.body.find(boundary); 50 | local hstart = bindex + boundary.len(); 51 | local bstart = req.body.find("\r\n\r\n", hstart) + 4; 52 | local fstart = req.body.find("\r\n\r\n--" + boundary + "--", bstart); 53 | return req.body.slice(bstart, fstart); 54 | } 55 | 56 | 57 | //------------------------------------------------------------------------------------------------------------------------------ 58 | // Parses a hex string and turns it into an integer 59 | function hextoint(str) { 60 | local hex = 0x0000; 61 | foreach (ch in str) { 62 | local nibble; 63 | if (ch >= '0' && ch <= '9') { 64 | nibble = (ch - '0'); 65 | } else { 66 | nibble = (ch - 'A' + 10); 67 | } 68 | hex = (hex << 4) + nibble; 69 | } 70 | return hex; 71 | } 72 | 73 | 74 | //------------------------------------------------------------------------------------------------------------------------------ 75 | // Breaks the program into chunks and sends it to the device 76 | function send_program() { 77 | if (program != null && program.len() > 0) { 78 | local addr = 0; 79 | local pline = {}; 80 | local max_addr = program.len(); 81 | 82 | device.send("burn", {first=true}); 83 | while (addr < max_addr) { 84 | program.seek(addr); 85 | pline.data <- program.readblob(ARDUINO_BLOB_SIZE); 86 | pline.addr <- addr / 2; // Address space is 16-bit 87 | device.send("burn", pline) 88 | addr += pline.data.len(); 89 | } 90 | device.send("burn", {last=true}); 91 | } 92 | } 93 | 94 | //------------------------------------------------------------------------------------------------------------------------------ 95 | // Parse the hex into an array of blobs 96 | function parse_hexfile(hex) { 97 | 98 | try { 99 | // Look at this doc to work out what we need and don't. Max is about 122kb. 100 | // https://bluegiga.zendesk.com/entries/42713448--REFERENCE-Updating-BLE11x-firmware-using-UART-DFU 101 | server.log("Parsing hex file"); 102 | 103 | // Create and blank the program blob 104 | program = blob(0x20000); // 128k maximum 105 | for (local i = 0; i < program.len(); i++) program.writen(0x00, 'b'); 106 | program.seek(0); 107 | 108 | local maxaddress = 0, from = 0, to = 0, line = "", offset = 0x00000000; 109 | do { 110 | if (to < 0 || to == null || to >= hex.len()) break; 111 | from = hex.find(":", to); 112 | 113 | if (from < 0 || from == null || from+1 >= hex.len()) break; 114 | to = hex.find(":", from+1); 115 | 116 | if (to < 0 || to == null || from >= to || to >= hex.len()) break; 117 | line = hex.slice(from+1, to); 118 | // server.log(format("[%d,%d] => %s", from, to, line)); 119 | 120 | if (line.len() > 10) { 121 | local len = hextoint(line.slice(0, 2)); 122 | local addr = hextoint(line.slice(2, 6)); 123 | local type = hextoint(line.slice(6, 8)); 124 | 125 | // Ignore all record types except 00, which is a data record. 126 | // Look out for 02 records which set the high order byte of the address space 127 | if (type == 0) { 128 | // Normal data record 129 | } else if (type == 4 && len == 2 && addr == 0 && line.len() > 12) { 130 | // Set the offset 131 | offset = hextoint(line.slice(8, 12)) << 16; 132 | if (offset != 0) { 133 | server.log(format("Set offset to 0x%08X", offset)); 134 | } 135 | continue; 136 | } else { 137 | server.log("Skipped: " + line) 138 | continue; 139 | } 140 | 141 | // Read the data from 8 to the end (less the last checksum byte) 142 | program.seek(offset + addr) 143 | for (local i = 8; i < 8+(len*2); i+=2) { 144 | local datum = hextoint(line.slice(i, i+2)); 145 | program.writen(datum, 'b') 146 | } 147 | 148 | // Checking the checksum would be a good idea but skipped for now 149 | local checksum = hextoint(line.slice(-2)); 150 | 151 | /// Shift the end point forward 152 | if (program.tell() > maxaddress) maxaddress = program.tell(); 153 | 154 | } 155 | } while (from != null && to != null && from < to); 156 | 157 | // Crop, save and send the program 158 | server.log(format("Max address: 0x%08x", maxaddress)); 159 | program.resize(maxaddress); 160 | send_program(); 161 | server.log("Free RAM: " + (imp.getmemoryfree()/1024) + " kb") 162 | return true; 163 | 164 | } catch (e) { 165 | server.log(e) 166 | return false; 167 | } 168 | 169 | } 170 | 171 | 172 | //------------------------------------------------------------------------------------------------------------------------------ 173 | // Handle the agent requests 174 | http.onrequest(function (req, res) { 175 | // return res.send(400, "Bad request"); 176 | // server.log(req.method + " to " + req.path) 177 | if (req.method == "GET") { 178 | res.send(200, html); 179 | } else if (req.method == "POST") { 180 | 181 | if ("content-type" in req.headers) { 182 | if (req.headers["content-type"].len() >= 19 183 | && req.headers["content-type"].slice(0, 19) == "multipart/form-data") { 184 | local hex = parse_hexpost(req, res); 185 | if (hex == "") { 186 | res.header("Location", http.agenturl()); 187 | res.send(302, "HEX file uploaded"); 188 | } else { 189 | device.on("done", function(ready) { 190 | res.header("Location", http.agenturl()); 191 | res.send(302, "HEX file uploaded"); 192 | server.log("Programming completed") 193 | }) 194 | server.log("Programming started") 195 | parse_hexfile(hex); 196 | } 197 | } else if (req.headers["content-type"] == "application/json") { 198 | local json = null; 199 | try { 200 | json = http.jsondecode(req.body); 201 | } catch (e) { 202 | server.log("JSON decoding failed for: " + req.body); 203 | return res.send(400, "Invalid JSON data"); 204 | } 205 | local log = ""; 206 | foreach (k,v in json) { 207 | if (typeof v == "array" || typeof v == "table") { 208 | foreach (k1,v1 in v) { 209 | log += format("%s[%s] => %s, ", k, k1, v1.tostring()); 210 | } 211 | } else { 212 | log += format("%s => %s, ", k, v.tostring()); 213 | } 214 | } 215 | server.log(log) 216 | return res.send(200, "OK"); 217 | } else { 218 | return res.send(400, "Bad request"); 219 | } 220 | } else { 221 | return res.send(400, "Bad request"); 222 | } 223 | } 224 | }) 225 | 226 | 227 | //------------------------------------------------------------------------------------------------------------------------------ 228 | // Handle the device coming online 229 | device.on("ready", function(ready) { 230 | if (ready) send_program(); 231 | }); 232 | 233 | //------------------------------------------------------------------------------------------------------------------------------ 234 | 235 | 236 | // When we hear something from the device, split it apart and post it 237 | device.on("postToInternet", function(dataString) { 238 | 239 | //server.log("Incoming: " + dataString); 240 | 241 | //Break the incoming string into pieces by comma 242 | a <- mysplit(dataString,','); 243 | 244 | if(a[0] != "$" || a[16] != "#") 245 | { 246 | server.log(format("Error: incorrect frame received (%s, %s)", a[0], a[16])); 247 | server.log(format("Received: %s)", dataString)); 248 | return(0); 249 | } 250 | 251 | //Pull the various bits from the blob 252 | 253 | //a[0] is $ 254 | local winddir = a[1]; 255 | local windspeedmph = a[2]; 256 | local windgustmph = a[3]; 257 | local windgustdir = a[4]; 258 | local windspdmph_avg2m = a[5]; 259 | local winddir_avg2m = a[6]; 260 | local windgustmph_10m = a[7]; 261 | local windgustdir_10m = a[8]; 262 | local humidity = a[9]; 263 | local tempf = a[10]; 264 | local rainin = a[11]; 265 | local dailyrainin = a[12]; 266 | local pressure = a[13].tofloat(); 267 | local batt_lvl = a[14]; 268 | local light_lvl = a[15]; 269 | //a[16] is # 270 | 271 | server.log(tempf); 272 | 273 | //Correct for the actual orientation of the weather station 274 | //For my station the north indicator is pointing due west 275 | winddir = windCorrect(winddir); 276 | windgustdir = windCorrect(windgustdir); 277 | winddir_avg2m = windCorrect(winddir_avg2m); 278 | windgustdir_10m = windCorrect(windgustdir_10m); 279 | 280 | //Correct for negative temperatures. This is fixed in the latest libraries: https://learn.sparkfun.com/tutorials/mpl3115a2-pressure-sensor-hookup-guide 281 | currentTemp <- mysplit(tempf, '='); 282 | local badTempf = currentTemp[1].tointeger(); 283 | if(badTempf > 200) 284 | { 285 | local tempc = (badTempf - 32) * 5/9; //Convert F to C 286 | tempc = (tempc<<24)>>24; //Force this 8 bit value into 32 bit variable 287 | tempc = ~(tempc) + 1; //Take 2s compliment 288 | tempc *= -1; //Assign negative sign 289 | tempf = tempc * 9/5 + 32; //Convert back to F 290 | tempf = "tempf=" + tempf; //put a string on it 291 | } 292 | 293 | //Correct for humidity out of bounds 294 | currentHumidity <- mysplit(humidity, '='); 295 | if(currentHumidity[1].tointeger() > 99) humidity = "humidity=99"; 296 | if(currentHumidity[1].tointeger() < 0) humidity = "humidity=0"; 297 | 298 | //Turn Pascal pressure into baromin (Inches Mercury at Altimeter Setting) 299 | local baromin = "baromin=" + convertToInHg(pressure); 300 | 301 | //Calculate a dew point 302 | currentHumidity <- mysplit(humidity, '='); 303 | currentTempF <- mysplit(tempf, '='); 304 | local dewptf = "dewptf=" + calcDewPoint(currentHumidity[1].tointeger(), currentTempF[1].tointeger()); 305 | 306 | //Now we form the large string to pass to wunderground 307 | local strMainSite = "http://rtupdate.wunderground.com/weatherstation/updateweatherstation.php"; 308 | 309 | local strID = "ID=" + STATION_ID; 310 | local strPW = "PASSWORD=" + STATION_PW; 311 | 312 | //Form the current date/time 313 | //Note: .month is 0 to 11! 314 | local currentTime = date(time(), 'u'); 315 | local strCT = "dateutc="; 316 | strCT += currentTime.year + "-" + format("%02d", currentTime.month + 1) + "-" + format("%02d", currentTime.day); 317 | strCT += "+" + format("%02d", currentTime.hour) + "%3A" + format("%02d", currentTime.min) + "%3A" + format("%02d", currentTime.sec); 318 | //Not sure if wunderground expects the + or a %2B. We shall see. 319 | //server.log(strCT); 320 | 321 | local bigString = strMainSite; 322 | bigString += "?" + strID; 323 | bigString += "&" + strPW; 324 | bigString += "&" + strCT; 325 | bigString += "&" + winddir; 326 | bigString += "&" + windspeedmph; 327 | bigString += "&" + windgustmph; 328 | bigString += "&" + windgustdir; 329 | bigString += "&" + windspdmph_avg2m; 330 | bigString += "&" + winddir_avg2m; 331 | bigString += "&" + windgustmph_10m; 332 | bigString += "&" + windgustdir_10m; 333 | bigString += "&" + humidity; 334 | bigString += "&" + tempf; 335 | bigString += "&" + rainin; 336 | bigString += "&" + dailyrainin; 337 | bigString += "&" + baromin; 338 | bigString += "&" + dewptf; 339 | //bigString += "&" + weather; 340 | //bigString += "&" + clouds; 341 | bigString += "&" + "softwaretype=SparkFunWeatherImp"; //Cause we can 342 | bigString += "&" + "realtime=1"; //You better believe it! 343 | bigString += "&" + "rtfreq=10"; //Set rapid fire freq to once every 10 seconds 344 | bigString += "&" + "action=updateraw"; 345 | 346 | //server.log("string to send: " + bigString); 347 | 348 | //Push to Wunderground 349 | local request = http.post(bigString, {}, ""); 350 | local response = request.sendsync(); 351 | server.log("Wunderground response = " + response.body); 352 | server.log(batt_lvl + " " + light_lvl); 353 | 354 | //Get the local time that this measurement was taken 355 | local localMeasurementTime = "measurementtime=" + calcLocalTime(); 356 | 357 | //Now post to data.sparkfun.com 358 | //Here is a list of datums: measurementTime, winddir, windspeedmph, windgustmph, windgustdir, windspdmph_avg2m, winddir_avg2m, windgustmph_10m, windgustdir_10m, humidity, tempf, rainin, dailyrainin, baromin, dewptf, batt_lvl, light_lvl 359 | 360 | //Now we form the large string to pass to sparkfun 361 | local strSparkFun = "http://data.sparkfun.com/input/"; 362 | local privateKey = "private_key=" + sparkfun_privateKey; 363 | 364 | bigString = strSparkFun; 365 | bigString += sparkfun_publicKey; 366 | bigString += "?" + privateKey; 367 | bigString += "&" + localMeasurementTime; 368 | bigString += "&" + winddir; 369 | bigString += "&" + windspeedmph; 370 | bigString += "&" + windgustmph; 371 | bigString += "&" + windgustdir; 372 | bigString += "&" + windspdmph_avg2m; 373 | bigString += "&" + winddir_avg2m; 374 | bigString += "&" + windgustmph_10m; 375 | bigString += "&" + windgustdir_10m; 376 | bigString += "&" + humidity; 377 | bigString += "&" + tempf; 378 | bigString += "&" + rainin; 379 | bigString += "&" + dailyrainin; 380 | bigString += "&" + baromin; 381 | bigString += "&" + dewptf; 382 | bigString += "&" + batt_lvl; 383 | bigString += "&" + light_lvl; 384 | 385 | //Push to SparkFun 386 | local request = http.get(bigString); 387 | local response = request.sendsync(); 388 | server.log("SparkFun response = " + response.body); 389 | 390 | //Check to see if we need to send a midnight reset 391 | checkMidnight(1); 392 | 393 | server.log("Update complete!"); 394 | }); 395 | 396 | //Given a string, break out the direction, correct by some value 397 | //Return a string 398 | function windCorrect(direction) { 399 | temp <- mysplit(direction, '='); 400 | 401 | //My station's North arrow is pointing due west 402 | //So correct by 90 degrees 403 | local dir = temp[1].tointeger() - 90; 404 | if(dir < 0) dir += 360; 405 | return(temp[0] + "=" + dir); 406 | } 407 | 408 | //With relative humidity and temp, calculate a dew point 409 | //From: http://ag.arizona.edu/azmet/dewpoint.html 410 | function calcDewPoint(relativeHumidity, tempF) { 411 | local tempC = (tempF - 32) * 5 / 9.0; 412 | 413 | local L = math.log(relativeHumidity / 100.0); 414 | local M = 17.27 * tempC; 415 | local N = 237.3 + tempC; 416 | local B = (L + (M / N)) / 17.27; 417 | local dewPoint = (237.3 * B) / (1.0 - B); 418 | 419 | //Result is in C 420 | //Convert back to F 421 | dewPoint = dewPoint * 9 / 5.0 + 32; 422 | 423 | //server.log("rh=" + relativeHumidity + " tempF=" + tempF + " tempC=" + tempC); 424 | //server.log("DewPoint = " + dewPoint); 425 | return(dewPoint); 426 | } 427 | 428 | function checkMidnight(ignore) { 429 | //Check to see if it's midnight. If it is, send @ to Arduino to reset time based variables 430 | 431 | //Get the local time that this measurement was taken 432 | local localTime = calcLocalTime(); 433 | 434 | //server.log("Local hour = " + format("%c", localTime[0]) + format("%c", localTime[1])); 435 | 436 | if(localTime[0].tochar() == "0" && localTime[1].tochar() == "4") 437 | { 438 | if(midnightReset == false) 439 | { 440 | server.log("Sending midnight reset"); 441 | midnightReset = true; //We should only reset once 442 | device.send("sendMidnightReset", 1); 443 | } 444 | } 445 | else { 446 | midnightReset = false; //Reset our state 447 | } 448 | } 449 | 450 | //Recording to a google doc is a bit tricky. Many people have found ways of posting 451 | //to a google form to get data into a spreadsheet. This requires a https connection 452 | //so we use pushingbox to handle the secure connection. 453 | //See http://productforums.google.com/forum/#!topic/docs/f4hJKF1OQOw for more info 454 | //To push two items I had to use a GET instead of a post 455 | function recordLevels(batt, light) { 456 | 457 | //Smash it all together 458 | local stringToSend = "http://api.pushingbox.com/pushingbox?devid=vB0A3446EBB4828F"; 459 | stringToSend = stringToSend + "&" + batt; 460 | stringToSend = stringToSend + "&" + light; 461 | //server.log("string to send: " + stringToSend); //Debugging 462 | 463 | //Push to internet 464 | local request = http.post(stringToSend, {}, ""); 465 | local response = request.sendsync(); 466 | //server.log("Google response=" + response.body); 467 | 468 | server.log("Post to spreadsheet complete.") 469 | } 470 | 471 | //Given pressure in pascals, convert the pressure to Altimeter Setting, inches mercury 472 | function convertToInHg(pressure_Pa) 473 | { 474 | local pressure_mb = pressure_Pa / 100; //pressure is now in millibars, 1 pascal = 0.01 millibars 475 | 476 | local part1 = pressure_mb - 0.3; //Part 1 of formula 477 | local part2 = 8.42288 / 100000.0; 478 | local part3 = math.pow((pressure_mb - 0.3), 0.190284); 479 | local part4 = LOCAL_ALTITUDE_METERS / part3; 480 | local part5 = (1.0 + (part2 * part4)); 481 | local part6 = math.pow(part5, (1.0/0.190284)); 482 | local altimeter_setting_pressure_mb = part1 * part6; //Output is now in adjusted millibars 483 | local baromin = altimeter_setting_pressure_mb * 0.02953; 484 | //server.log(format("%s", baromin)); 485 | return(baromin); 486 | } 487 | 488 | //From Hugo: http://forums.electricimp.com/discussion/915/processing-nmea-0183-gps-strings/p1 489 | //You rock! Thanks Hugo! 490 | function mysplit(a, b) { 491 | local ret = []; 492 | local field = ""; 493 | foreach(c in a) { 494 | if (c == b) { 495 | // found separator, push field 496 | ret.push(field); 497 | field=""; 498 | } else { 499 | field += c.tochar(); // append to field 500 | } 501 | } 502 | // Push the last field 503 | ret.push(field); 504 | return ret; 505 | } 506 | 507 | //Given UTC time and a local offset and a date, calculate the local time 508 | //Includes a daylight savings time calc for the US 509 | function calcLocalTime() 510 | { 511 | //Get the time that this measurement was taken 512 | local currentTime = date(time(), 'u'); 513 | local hour = currentTime.hour; //Most of the work will be on the current hour 514 | 515 | //Since 2007 DST starts on the second Sunday in March and ends the first Sunday of November 516 | //Let's just assume it's going to be this way for awhile (silly US government!) 517 | //Example from: http://stackoverflow.com/questions/5590429/calculating-daylight-savings-time-from-only-date 518 | 519 | //The Imp .month returns 0-11. DoW expects 1-12 so we add one. 520 | local month = currentTime.month + 1; 521 | 522 | local DoW = day_of_week(currentTime.year, month, currentTime.day); //Get the day of the week. 0 = Sunday, 6 = Saturday 523 | local previousSunday = currentTime.day - DoW; 524 | 525 | local dst = false; //Assume we're not in DST 526 | if(month > 3 && month < 11) dst = true; //DST is happening! 527 | 528 | //In March, we are DST if our previous Sunday was on or after the 8th. 529 | if (month == 3) 530 | { 531 | if(previousSunday >= 8) dst = true; 532 | } 533 | //In November we must be before the first Sunday to be dst. 534 | //That means the previous Sunday must be before the 1st. 535 | if(month == 11) 536 | { 537 | if(previousSunday <= 0) dst = true; 538 | } 539 | 540 | if(dst == true) 541 | { 542 | hour++; //If we're in DST add an extra hour 543 | } 544 | 545 | //Convert UTC hours to local current time using local_hour 546 | if(hour < local_hour_offset) 547 | hour += 24; //Add 24 hours before subtracting local offset 548 | hour -= local_hour_offset; 549 | 550 | local AMPM = "AM"; 551 | if(hour > 12) 552 | { 553 | hour -= 12; //Get rid of military time 554 | AMPM = "PM"; 555 | } 556 | if(hour == 0) hour = 12; //Midnight edge case 557 | 558 | currentTime = format("%02d", hour) + "%3A" + format("%02d", currentTime.min) + "%3A" + format("%02d", currentTime.sec) + "%20" + AMPM; 559 | //server.log("Local time: " + currentTime); 560 | return(currentTime); 561 | } 562 | 563 | //Given the current year/month/day 564 | //Returns 0 (Sunday) through 6 (Saturday) for the day of the week 565 | //Assumes we are operating in the 2000-2099 century 566 | //From: http://en.wikipedia.org/wiki/Calculating_the_day_of_the_week 567 | function day_of_week(year, month, day) 568 | { 569 | 570 | //offset = centuries table + year digits + year fractional + month lookup + date 571 | local centuries_table = 6; //We assume this code will only be used from year 2000 to year 2099 572 | local year_digits; 573 | local year_fractional; 574 | local month_lookup; 575 | local offset; 576 | 577 | //Example Feb 9th, 2011 578 | 579 | //First boil down year, example year = 2011 580 | year_digits = year % 100; //year_digits = 11 581 | year_fractional = year_digits / 4; //year_fractional = 2 582 | 583 | switch(month) { 584 | case 1: 585 | month_lookup = 0; //January = 0 586 | break; 587 | case 2: 588 | month_lookup = 3; //February = 3 589 | break; 590 | case 3: 591 | month_lookup = 3; //March = 3 592 | break; 593 | case 4: 594 | month_lookup = 6; //April = 6 595 | break; 596 | case 5: 597 | month_lookup = 1; //May = 1 598 | break; 599 | case 6: 600 | month_lookup = 4; //June = 4 601 | break; 602 | case 7: 603 | month_lookup = 6; //July = 6 604 | break; 605 | case 8: 606 | month_lookup = 2; //August = 2 607 | break; 608 | case 9: 609 | month_lookup = 5; //September = 5 610 | break; 611 | case 10: 612 | month_lookup = 0; //October = 0 613 | break; 614 | case 11: 615 | month_lookup = 3; //November = 3 616 | break; 617 | case 12: 618 | month_lookup = 5; //December = 5 619 | break; 620 | default: 621 | month_lookup = 0; //Error! 622 | return(-1); 623 | } 624 | 625 | offset = centuries_table + year_digits + year_fractional + month_lookup + day; 626 | //offset = 6 + 11 + 2 + 3 + 9 = 31 627 | offset %= 7; // 31 % 7 = 3 Wednesday! 628 | 629 | return(offset); //Day of week, 0 to 6 630 | 631 | //Example: May 11th, 2012 632 | //6 + 12 + 3 + 1 + 11 = 33 633 | //5 = Friday! It works! 634 | 635 | //Devised by Tomohiko Sakamoto in 1993, it is accurate for any Gregorian date: 636 | /*t <- [ 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]; 637 | if(month < 3) year--; 638 | //year = month < 3; 639 | return (year + year/4 - year/100 + year/400 + t[month-1] + day) % 7; 640 | //return 4; 641 | */ 642 | } 643 | -------------------------------------------------------------------------------- /device.nut: -------------------------------------------------------------------------------- 1 | // Reads data from a station and pushes it to an agent 2 | // Agent then pushes the weather data to Wunderground 3 | // by: Nathan Seidle 4 | // SparkFun Electronics 5 | // date: October 4, 2013 6 | // license: BeerWare 7 | // Please use, reuse, and modify this code as you need. 8 | // We hope it saves you some time, or helps you learn something! 9 | // If you find it handy, and we meet some day, you can buy me a beer or iced tea in return. 10 | 11 | 12 | 13 | local rxLEDToggle = 1; // These variables keep track of rx/tx LED toggling status 14 | local txLEDToggle = 1; 15 | 16 | local NOCHAR = -1; 17 | 18 | //--------------------------------------------- 19 | //Everything below is used for bootloading 20 | 21 | server.log("Device started, impee_id " + hardware.getimpeeid() + " and mac = " + imp.getmacaddress() ); 22 | 23 | //------------------------------------------------------------------------------------------------------------------------------ 24 | // Uart57 for TX/RX 25 | SERIAL <- hardware.uart57; 26 | SERIAL.configure(115200, 8, PARITY_NONE, 1, NO_CTSRTS); 27 | 28 | // Set pin1 high for normal operation 29 | // Set pin1 low to reset a standard Arduino 30 | RESET <- hardware.pin1; 31 | //RESET.configure(DIGITAL_OUT); //This causes the board to stick in reset state? Not sure. 32 | RESET.write(1); //Leave Arduino in normal (non-reset) state 33 | 34 | // Pin 9 is the yellow LED on the Imp Shield 35 | ACTIVITY <- hardware.pin9; 36 | ACTIVITY.configure(DIGITAL_OUT); 37 | ACTIVITY.write(1); 38 | 39 | // Pin 8 is the orange LED 40 | LINK <- hardware.pin8; 41 | LINK.configure(DIGITAL_OUT); 42 | LINK.write(1); 43 | 44 | // Sequence number 45 | __seq <- 0x28; 46 | 47 | //------------------------------------------------------------------------------------------------------------------------------ 48 | /* STK500 constants list, from AVRDUDE */ 49 | const MESSAGE_START = 0x1B; 50 | const TOKEN = 0x0E; 51 | const STK_OK = 0x10; 52 | const STK_FAILED = 0x11; // Not used 53 | const STK_UNKNOWN = 0x12; // Not used 54 | const STK_NODEVICE = 0x13; // Not used 55 | const STK_INSYNC = 0x14; // ' ' 56 | const STK_NOSYNC = 0x15; // Not used 57 | const ADC_CHANNEL_ERROR = 0x16; // Not used 58 | const ADC_MEASURE_OK = 0x17; // Not used 59 | const PWM_CHANNEL_ERROR = 0x18; // Not used 60 | const PWM_ADJUST_OK = 0x19; // Not used 61 | const CRC_EOP = 0x20; // 'SPACE' 62 | const STK_GET_SYNC = 0x30; // '0' 63 | const STK_GET_SIGN_ON = 0x31; // '1' 64 | const STK_SET_PARAMETER = 0x40; // '@' 65 | const STK_GET_PARAMETER = 0x41; // 'A' 66 | const STK_SET_DEVICE = 0x42; // 'B' 67 | const STK_SET_DEVICE_EXT = 0x45; // 'E' 68 | const STK_ENTER_PROGMODE = 0x50; // 'P' 69 | const STK_LEAVE_PROGMODE = 0x51; // 'Q' 70 | const STK_CHIP_ERASE = 0x52; // 'R' 71 | const STK_CHECK_AUTOINC = 0x53; // 'S' 72 | const STK_LOAD_ADDRESS = 0x55; // 'U' 73 | const STK_UNIVERSAL = 0x56; // 'V' 74 | const STK_PROG_FLASH = 0x60; // '`' 75 | const STK_PROG_DATA = 0x61; // 'a' 76 | const STK_PROG_FUSE = 0x62; // 'b' 77 | const STK_PROG_LOCK = 0x63; // 'c' 78 | const STK_PROG_PAGE = 0x64; // 'd' 79 | const STK_PROG_FUSE_EXT = 0x65; // 'e' 80 | const STK_READ_FLASH = 0x70; // 'p' 81 | const STK_READ_DATA = 0x71; // 'q' 82 | const STK_READ_FUSE = 0x72; // 'r' 83 | const STK_READ_LOCK = 0x73; // 's' 84 | const STK_READ_PAGE = 0x74; // 't' 85 | const STK_READ_SIGN = 0x75; // 'u' 86 | const STK_READ_OSCCAL = 0x76; // 'v' 87 | const STK_READ_FUSE_EXT = 0x77; // 'w' 88 | const STK_READ_OSCCAL_EXT = 0x78; // 'x' 89 | 90 | 91 | //------------------------------------------------------------------------------------------------------------------------------ 92 | function HEXDUMP(buf, len = null) { 93 | if (buf == null) return "null"; 94 | if (len == null) { 95 | len = (typeof buf == "blob") ? buf.tell() : buf.len(); 96 | } 97 | 98 | local dbg = ""; 99 | for (local i = 0; i < len; i++) { 100 | local ch = buf[i]; 101 | dbg += format("0x%02X ", ch); 102 | } 103 | 104 | return format("%s (%d bytes)", dbg, len) 105 | } 106 | 107 | 108 | //------------------------------------------------------------------------------------------------------------------------------ 109 | function SERIAL_READ(len = 100, timeout = 300) { 110 | 111 | local rxbuf = blob(len); 112 | local write = rxbuf.writen.bindenv(rxbuf); 113 | local read = SERIAL.read.bindenv(SERIAL); 114 | local hw = hardware; 115 | local ms = hw.millis.bindenv(hw); 116 | local started = ms(); 117 | 118 | local charsRead = 0; 119 | LINK.write(0); //Turn LED on 120 | do { 121 | local ch = read(); 122 | if (ch != -1) { 123 | write(ch, 'b') 124 | charsRead++; 125 | if(charsRead == len) break; 126 | } 127 | } while (ms() - started < timeout); 128 | LINK.write(1); //Turn LED off 129 | 130 | // Clean up any extra bytes 131 | while (SERIAL.read() != -1); 132 | 133 | if (rxbuf.tell() == 0) { 134 | return null; 135 | } else { 136 | return rxbuf; 137 | } 138 | } 139 | 140 | 141 | //------------------------------------------------------------------------------------------------------------------------------ 142 | function execute(command = null, param = null, response_length = 100, response_timeout = 300) { 143 | 144 | local send_buffer = null; 145 | if (command == null) { 146 | send_buffer = format("%c", CRC_EOP); 147 | } else if (param == null) { 148 | send_buffer = format("%c%c", command, CRC_EOP); 149 | } else if (typeof param == "array") { 150 | send_buffer = format("%c", command); 151 | foreach (datum in param) { 152 | switch (typeof datum) { 153 | case "string": 154 | case "blob": 155 | case "array": 156 | case "table": 157 | foreach (adat in datum) { 158 | send_buffer += format("%c", adat); 159 | } 160 | break; 161 | default: 162 | send_buffer += format("%c", datum); 163 | } 164 | } 165 | send_buffer += format("%c", CRC_EOP); 166 | } else { 167 | send_buffer = format("%c%c%c", command, param, CRC_EOP); 168 | } 169 | 170 | // server.log("Sending: " + HEXDUMP(send_buffer)); 171 | SERIAL.write(send_buffer); 172 | 173 | local resp_buffer = SERIAL_READ(response_length+2, response_timeout); 174 | // server.log("Received: " + HEXDUMP(resp_buffer)); 175 | 176 | assert(resp_buffer != null); 177 | assert(resp_buffer.tell() >= 2); 178 | assert(resp_buffer[0] == STK_INSYNC); 179 | assert(resp_buffer[resp_buffer.tell()-1] == STK_OK); 180 | 181 | local tell = resp_buffer.tell(); 182 | if (tell == 2) return blob(0); 183 | resp_buffer.seek(1); 184 | return resp_buffer.readblob(tell-2); 185 | } 186 | 187 | 188 | //------------------------------------------------------------------------------------------------------------------------------ 189 | function check_duino() { 190 | // Clear the read buffer 191 | SERIAL_READ(); 192 | 193 | // Check everything we can check to ensure we are speaking to the correct boot loader 194 | local major = execute(STK_GET_PARAMETER, 0x81, 1); 195 | local minor = execute(STK_GET_PARAMETER, 0x82, 1); 196 | local invalid = execute(STK_GET_PARAMETER, 0x83, 1); 197 | local signature = execute(STK_READ_SIGN); 198 | assert(major.len() == 1 && minor.len() == 1); 199 | assert((major[0] >= 5) || (major[0] == 4 && minor[0] >= 4)); 200 | assert(invalid.len() == 1 && invalid[0] == 0x03); 201 | assert(signature.len() == 3 && signature[0] == 0x1E && signature[1] == 0x95 && signature[2] == 0x0F); 202 | } 203 | 204 | 205 | //------------------------------------------------------------------------------------------------------------------------------ 206 | function program_duino(address16, data) { 207 | 208 | local addr8_hi = (address16 >> 8) & 0xFF; 209 | local addr8_lo = address16 & 0xFF; 210 | local data_len = data.len(); 211 | 212 | execute(STK_LOAD_ADDRESS, [addr8_lo, addr8_hi], 0); 213 | execute(STK_PROG_PAGE, [0x00, data_len, 0x46, data], 0) 214 | local data_check = execute(STK_READ_PAGE, [0x00, data_len, 0x46], data_len) 215 | 216 | assert(data_check.len() == data_len); 217 | for (local i = 0; i < data_len; i++) { 218 | assert(data_check[i] == data[i]); 219 | } 220 | 221 | } 222 | 223 | 224 | //------------------------------------------------------------------------------------------------------------------------------ 225 | function bounce() { 226 | 227 | // Bounce the reset pin 228 | server.log("Bouncing the Arduino reset pin"); 229 | RESET.configure(DIGITAL_OUT); //We need control of the reset pin 230 | 231 | imp.sleep(0.5); 232 | ACTIVITY.write(0); //Turn on LED 233 | RESET.write(0); //Reset Arduino 234 | imp.sleep(0.2); 235 | RESET.write(1); //Return reset to high, bootloader on Arduino now begins 236 | imp.sleep(0.3); 237 | check_duino(); 238 | ACTIVITY.write(1); //Turn off LED 239 | 240 | RESET.configure(DIGITAL_IN); //Give up control of reset pin or else the board seems to reset on each wakeup 241 | 242 | } 243 | 244 | //------------------------------------------------------------------------------------------------------------------------------ 245 | function scan_serial() { 246 | local ch = null; 247 | local str = ""; 248 | do { 249 | ch = SERIAL.read(); 250 | if (ch != -1 && ch != 0) { 251 | str += format("%c", ch); 252 | } 253 | } while (ch != -1); 254 | 255 | if (str.len() > 0) { 256 | server.log("Serial: " + str); 257 | } 258 | } 259 | 260 | //------------------------------------------------------------------------------------------------------------------------------ 261 | function burn(pline) { 262 | 263 | if ("first" in pline) { 264 | server.log("Starting to burn"); 265 | SERIAL.configure(115200, 8, PARITY_NONE, 1, NO_CTSRTS); 266 | bounce(); 267 | } else if ("last" in pline) { 268 | server.log("Done!") 269 | agent.send("done", true); 270 | SERIAL.configure(9600, 8, PARITY_NONE, 1, NO_CTSRTS, checkWeather); 271 | } else { 272 | program_duino(pline.addr, pline.data); 273 | } 274 | 275 | } 276 | 277 | //--------------------------------------------- 278 | //Everything above is used for bootloading 279 | 280 | function toggleTxLED() 281 | { 282 | txLEDToggle = 1 - txLEDToggle; // toggle the txLEDtoggle variable 283 | ACTIVITY.write(txLEDToggle); // TX LED is on pin 8 (active-low) 284 | } 285 | 286 | function toggleRxLED() 287 | { 288 | rxLEDToggle = 1 - rxLEDToggle; // toggle the rxLEDtoggle variable 289 | LINK.write(rxLEDToggle); // RX LED is on pin 8 (active-low) 290 | } 291 | 292 | //When the agent detects a midnight cross over, send a reset to arduino 293 | //This resets the cumulative rain and other daily variables 294 | agent.on("sendMidnightReset", function(ignore) { 295 | server.log("Device midnight reset"); 296 | SERIAL.write("@"); //Special midnight command 297 | }); 298 | 299 | // Send a character to the Arduino to gather the latest data 300 | // Pass that data onto the Agent for parsing and posting to Wunderground 301 | function checkWeather() { 302 | 303 | //Get all the various bits from the Arduino over UART 304 | server.log("Gathering new weather data"); 305 | 306 | //Clean out any previous characters in any buffers 307 | SERIAL.flush(); 308 | 309 | //Ping the Arduino with the ! character to get the latest data 310 | SERIAL.write("!"); 311 | 312 | //Wait for initial character to come in 313 | local counter = 0; 314 | local result = NOCHAR; 315 | while(result == NOCHAR) 316 | { 317 | result = SERIAL.read(); //Wait for a new character to arrive 318 | 319 | imp.sleep(0.01); 320 | if(counter++ > 200) //2 seconds 321 | { 322 | server.log("Serial timeout error initial"); 323 | return(0); //Bail after 2000ms max wait 324 | } 325 | } 326 | //server.log("Counter: " + counter); 327 | 328 | // Collect bytes 329 | local incomingStream = ""; 330 | while (result != '\n') // Keep reading until we see a newline 331 | { 332 | counter = 0; 333 | while(result == NOCHAR) 334 | { 335 | result = SERIAL.read(); 336 | 337 | if(result == NOCHAR) 338 | { 339 | imp.sleep(0.01); 340 | if(counter++ > 20) //Wait no more than 20ms for another character 341 | { 342 | server.log("Serial timeout error"); 343 | return(0); //Bail after 20ms max wait 344 | } 345 | } 346 | } 347 | 348 | //server.log("Test: " + format("%c", result)); // Display in log window 349 | 350 | incomingStream += format("%c", result); 351 | toggleTxLED(); // Toggle the TX LED 352 | 353 | result = SERIAL.read(); //Grab the next character in the que 354 | } 355 | 356 | 357 | server.log("We heard: " + format("%s", incomingStream)); // Display in log window 358 | server.log("Arduino read complete"); 359 | 360 | ACTIVITY.write(1); //TX LED off 361 | 362 | // Send info to agent, that will in turn push to internet 363 | agent.send("postToInternet", incomingStream); 364 | 365 | //imp.wakeup(10.0, checkWeather); 366 | } 367 | 368 | //These are needed for the wireless reprogramming 369 | agent.on("burn", burn); 370 | agent.send("ready", true); 371 | 372 | SERIAL.configure(9600, 8, PARITY_NONE, 1, NO_CTSRTS); // 9600 baud worked well, no parity, 1 stop bit, 8 data bits 373 | 374 | // Start this party going! 375 | checkWeather(); 376 | 377 | //Power down the imp to low power mode, then wake up after 10 seconds 378 | //Wunderground has a minimum of 2.5 seconds between Rapidfire reports 379 | imp.onidle(function() { 380 | server.log("Nothing to do, going to sleep for 60 seconds"); 381 | server.sleepfor(60); 382 | }); 383 | --------------------------------------------------------------------------------