root / RF12 / examples / roomNode / roomNode.pde
History | View | Annotate | Download (9 KB)
| 1 | //>>> The latest version of this code can be found at https://github.com/jcw/ !! |
|---|---|
| 2 | |
| 3 | // New version of the Room Node, derived from rooms.pde |
| 4 | // 2010-10-19 <jcw@equi4.com> http://opensource.org/licenses/mit-license.php |
| 5 | // $Id: roomNode.pde 7763 2011-12-11 01:28:16Z jcw $ |
| 6 | |
| 7 | // see http://jeelabs.org/2010/10/20/new-roomnode-code/ |
| 8 | // and http://jeelabs.org/2010/10/21/reporting-motion/ |
| 9 | |
| 10 | // The complexity in the code below comes from the fact that newly detected PIR |
| 11 | // motion needs to be reported as soon as possible, but only once, while all the |
| 12 | // other sensor values are being collected and averaged in a more regular cycle. |
| 13 | |
| 14 | #include <Ports.h> |
| 15 | #include <PortsSHT11.h> |
| 16 | #include <RF12.h> |
| 17 | #include <avr/sleep.h> |
| 18 | #include <util/atomic.h> |
| 19 | |
| 20 | #define SERIAL 1 // set to 1 to also report readings on the serial port |
| 21 | #define DEBUG 0 // set to 1 to display each loop() run and PIR trigger |
| 22 | |
| 23 | #define SHT11_PORT 1 // defined if SHT11 is connected to a port |
| 24 | #define LDR_PORT 4 // defined if LDR is connected to a port's AIO pin |
| 25 | #define PIR_PORT 4 // defined if PIR is connected to a port's DIO pin |
| 26 | |
| 27 | #define MEASURE_PERIOD 600 // how often to measure, in tenths of seconds |
| 28 | #define RETRY_PERIOD 10 // how soon to retry if ACK didn't come in |
| 29 | #define RETRY_LIMIT 5 // maximum number of times to retry |
| 30 | #define ACK_TIME 10 // number of milliseconds to wait for an ack |
| 31 | #define REPORT_EVERY 5 // report every N measurement cycles |
| 32 | #define SMOOTH 3 // smoothing factor used for running averages |
| 33 | |
| 34 | // set the sync mode to 2 if the fuses are still the Arduino default |
| 35 | // mode 3 (full powerdown) can only be used with 258 CK startup fuses |
| 36 | #define RADIO_SYNC_MODE 2 |
| 37 | |
| 38 | // The scheduler makes it easy to perform various tasks at various times: |
| 39 | |
| 40 | enum { MEASURE, REPORT, TASK_END };
|
| 41 | |
| 42 | static word schedbuf[TASK_END]; |
| 43 | Scheduler scheduler (schedbuf, TASK_END); |
| 44 | |
| 45 | // Other variables used in various places in the code: |
| 46 | |
| 47 | static byte reportCount; // count up until next report, i.e. packet send |
| 48 | static byte myNodeID; // node ID used for this unit |
| 49 | |
| 50 | // This defines the structure of the packets which get sent out by wireless: |
| 51 | |
| 52 | struct {
|
| 53 | byte light; // light sensor: 0..255 |
| 54 | byte moved :1; // motion detector: 0..1 |
| 55 | byte humi :7; // humidity: 0..100 |
| 56 | int temp :10; // temperature: -500..+500 (tenths) |
| 57 | byte lobat :1; // supply voltage dropped under 3.1V: 0..1 |
| 58 | } payload; |
| 59 | |
| 60 | // Conditional code, depending on which sensors are connected and how: |
| 61 | |
| 62 | #if SHT11_PORT |
| 63 | SHT11 sht11 (SHT11_PORT); |
| 64 | #endif |
| 65 | |
| 66 | #if LDR_PORT |
| 67 | Port ldr (LDR_PORT); |
| 68 | #endif |
| 69 | |
| 70 | #if PIR_PORT |
| 71 | #define PIR_HOLD_TIME 30 // hold PIR value this many seconds after change |
| 72 | #define PIR_PULLUP 1 // set to one to pull-up the PIR input pin |
| 73 | #define PIR_FLIP 1 // 0 or 1, to match PIR reporting high or low |
| 74 | |
| 75 | class PIR : public Port {
|
| 76 | volatile byte value, changed; |
| 77 | volatile uint32_t lastOn; |
| 78 | public: |
| 79 | PIR (byte portnum) |
| 80 | : Port (portnum), value (0), changed (0), lastOn (0) {}
|
| 81 | |
| 82 | // this code is called from the pin-change interrupt handler |
| 83 | void poll() {
|
| 84 | // see http://talk.jeelabs.net/topic/811#post-4734 for PIR_FLIP |
| 85 | byte pin = digiRead() ^ PIR_FLIP; |
| 86 | // if the pin just went on, then set the changed flag to report it |
| 87 | if (pin) {
|
| 88 | if (!state()) |
| 89 | changed = 1; |
| 90 | lastOn = millis(); |
| 91 | } |
| 92 | value = pin; |
| 93 | } |
| 94 | |
| 95 | // state is true if curr value is still on or if it was on recently |
| 96 | byte state() const {
|
| 97 | byte f = value; |
| 98 | if (lastOn > 0) |
| 99 | ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
| 100 | if (millis() - lastOn < 1000 * PIR_HOLD_TIME) |
| 101 | f = 1; |
| 102 | } |
| 103 | return f; |
| 104 | } |
| 105 | |
| 106 | // return true if there is new motion to report |
| 107 | byte triggered() {
|
| 108 | byte f = changed; |
| 109 | changed = 0; |
| 110 | return f; |
| 111 | } |
| 112 | }; |
| 113 | |
| 114 | PIR pir (PIR_PORT); |
| 115 | |
| 116 | // the PIR signal comes in via a pin-change interrupt |
| 117 | ISR(PCINT2_vect) { pir.poll(); }
|
| 118 | #endif |
| 119 | |
| 120 | // has to be defined because we're using the watchdog for low-power waiting |
| 121 | ISR(WDT_vect) { Sleepy::watchdogEvent(); }
|
| 122 | |
| 123 | // utility code to perform simple smoothing as a running average |
| 124 | static int smoothedAverage(int prev, int next, byte firstTime =0) {
|
| 125 | if (firstTime) |
| 126 | return next; |
| 127 | return ((SMOOTH - 1) * prev + next + SMOOTH / 2) / SMOOTH; |
| 128 | } |
| 129 | |
| 130 | // spend a little time in power down mode while the SHT11 does a measurement |
| 131 | static void shtDelay () {
|
| 132 | Sleepy::loseSomeTime(32); // must wait at least 20 ms |
| 133 | } |
| 134 | |
| 135 | // wait a few milliseconds for proper ACK to me, return true if indeed received |
| 136 | static byte waitForAck() {
|
| 137 | MilliTimer ackTimer; |
| 138 | while (!ackTimer.poll(ACK_TIME)) {
|
| 139 | if (rf12_recvDone() && rf12_crc == 0 && |
| 140 | // see http://talk.jeelabs.net/topic/811#post-4712 |
| 141 | rf12_hdr == (RF12_HDR_DST | RF12_HDR_CTL | myNodeID)) |
| 142 | return 1; |
| 143 | set_sleep_mode(SLEEP_MODE_IDLE); |
| 144 | sleep_mode(); |
| 145 | } |
| 146 | return 0; |
| 147 | } |
| 148 | |
| 149 | // readout all the sensors and other values |
| 150 | static void doMeasure() {
|
| 151 | byte firstTime = payload.humi == 0; // special case to init running avg |
| 152 | |
| 153 | payload.lobat = rf12_lowbat(); |
| 154 | |
| 155 | #if SHT11_PORT |
| 156 | #ifndef __AVR_ATtiny84__ |
| 157 | sht11.measure(SHT11::HUMI, shtDelay); |
| 158 | sht11.measure(SHT11::TEMP, shtDelay); |
| 159 | float h, t; |
| 160 | sht11.calculate(h, t); |
| 161 | int humi = h + 0.5, temp = 10 * t + 0.5; |
| 162 | #else |
| 163 | //XXX TINY! |
| 164 | int humi = 50, temp = 25; |
| 165 | #endif |
| 166 | payload.humi = smoothedAverage(payload.humi, humi, firstTime); |
| 167 | payload.temp = smoothedAverage(payload.temp, temp, firstTime); |
| 168 | #endif |
| 169 | #if LDR_PORT |
| 170 | ldr.digiWrite2(1); // enable AIO pull-up |
| 171 | byte light = ~ ldr.anaRead() >> 2; |
| 172 | ldr.digiWrite2(0); // disable pull-up to reduce current draw |
| 173 | payload.light = smoothedAverage(payload.light, light, firstTime); |
| 174 | #endif |
| 175 | #if PIR_PORT |
| 176 | payload.moved = pir.state(); |
| 177 | #endif |
| 178 | } |
| 179 | |
| 180 | // periodic report, i.e. send out a packet and optionally report on serial port |
| 181 | static void doReport() {
|
| 182 | rf12_sleep(RF12_WAKEUP); |
| 183 | while (!rf12_canSend()) |
| 184 | rf12_recvDone(); |
| 185 | rf12_sendStart(0, &payload, sizeof payload, RADIO_SYNC_MODE); |
| 186 | rf12_sleep(RF12_SLEEP); |
| 187 | |
| 188 | #if SERIAL |
| 189 | Serial.print("ROOM ");
|
| 190 | Serial.print((int) payload.light); |
| 191 | Serial.print(' ');
|
| 192 | Serial.print((int) payload.moved); |
| 193 | Serial.print(' ');
|
| 194 | Serial.print((int) payload.humi); |
| 195 | Serial.print(' ');
|
| 196 | Serial.print((int) payload.temp); |
| 197 | Serial.print(' ');
|
| 198 | Serial.print((int) payload.lobat); |
| 199 | Serial.println(); |
| 200 | delay(2); // make sure tx buf is empty before going back to sleep |
| 201 | #endif |
| 202 | } |
| 203 | |
| 204 | // send packet and wait for ack when there is a motion trigger |
| 205 | static void doTrigger() {
|
| 206 | #if DEBUG |
| 207 | Serial.print("PIR ");
|
| 208 | Serial.print((int) payload.moved); |
| 209 | delay(2); |
| 210 | #endif |
| 211 | |
| 212 | for (byte i = 0; i < RETRY_LIMIT; ++i) {
|
| 213 | rf12_sleep(RF12_WAKEUP); |
| 214 | while (!rf12_canSend()) |
| 215 | rf12_recvDone(); |
| 216 | rf12_sendStart(RF12_HDR_ACK, &payload, sizeof payload, RADIO_SYNC_MODE); |
| 217 | byte acked = waitForAck(); |
| 218 | rf12_sleep(RF12_SLEEP); |
| 219 | |
| 220 | if (acked) {
|
| 221 | #if DEBUG |
| 222 | Serial.print(" ack ");
|
| 223 | Serial.println((int) i); |
| 224 | delay(2); |
| 225 | #endif |
| 226 | // reset scheduling to start a fresh measurement cycle |
| 227 | scheduler.timer(MEASURE, MEASURE_PERIOD); |
| 228 | return; |
| 229 | } |
| 230 | |
| 231 | Sleepy::loseSomeTime(RETRY_PERIOD * 100); |
| 232 | } |
| 233 | scheduler.timer(MEASURE, MEASURE_PERIOD); |
| 234 | #if DEBUG |
| 235 | Serial.println(" no ack!");
|
| 236 | delay(2); |
| 237 | #endif |
| 238 | } |
| 239 | |
| 240 | void setup () {
|
| 241 | #if SERIAL || DEBUG |
| 242 | Serial.begin(57600); |
| 243 | Serial.print("\n[roomNode.3]");
|
| 244 | myNodeID = rf12_config(); |
| 245 | #else |
| 246 | myNodeID = rf12_config(0); // don't report info on the serial port |
| 247 | #endif |
| 248 | |
| 249 | rf12_sleep(RF12_SLEEP); // power down |
| 250 | |
| 251 | #if PIR_PORT |
| 252 | pir.digiWrite(PIR_PULLUP); |
| 253 | #ifdef PCMSK2 |
| 254 | bitSet(PCMSK2, PIR_PORT + 3); |
| 255 | bitSet(PCICR, PCIE2); |
| 256 | #else |
| 257 | //XXX TINY! |
| 258 | #endif |
| 259 | #endif |
| 260 | |
| 261 | reportCount = REPORT_EVERY; // report right away for easy debugging |
| 262 | scheduler.timer(MEASURE, 0); // start the measurement loop going |
| 263 | } |
| 264 | |
| 265 | void loop () {
|
| 266 | #if DEBUG |
| 267 | Serial.print('.');
|
| 268 | delay(2); |
| 269 | #endif |
| 270 | |
| 271 | #if PIR_PORT |
| 272 | if (pir.triggered()) {
|
| 273 | payload.moved = pir.state(); |
| 274 | doTrigger(); |
| 275 | } |
| 276 | #endif |
| 277 | |
| 278 | switch (scheduler.pollWaiting()) {
|
| 279 | |
| 280 | case MEASURE: |
| 281 | // reschedule these measurements periodically |
| 282 | scheduler.timer(MEASURE, MEASURE_PERIOD); |
| 283 | |
| 284 | doMeasure(); |
| 285 | |
| 286 | // every so often, a report needs to be sent out |
| 287 | if (++reportCount >= REPORT_EVERY) {
|
| 288 | reportCount = 0; |
| 289 | scheduler.timer(REPORT, 0); |
| 290 | } |
| 291 | break; |
| 292 | |
| 293 | case REPORT: |
| 294 | doReport(); |
| 295 | break; |
| 296 | } |
| 297 | } |