/* * LM: This is two sketches in one. Select the approprite role to load. * Set the constant MASTER = true (This is the application master role) or * false (This is the application remote role) * * The remote application monitors the state of the mailbox door and * transmits a message to the master, whenever the state changes from * shut to open or from open to shut. * * The master application indicates the most recent activity visually, * either via LED or OLED or both. * * The master application records messages received and the time that * they were received in EEPROM. * * The master application also indicates the most recent activity visually, * either via LED or OLED or both. * * Rev.1T Sep. 2021 - Modification to prevent OLED burnout and port to * Teensy 3.5 * OLED displays message for specified duration, then goes dark * Push-button enables timed redisplay of last message. * */ const boolean MASTER = true; // See multi-line comment above const boolean SET_RTC = false; // If true, set RTC to the date & time this sketch was compiled const boolean RTC_TEST_MODE = false; // Displays time, updating once per second const boolean RTC_ONLY = false; // Bypass rest of loop when in RTC_TEST_MODE const boolean SOUND_TEST = false; // For tweaking audio filter etc. const boolean INIT_EEPROM = false; // Set true once before deployment, then false // Ground DIO pin 4 for EEPROM dump to Serial #include #include // OLED #include #include "nRF24L01.h" // Radio #include "RF24.h" #include // Non-volatile storage #include "RTClib.h" // Real-time clock #include // OLED display address #define OLED_ADDR 0x3C // Constructors Adafruit_SSD1306 display(128, 64); // Width, Height RF24 radio(9,10); RTC_DS3231 rtc; // Oscillator i2c address is 0x68 (can't be changed) const uint64_t pipe = 0xE8E8F0F0E1LL; //Define pipe const int MAX_MESSAGE = 64; char TXmsg [MAX_MESSAGE] = ""; char RXmsg [MAX_MESSAGE] = ""; #define BATTERY_STATUS A0 // Analog input for sensing battery voltage #define DOOR_SWITCH 3 // DIO pin for door switch (Remote sender context) #define DUMP_SWITCH 4 // DIO pin for indicating EEPROM dump to serial port (Master receiver context) #define DOOR_STATE_LED 5 // Indicator that mailbox door is open or shut (Not presently used) #define TONE_PWM 7 #define NEW_ACTIVITY 8 // Visual indicator of activity since last redisplay // Note: DIO 9, 10, 11, 12, 13 are used by RF-24 module (SPI) // RF-24 IRQ pin is NOT connected #define REDISPLAY_PB A1 // Rev.1 - [Re]enable timed display #define OPEN HIGH #define SHUT LOW // Reverse sense if necessary (mailbox end) // Tones const int BOO = 500; const int BEE = 900; const int BOOP = BOO; const int BEEP = BEE; const int A440 = 440; // General const int ADD_SECONDS = 17; // Number of seconds to add to adjust for compile and upload const unsigned long DEBOUNCE = 100; const unsigned long TONE_DURATION = 150; const unsigned long MILLISECONDS_BETWEEN_TONES = 150; const unsigned long MOMENT = 250; const unsigned long HALF_SECOND = 500; const unsigned long ONESEC = 1000; const unsigned long TWOSEC = 2000; // Startup delay boolean lastShut = true; // Last stable state of mailbox door int door = SHUT; // Alternative to boolean variable lastShut // Rev 1C const unsigned long DISPLAY_ON_DURATION = 60000; unsigned long display_timeout = 0; // Current millis() + DISPLAY_ON_DURATION // Time - i2c from RTC module: A4=SDA, A5=SCL #define STX 0x02 #define ETX 0x03 //RTC boolean rtcON; // Flag indicating RTC present (found) and successfully initialized const char SLANT = '/'; // Date formatting const char COLON = ':'; // Time formatting boolean timeUpdateReceived = false; DateTime rtcNow; // From rtc.now() int iYR; int iMON; int iDAY; int iHR; int iMN; int iSC; String sDate = ""; String sTime = ""; long last_time_update = 0; // EEPROM const int EE_RESERVED = 4; // Number of reserved bytes, starting at 0 String sTmp; // Avoid encapsulated string declaration // Redisplay boolean activity_detected = false; String lastBatteryVoltage = ""; void setup() { radio.begin(); if (MASTER) { pinMode(DUMP_SWITCH, INPUT_PULLUP); pinMode(DOOR_STATE_LED, OUTPUT); pinMode(TONE_PWM, OUTPUT); pinMode(NEW_ACTIVITY, OUTPUT); pinMode(REDISPLAY_PB, INPUT); // initialize read from radio radio.openReadingPipe(1,pipe); radio.startListening(); // initialize real time clock rtcON = rtc.begin(); if (rtcON && SET_RTC) rtc.adjust(DateTime(F(__DATE__), F(__TIME__)) + TimeSpan(0, 0, 0, ADD_SECONDS)); } else { pinMode(DOOR_SWITCH, INPUT); // Switch has 10K pullup resistor radio.openWritingPipe(pipe); analogReference(INTERNAL); } if (MASTER) { // initialize and clear display display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR); display.clearDisplay(); display.display(); testTone(); displaySplash(); // Rev 1 delay(TWOSEC); // Override default timeout oledClear(); if (digitalRead(DUMP_SWITCH) == LOW) { Serial.begin(9600); dumpEEPROM(); } if (INIT_EEPROM) { eraseEEPROM(EEPROM.length()); recordPlaceEEPROM(EE_RESERVED); } clearNewActivityIndicator(); } } void loop() { if (MASTER) { // Data receiver if (rtcON && RTC_TEST_MODE) { if (last_time_update + ONESEC < millis()) { // oledClear(); oledDisplayTime(); last_time_update = millis(); } if (RTC_ONLY) return; // Clock only test } // Wait for data from radio if ( radio.available() ) { while (radio.available()) { int len = radio.getDynamicPayloadSize(); radio.read(&RXmsg, len); } processReceivedData(); } if (isDisplayTimeout()) processDisplayTimeout(); if (isRedisplayPB()) processRedisplay(); } else { // Remote sensor / transmitter // Rewrite if (digitalRead(DOOR_SWITCH) == OPEN) { if (door == SHUT) { door = OPEN; transmitShut2Open(); transmitBatteryStatus(); } } else if (door == OPEN) { door = SHUT; transmitOpen2Shut(); transmitBatteryStatus(); } } } // RADIO void clearRxBuffer() { RXmsg[0] = (byte) 0; } void clearTxBuffer() { TXmsg[0] = (byte) 0; } void processReceivedData() { // Check validity and complete processing if ((RXmsg[0] == '1') || (RXmsg[0] == '2')) setNewActivityIndicator(); if (RXmsg[0] == '1') { receiveShut2Open(); } else if (RXmsg[0] == '2') { receiveOpen2Shut(); } else if (RXmsg[0] == '3') { receiveBatteryStatus((byte) RXmsg[1]); } // Other messages here clearRxBuffer(); } void transmitBatteryStatus() { TXmsg[0] = '3'; TXmsg[1] = (byte) (analogRead(BATTERY_STATUS) / 4); TXmsg[2] = (byte) 0; transmitMessage(3); // Length } void transmitShut2Open() { TXmsg[0] = '2'; TXmsg[1] = (byte) 0; transmitMessage(2); // Length } void transmitOpen2Shut() { TXmsg[0] = '1'; TXmsg[1] = (byte) 0; transmitMessage(2); // Length } void receiveShut2Open() { boobeep(); oledDisplayOpenMessage(); digitalWrite(DOOR_STATE_LED, OPEN); storeEventToEEPROM('1'); // Record to EEPROM // Post to IOT delay(HALF_SECOND); } void receiveOpen2Shut() { beeboop(); oledDisplayShutMessage(); digitalWrite(DOOR_STATE_LED, SHUT); storeEventToEEPROM('2'); // Record to EEPROM // Post to IOT delay(HALF_SECOND); } void receiveBatteryStatus(byte b) { int v = b * 44 / 255; String sV = String(v); sV = sV.substring(0, 1) + '.' + sV.substring(1); lastBatteryVoltage = ""; // For redisplay lastBatteryVoltage.concat(sV); if (b >= 250) oledDisplayText("B.Full", 1, 80, 45); else oledDisplayText("B " + sV + " v", 1, 80, 45); } void transmitMessage(int msgLength) { radio.write(&TXmsg, msgLength); // transmitting the string clearTxBuffer(); } // DISPLAY (Master) void oledClear() { display.clearDisplay(); display.display(); } void oledDisplayText(String s, int size, int x_coord, int y_coord) { // display a line of text setDisplayTimeout(); display.setTextSize(size); display.setTextColor(WHITE); display.setCursor(x_coord,y_coord); display.print(s); display.display(); } void oledDisplayOpenMessage() { oledClear(); oledDisplayText("Mailbox", 1, 0, 0); oledDisplayText("opened.", 1, 0, 15); oledDisplayTime(); } void oledDisplayShutMessage() { oledClear(); oledDisplayText("Mailbox", 1, 0, 0); oledDisplayText("closed.", 1, 0, 15); oledDisplayTime(); } void oledDisplayTime() { if (rtcON) { readRTC(); formatDate(); oledClearLine(30, 1); oledDisplayText(sDate, 1, 0, 30); formatTime(); oledClearLine(45, 1); oledDisplayText(sTime, 1, 0, 45); if (SOUND_TEST) if (iSC == 0) beeboop(); else if (iSC % 30 == 0) boobeep(); } } void oledClearLine(int jPos, int jVsize) { display.fillRect(0, jPos, display.width(), jVsize*10, BLACK); } // EEPROM (Master) void writeEEPROM(String s) { int sLen = s.length(); if (sLen > EEPROM.length()) return; for (int i=0; i (EEPROM.length() - jStart)) return; for (int i=0; i EEPROM.length()) sLen = EEPROM.length(); sTmp = ""; for (int i=0; i (EEPROM.length() - jStart)) sLen = EEPROM.length() - jStart; sTmp = ""; for (int i=0; i EEPROM.length()) len = EEPROM.length(); for (int i=0; i (EEPROM.length() - jStart)) len = EEPROM.length() - jStart; for (int i=0; i EEPROM.length()) jPlace = EE_RESERVED; writeEEPROM(sTmp, jPlace); recordPlaceEEPROM(jPlace +sTmp.length()); } void dumpEEPROM() { // Format and print contents of EEPROM to serial port char c; for (int i=EE_RESERVED; i 31) && ((byte) c < 127)) Serial.print(c); } } // Sounds cribbed from Dave Benson's Hilltopper sketch - void boobeep () { tone (TONE_PWM,BOO,TONE_DURATION); delay(MILLISECONDS_BETWEEN_TONES); tone (TONE_PWM,BEEP,TONE_DURATION); } void beeboop () { tone (TONE_PWM,BEE,TONE_DURATION); delay (MILLISECONDS_BETWEEN_TONES); tone (TONE_PWM,BOOP,TONE_DURATION); } // Adapted from NTP sketch void readRTC() { // Read date and time from RTC activity_detected = true; rtcNow = rtc.now(); iYR = rtcNow.year(); iMON = rtcNow.month(); iDAY = rtcNow.day(); iHR = rtcNow.hour(); iMN = rtcNow.minute(); iSC = rtcNow.second(); } void formatDate() { sDate = String(iMON); sDate.concat(SLANT); sDate.concat(String(iDAY)); sDate.concat(SLANT); sDate.concat(String(iYR).substring(2)); } void formatTime() { sTime = String(iHR); sTime.concat(COLON); if (String(iMN).length() == 1) sTime.concat("0"); sTime.concat(String(iMN)); sTime.concat(COLON); // Do seconds matter? if (String(iSC).length() == 1) sTime.concat("0"); sTime.concat(String(iSC)); } // Rev 1C void testTone() { // At startup tone(TONE_PWM,A440,MOMENT); delay(HALF_SECOND); tone(TONE_PWM,A440,MOMENT); } void displaySplash() { // Encapsulate oledDisplayText("Initialization", 1, 0, 0); oledDisplayText("complete", 1, 4, 10); } void setDisplayTimeout() { display_timeout = millis() + DISPLAY_ON_DURATION; } boolean isDisplayTimeout() { //if ((display_timeout) > 0 && (millis() > display_timeout)) if (millis() > display_timeout) return true; else return false; } boolean isRedisplayPB() { if (digitalRead(REDISPLAY_PB) == LOW) { while (digitalRead(REDISPLAY_PB) == LOW) ; delay(DEBOUNCE); return true; } else return false; } void processDisplayTimeout() { oledClear(); } void setNewActivityIndicator() { digitalWrite(NEW_ACTIVITY, LOW); // Sink LED (on) } void clearNewActivityIndicator() { digitalWrite(NEW_ACTIVITY, HIGH); // Reset LED (off) } void processRedisplay() { oledClear(); if (sTime == "") oledDisplayText("No recent activity", 1, 0, 50); else { oledDisplayText("Last activity on:", 1, 0, 15); oledDisplayText(sDate, 1, 0, 30); oledDisplayText("at", 1, 55, 30); oledDisplayText(sTime, 1, 0, 45); } if (lastBatteryVoltage.length() > 0) oledDisplayText("B " + lastBatteryVoltage + " v", 1, 80, 45); clearNewActivityIndicator(); } // Debug void oledDisplayDebug(char c) { oledDisplayText("Debug " + String(c), 1, 0, 50); delay(HALF_SECOND); oledClearLine(50,1); } void oledDisplayDebugChar(int letterCode) { // letterCode is 1 for 'A', 2 for 'B', etc. char c = char(letterCode+64); display.setTextSize(1); display.setTextColor(WHITE); display.setCursor(0, 50); display.print("Debug "); display.print(c); display.display(); delay(HALF_SECOND); display.fillRect(0, 50, display.width(), 10, BLACK); delay(MOMENT); }