Statistics
| Revision:

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
}