├── .gitattributes ├── .gitignore ├── Documentation └── Mailbox Notifier E32.docx ├── LICENSE ├── LetterBoxE32_ATTINY_Battery └── LetterBoxE32_ATTINY_Battery.ino ├── LetterBoxE32_ATTINY_Testprogram └── LetterBoxE32_ATTINY_Testprogram.ino ├── LetterBoxE32_ATTINY_V2 └── LetterBoxE32_ATTINY_V2.ino ├── LetterBoxE32_Test_ESP32 └── LetterBoxE32_Test_ESP32.ino ├── LetterBoxGatewayV2 ├── LetterBoxGatewayV2.ino └── Secrets1.h ├── LetterboxGatewayHA └── LetterboxGatewayHA.ino ├── README.md ├── STL Files ├── Mailbox Notifier ATTINY v2_Mailbox Notifier ATTINY v2_Enclosure_1_Case_1_Body1.stl ├── Mailbox Notifier ATTINY v2_Mailbox Notifier ATTINY v2_Enclosure_1_Top Cover_1_Body1.stl ├── Notifier Gateway v10_Notifier Gateway v10_Enclosure_1_Base_1_Body1.stl └── Notifier Gateway v10_Notifier Gateway v10_Enclosure_1_Top_1_Body1.stl └── States.pdf /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | secrets.h 2 | -------------------------------------------------------------------------------- /Documentation/Mailbox Notifier E32.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/LoRa_Mailbox_Notifier/d711a56c890b501e75fb8ed9cee90ba3f754daa4/Documentation/Mailbox Notifier E32.docx -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 SensorsIOT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LetterBoxE32_ATTINY_Battery/LetterBoxE32_ATTINY_Battery.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // #define SWITCH_OPENING 1 //PA5 for prototype 5 | #define SWITCH_OPENING 10 //PA3 6 | #define SWITCH_DOOR 0 //PA4 7 | #define M0 7 //PB0 8 | #define M1 6 //PB1 9 | #define AUX 3 // PA7 10 | #define LED 1 // PA5 for testing only 11 | 12 | #define FULL 0x55 13 | #define EMPTY 0xAA 14 | #define ACKNOWLEDGE 0x25 15 | 16 | bool transmitted = false; 17 | bool acknowledged = false; 18 | unsigned long transmissionTime = millis(); 19 | int retransmissions = 0; 20 | bool opening_pressed = false; 21 | bool door_pressed = false; 22 | 23 | enum boxStatus { 24 | empty, 25 | full 26 | } mailBoxStatus = empty; 27 | 28 | enum programstat { 29 | boxinit, 30 | boxfilled, 31 | boxemptied, 32 | boxfull, 33 | boxempty, 34 | waitackfull, 35 | waitackempty 36 | } programStatus; 37 | 38 | // --- Battery Voltage Reading --- 39 | // This function uses the internal 1.1V reference to measure Vcc in millivolts. 40 | // (Adapted from common Arduino techniques.) 41 | uint16_t readBatteryVoltage() { 42 | // set the ADC to measure the internal 1.1V bandgap voltage against Vcc 43 | // ADMUX: REFS0=1 (use Vcc as reference), MUX[3:1]=111 to select 1.1V (see datasheet) 44 | ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1); 45 | delay(2); // Wait for the reference to settle 46 | ADCSRA |= _BV(ADSC); // Start conversion 47 | while (ADCSRA & _BV(ADSC)) ; // Wait until conversion is complete 48 | uint8_t low = ADCL; 49 | uint8_t high = ADCH; 50 | uint16_t result = (high << 8) | low; 51 | // The typical constant is 1.1 (in volts) * 1023 * 1000 = 1125300. 52 | uint32_t vcc = 1125300UL / result; // Vcc in millivolts 53 | return (uint16_t)vcc; 54 | } 55 | 56 | // This helper function sends the status byte plus the two bytes of battery voltage. 57 | void transmitWithBattery(byte status) { 58 | Serial.write(status); 59 | uint16_t batt = readBatteryVoltage(); 60 | // Send the high byte then the low byte. 61 | Serial.write(highByte(batt)); 62 | Serial.write(lowByte(batt)); 63 | Serial.flush(); 64 | } 65 | 66 | void receive() { 67 | byte _receivedCode = 0; 68 | if (Serial.available() > 0) { 69 | while (Serial.available()) { 70 | _receivedCode = Serial.read(); 71 | if (_receivedCode == ACKNOWLEDGE) { 72 | acknowledged = true; 73 | } 74 | } 75 | } 76 | } 77 | 78 | // needs one ISR for each interrupt pin 79 | void wakeUpDoor() { 80 | sleep_disable(); 81 | detachInterrupt(digitalPinToInterrupt(SWITCH_DOOR)); 82 | // needed (I do not know why) 83 | digitalWrite(M0, LOW); 84 | digitalWrite(M1, LOW); 85 | opening_pressed = false; 86 | door_pressed = true; 87 | } 88 | 89 | void wakeUpOpening() { 90 | sleep_disable(); 91 | detachInterrupt(digitalPinToInterrupt(SWITCH_OPENING)); 92 | // needed (I do not know why) 93 | digitalWrite(M0, LOW); 94 | digitalWrite(M1, LOW); 95 | opening_pressed = true; 96 | door_pressed = false; 97 | } 98 | 99 | void goToSleep() { 100 | /* 101 | Serial.print("opening_pressed "); 102 | Serial.print(opening_pressed); 103 | Serial.print(" door_pressed "); 104 | Serial.println(door_pressed); 105 | while (digitalRead(SWITCH_OPENING) != HIGH && digitalRead(SWITCH_DOOR != HIGH)) { // debounce 106 | delay(100); 107 | } 108 | Serial.println(" go to sleep"); 109 | */ 110 | Serial.flush(); 111 | digitalWrite(M0, HIGH); 112 | digitalWrite(M1, HIGH); 113 | attachInterrupt(digitalPinToInterrupt(SWITCH_OPENING), wakeUpOpening, LOW); 114 | attachInterrupt(digitalPinToInterrupt(SWITCH_DOOR), wakeUpDoor, LOW); 115 | set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Choose sleep mode 116 | sleep_enable(); // Enable sleep mode 117 | sleep_cpu(); // Go to sleep 118 | } 119 | 120 | void setup() { 121 | // Initialize pins 0 to 11 as INPUT_PULLUP to save deepsleep current 122 | for (byte i = 0; i <= 11; i++) pinMode(i, INPUT_PULLUP); 123 | Serial.begin(9600); 124 | pinMode(M0, OUTPUT); 125 | pinMode(M1, OUTPUT); 126 | pinMode(LED, OUTPUT); 127 | digitalWrite(M0, HIGH); 128 | digitalWrite(M1, HIGH); 129 | pinMode(AUX, INPUT_PULLUP); 130 | pinMode(SWITCH_OPENING, INPUT_PULLUP); 131 | pinMode(SWITCH_DOOR, INPUT_PULLUP); 132 | 133 | delay(100); 134 | // Initialize the module (for 20dBm Module) 135 | byte data[] = { 0xC0, 0x0, 0x1, 0x1A, 0x17, 0x44 }; 136 | for (unsigned int i = 0; i < sizeof(data); i++) { 137 | Serial.write(data[i]); 138 | } 139 | delay(100); 140 | // Enter sleep until an event occurs. 141 | goToSleep(); 142 | digitalWrite(M0, LOW); 143 | digitalWrite(M1, LOW); 144 | // On wakeup, transmit the initial EMPTY status along with battery voltage. 145 | transmitWithBattery(EMPTY); 146 | } 147 | 148 | void loop() { 149 | switch (programStatus) { 150 | case boxinit: 151 | // exit: 152 | if (digitalRead(SWITCH_OPENING) == LOW) programStatus = boxfilled; 153 | if (digitalRead(SWITCH_DOOR) == LOW) programStatus = boxemptied; 154 | break; 155 | case boxfilled: 156 | // Transmit FULL status plus battery voltage. 157 | transmitWithBattery(FULL); 158 | transmissionTime = millis(); 159 | retransmissions = 0; 160 | programStatus = waitackfull; 161 | break; 162 | case boxemptied: 163 | // Transmit EMPTY status plus battery voltage. 164 | transmitWithBattery(EMPTY); 165 | transmissionTime = millis(); 166 | retransmissions = 0; 167 | programStatus = waitackempty; 168 | break; 169 | case boxfull: 170 | goToSleep(); 171 | // exit: 172 | if (door_pressed) programStatus = boxemptied; 173 | break; 174 | case boxempty: 175 | goToSleep(); 176 | // exit: 177 | if (opening_pressed) programStatus = boxfilled; 178 | break; 179 | case waitackfull: 180 | if (acknowledged) { 181 | acknowledged = false; 182 | programStatus = boxfull; 183 | } else { 184 | if ((millis() - transmissionTime > 1000) && (retransmissions < 5)) { // 5 retransmissions max every second 185 | transmitWithBattery(FULL); 186 | retransmissions++; 187 | transmissionTime = millis(); 188 | } 189 | if (retransmissions >= 5) programStatus = boxfull; // go to status anyway 190 | } 191 | break; 192 | case waitackempty: 193 | if (acknowledged) { 194 | acknowledged = false; 195 | programStatus = boxempty; 196 | } else { 197 | if ((millis() - transmissionTime > 1000) && (retransmissions < 5)) { // 5 retransmissions max every second 198 | transmitWithBattery(EMPTY); 199 | retransmissions++; 200 | transmissionTime = millis(); 201 | } 202 | if (retransmissions >= 5) programStatus = boxempty; // go to status anyway 203 | } 204 | break; 205 | default: 206 | break; 207 | } 208 | 209 | receive(); 210 | } 211 | 212 | -------------------------------------------------------------------------------- /LetterBoxE32_ATTINY_Testprogram/LetterBoxE32_ATTINY_Testprogram.ino: -------------------------------------------------------------------------------- 1 | 2 | // This program generates full and empty messages on 433MHz 3 | 4 | 5 | #define M0 6 //PB0 6 | #define M1 7 //PB1 7 | #define AUX 3 // PA7 8 | 9 | #define TXD2 17 10 | #define RXD2 16 11 | 12 | 13 | #define FULL 0x55 14 | #define EMPTY 0xAA 15 | #define ACKNOWLEDGE 0x25 16 | 17 | bool transmitted = false; 18 | bool acknowledged = false; 19 | unsigned long transmissionTime = millis(); 20 | int retransmissions = 0; 21 | bool opening_pressed = false; 22 | bool door_pressed = false; 23 | 24 | void waitForAUX() { 25 | while (digitalRead(AUX) == LOW) 26 | delay(10); 27 | } 28 | 29 | 30 | 31 | 32 | void setup() { 33 | delay(1000); 34 | Serial.begin(9600); 35 | pinMode(M0, OUTPUT); 36 | pinMode(M1, OUTPUT); 37 | pinMode(AUX, INPUT_PULLUP); 38 | 39 | digitalWrite(M0, HIGH); 40 | digitalWrite(M1, HIGH); 41 | delay(100); 42 | byte data1[] = { 0xC4, 0xC4, 0xC4 }; 43 | Serial.write(data1, sizeof(data1)); 44 | Serial.flush(); 45 | delay(100); 46 | //byte data[] = { 0xC0, 0x0, 0x1, 0x1A, 0x17, 0x47 }; // for 30dBm Module 47 | byte data[] = { 0xC0, 0x0, 0x1, 0x1A, 0x17, 0x44 }; // for 20dBm Module 48 | Serial.write(data, sizeof(data)); 49 | delay(100); 50 | waitForAUX(); 51 | digitalWrite(M0, LOW); 52 | digitalWrite(M1, LOW); 53 | } 54 | 55 | 56 | void loop() { 57 | waitForAUX(); 58 | Serial.write(FULL); 59 | //Serial.println("Full"); 60 | delay(5000); 61 | waitForAUX(); 62 | Serial.write(EMPTY); 63 | //Serial.println("empty"); 64 | delay(2000); 65 | } 66 | -------------------------------------------------------------------------------- /LetterBoxE32_ATTINY_V2/LetterBoxE32_ATTINY_V2.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | //--------------------------------- 5 | // Pin Definitions 6 | #define SWITCH_OPENING 10 // Opening switch (e.g. mailbox door opening) 7 | #define SWITCH_DOOR 0 // Door switch (e.g. physical door press) 8 | #define M0 6 // Module control: M0 pin 9 | #define M1 7 // Module control: M1 pin 10 | #define AUX 3 // Module ready indicator (AUX pin) 11 | #define LED 1 // LED for testing (optional) 12 | 13 | // Mailbox signal definitions 14 | #define FULL 0x55 // Signal: mailbox filled (mail deposited) 15 | #define EMPTY 0xAA // Signal: mailbox empty (mail removed) 16 | #define ACKNOWLEDGE 0x25 // Signal: acknowledgment received 17 | 18 | //--------------------------------- 19 | // Global Variables & Program State 20 | bool acknowledged = false; 21 | unsigned long transmissionTime = 0; // Time when a transmission was sent 22 | int retransmissions = 0; 23 | volatile bool opening_pressed = false; 24 | volatile bool door_pressed = false; 25 | byte signal; // Signal to send (FULL or EMPTY) 26 | 27 | // Program states for mailbox events 28 | enum ProgramState { 29 | boxfilled, // Mailbox has been filled (mail deposited) 30 | boxemptied, // Mailbox has been emptied (mail removed) 31 | boxfull, // Mailbox remains full; waiting for door action 32 | boxempty, // Mailbox remains empty; waiting for opening action 33 | waitackfull, // Waiting for ACK after sending FULL 34 | waitackempty // Waiting for ACK after sending EMPTY 35 | } programStatus = boxempty; // Start with an empty mailbox 36 | 37 | //--------------------------------- 38 | // Module Programming Configuration 39 | // Expected parameter configurations for the two module types 40 | byte expected_30dBm[6] = { 0xC0, 0x00, 0x01, 0x1A, 0x17, 0x47 }; 41 | byte expected_20dBm[6] = { 0xC0, 0x00, 0x01, 0x1A, 0x17, 0x44 }; 42 | 43 | // 44 | // ======================== MODULE CONTROL FUNCTIONS ======================== 45 | // 46 | 47 | // Wait until the AUX pin indicates that the module is ready. 48 | void wait_aux() { 49 | while (digitalRead(AUX) == LOW) { 50 | delay(1); 51 | } 52 | delay(5); // Additional delay for stability 53 | } 54 | 55 | // Set the module to programming (configuration) mode. 56 | void set_programming() { 57 | digitalWrite(M0, HIGH); 58 | digitalWrite(M1, HIGH); 59 | delay(20); 60 | wait_aux(); 61 | } 62 | 63 | // Set the module to transparent (normal operation) mode. 64 | void set_transparent() { 65 | digitalWrite(M0, LOW); 66 | digitalWrite(M1, LOW); 67 | delay(20); 68 | wait_aux(); 69 | } 70 | 71 | // Send a command over Serial using a String. 72 | void sendCommand(const String& command) { 73 | wait_aux(); 74 | Serial.write(command.c_str(), command.length()); 75 | Serial.flush(); 76 | delay(30); 77 | } 78 | 79 | // Send a command over Serial using a byte array. 80 | void sendCommand(const byte* command, size_t len) { 81 | wait_aux(); 82 | Serial.write(command, len); 83 | Serial.flush(); 84 | delay(30); 85 | } 86 | 87 | // Request the stored configuration and compare it with the expected configuration. 88 | // If a mismatch is detected, reprogram the module. 89 | void sendCommandIfMismatch(const byte* expected, size_t len) { 90 | // Request the stored configuration from the module. 91 | sendCommand("\xC1\xC1\xC1"); 92 | 93 | // Wait until 6 bytes are available on Serial. 94 | while (Serial.available() < 6) { 95 | delay(10); 96 | } 97 | 98 | // Read the 6-byte current configuration from the module. 99 | byte currentConfig[6]; 100 | Serial.readBytes(currentConfig, 6); 101 | 102 | // Compare each byte with the expected configuration. 103 | bool mismatch = false; 104 | for (size_t i = 0; i < len; i++) { 105 | if (currentConfig[i] != expected[i]) { 106 | mismatch = true; 107 | break; 108 | } 109 | } 110 | 111 | // If a mismatch is detected, program the module with the expected parameters. 112 | if (mismatch) { 113 | sendCommand(expected, len); 114 | } 115 | } 116 | 117 | // Program the E32 module: switch to programming mode, request configuration, 118 | // determine module type from the response, and program the module if needed. 119 | bool programming_E32() { 120 | set_programming(); 121 | sendCommand("\xC3\xC3\xC3"); // Request module configuration 122 | 123 | // Wait until 4 bytes are available. 124 | while (Serial.available() < 4) { 125 | delay(10); 126 | } 127 | 128 | byte received[4]; 129 | Serial.readBytes(received, 4); 130 | 131 | // Determine module type based on the last byte of the response. 132 | const byte* expectedConfig; 133 | if (received[3] == 0x1E) { 134 | expectedConfig = expected_30dBm; 135 | } else { 136 | expectedConfig = expected_20dBm; 137 | } 138 | 139 | // Compare the current configuration and program if necessary. 140 | sendCommandIfMismatch(expectedConfig, 6); 141 | 142 | // Return the module to transparent mode. 143 | set_transparent(); 144 | return true; 145 | } 146 | 147 | // 148 | // ======================== INTERRUPT & SLEEP FUNCTIONS ======================== 149 | // 150 | 151 | // ISR: Wake up when the door switch is activated. 152 | void wakeUpDoor() { 153 | sleep_disable(); 154 | detachInterrupt(digitalPinToInterrupt(SWITCH_DOOR)); 155 | digitalWrite(M0, LOW); 156 | digitalWrite(M1, LOW); 157 | door_pressed = true; 158 | opening_pressed = false; 159 | } 160 | 161 | // ISR: Wake up when the opening switch is activated. 162 | void wakeUpOpening() { 163 | sleep_disable(); 164 | detachInterrupt(digitalPinToInterrupt(SWITCH_OPENING)); 165 | digitalWrite(M0, LOW); 166 | digitalWrite(M1, LOW); 167 | opening_pressed = true; 168 | door_pressed = false; 169 | } 170 | 171 | // Put the module into sleep mode with interrupts enabled on the switch pins. 172 | void goToSleep() { 173 | Serial.flush(); 174 | // Set the module to a state that minimizes power consumption. 175 | digitalWrite(M0, HIGH); 176 | digitalWrite(M1, HIGH); 177 | attachInterrupt(digitalPinToInterrupt(SWITCH_OPENING), wakeUpOpening, LOW); 178 | attachInterrupt(digitalPinToInterrupt(SWITCH_DOOR), wakeUpDoor, LOW); 179 | set_sleep_mode(SLEEP_MODE_PWR_DOWN); 180 | sleep_enable(); 181 | sleep_cpu(); 182 | } 183 | 184 | // 185 | // ======================== COMMUNICATION FUNCTIONS ======================== 186 | // 187 | 188 | // Check for incoming data on Serial and update acknowledgment flag if ACK is received. 189 | void receive() { 190 | if (Serial.available() > 0) { 191 | byte incoming = Serial.read(); 192 | if (incoming == ACKNOWLEDGE) { 193 | acknowledged = true; 194 | } 195 | } 196 | } 197 | 198 | // 199 | // ======================== SETUP & LOOP FUNCTIONS ======================== 200 | // 201 | 202 | void setup() { 203 | // Configure pins 0 to 11 as INPUT_PULLUP to reduce power consumption during sleep. 204 | for (byte i = 0; i <= 11; i++) { 205 | pinMode(i, INPUT_PULLUP); 206 | } 207 | Serial.begin(9600); 208 | 209 | // Configure control and switch pins. 210 | pinMode(M0, OUTPUT); 211 | pinMode(M1, OUTPUT); 212 | pinMode(LED, OUTPUT); 213 | pinMode(AUX, INPUT_PULLUP); 214 | pinMode(SWITCH_OPENING, INPUT_PULLUP); 215 | pinMode(SWITCH_DOOR, INPUT_PULLUP); 216 | 217 | // Program the module (reprogramming occurs only if configuration mismatches). 218 | programming_E32(); 219 | 220 | // Start with the mailbox in the empty state and send the EMPTY signal. 221 | signal = EMPTY; 222 | sendCommand(&signal, 1); 223 | 224 | // Enter sleep mode until an external event (switch activation) occurs. 225 | goToSleep(); 226 | } 227 | 228 | void loop() { 229 | switch (programStatus) { 230 | case boxfilled: 231 | signal = FULL; 232 | sendCommand(&signal, 1); 233 | transmissionTime = millis(); 234 | retransmissions = 0; 235 | programStatus = waitackfull; 236 | break; 237 | 238 | case boxemptied: 239 | signal = EMPTY; 240 | sendCommand(&signal, 1); 241 | transmissionTime = millis(); 242 | retransmissions = 0; 243 | programStatus = waitackempty; 244 | break; 245 | 246 | case boxfull: 247 | // Wait for a door action (to empty the mailbox). 248 | goToSleep(); 249 | if (door_pressed) { 250 | programStatus = boxemptied; 251 | } 252 | break; 253 | 254 | case boxempty: 255 | // Wait for an opening action (to fill the mailbox). 256 | goToSleep(); 257 | if (opening_pressed) { 258 | programStatus = boxfilled; 259 | } 260 | break; 261 | 262 | case waitackfull: 263 | if (acknowledged) { 264 | acknowledged = false; 265 | programStatus = boxfull; 266 | } else { 267 | if ((millis() - transmissionTime > 1000) && (retransmissions < 5)) { 268 | signal = FULL; 269 | sendCommand(&signal, 1); 270 | Serial.flush(); 271 | retransmissions++; 272 | transmissionTime = millis(); 273 | } 274 | if (retransmissions >= 5) { 275 | programStatus = boxfull; 276 | } 277 | } 278 | break; 279 | 280 | case waitackempty: 281 | if (acknowledged) { 282 | acknowledged = false; 283 | programStatus = boxempty; 284 | } else { 285 | if ((millis() - transmissionTime > 1000) && (retransmissions < 5)) { 286 | signal = EMPTY; 287 | sendCommand(&signal, 1); 288 | retransmissions++; 289 | transmissionTime = millis(); 290 | } 291 | if (retransmissions >= 5) { 292 | programStatus = boxempty; 293 | } 294 | } 295 | break; 296 | 297 | default: 298 | break; 299 | } 300 | 301 | // Continuously check for incoming data to update the acknowledgment status. 302 | receive(); 303 | } 304 | -------------------------------------------------------------------------------- /LetterBoxE32_Test_ESP32/LetterBoxE32_Test_ESP32.ino: -------------------------------------------------------------------------------- 1 | #define M0 7 //PB0 2 | #define M1 6 //PB1 3 | #define AUX 3 // PA7 4 | 5 | #define TXD2 17 6 | #define RXD2 16 7 | 8 | 9 | #define FULL 0x55 10 | #define EMPTY 0xAA 11 | #define ACKNOWLEDGE 0x25 12 | 13 | bool transmitted = false; 14 | bool acknowledged = false; 15 | unsigned long transmissionTime = millis(); 16 | int retransmissions = 0; 17 | bool opening_pressed = false; 18 | bool door_pressed = false; 19 | 20 | void waitForAUX() { 21 | while (digitalRead(AUX) == LOW) 22 | delay(10); 23 | } 24 | 25 | 26 | 27 | void setup() { 28 | Serial.begin(9600); 29 | pinMode(M0, OUTPUT); 30 | pinMode(M1, OUTPUT); 31 | pinMode(AUX, INPUT_PULLUP); 32 | 33 | digitalWrite(M0, HIGH); 34 | digitalWrite(M1, HIGH); 35 | 36 | //byte data[] = { 0xC0, 0x0, 0x1, 0x1A, 0x17, 0x47 }; // for 30dBm Module 37 | byte data[] = { 0xC0, 0x0, 0x1, 0x1A, 0x17, 0x44 }; // for 20dBm Module 38 | Serial.write(data, sizeof(data)); 39 | waitForAUX(); 40 | digitalWrite(M0, LOW); 41 | digitalWrite(M1, LOW); 42 | } 43 | 44 | 45 | void loop() { 46 | waitForAUX(); 47 | Serial.write(FULL); 48 | //Serial.println("Full"); 49 | delay(1000); 50 | waitForAUX(); 51 | Serial.write(EMPTY); 52 | //Serial.println("empty"); 53 | delay(1000); 54 | } 55 | -------------------------------------------------------------------------------- /LetterBoxGatewayV2/LetterBoxGatewayV2.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include // Contains definitions for: ssid, password, mqtt_server 5 | 6 | // --- Pin Definitions --- 7 | // Module control pins (for setting parameter vs. transparent mode) 8 | #define M0 21 // Mode control pin 0 9 | #define M1 19 // Mode control pin 1 10 | #define AUX 4 // Module status indicator (used to synchronize operations) 11 | 12 | // UART configuration for module communication 13 | #define TXD2 17 // UART TX for module commands/data 14 | #define RXD2 16 // UART RX for module responses 15 | 16 | // --- Mailbox Command Definitions --- 17 | #define ARRIVED 0x55 // Mail has arrived 18 | #define EMPTY 0xAA // Mailbox is empty 19 | #define ACKNOWLEDGE 0x25 // Acknowledge received command 20 | 21 | // --- Module Configuration --- 22 | // Expected parameter configurations for the two module types 23 | byte expected_30dBm[6] = { 0xC0, 0x00, 0x01, 0x1A, 0x17, 0x47 }; 24 | byte expected_20dBm[6] = { 0xC0, 0x00, 0x01, 0x1A, 0x17, 0x44 }; 25 | 26 | // --- Global Variables for MQTT and Device Identification --- 27 | String macAddr; 28 | String uniqueID; 29 | String messageTopic; 30 | String discoveryTopic; 31 | 32 | // --- Mailbox Status Enumeration --- 33 | enum boxStatus { 34 | emptyStatus, 35 | fullStatus 36 | } mailBoxStatus = emptyStatus; 37 | 38 | // --- WiFi and MQTT Client Objects --- 39 | WiFiClient espClient; 40 | PubSubClient client(espClient); 41 | 42 | // 43 | // ======================== MODULE CONTROL FUNCTIONS ======================== 44 | // 45 | 46 | // Wait until the AUX pin indicates that the module is ready 47 | void wait_aux() { 48 | while (digitalRead(AUX) == LOW) { 49 | delay(1); 50 | } 51 | delay(5); // Additional delay for stability 52 | } 53 | 54 | // Set the module to programming (parameter configuration) mode 55 | void set_programming() { 56 | digitalWrite(M0, HIGH); 57 | digitalWrite(M1, HIGH); 58 | delay(20); 59 | wait_aux(); 60 | } 61 | 62 | // Set the module to transparent (normal operation) mode 63 | void set_transparent() { 64 | digitalWrite(M0, LOW); 65 | digitalWrite(M1, LOW); 66 | delay(20); 67 | wait_aux(); 68 | } 69 | 70 | // Send a command over Serial2 using a String 71 | void sendCommand(const String& command) { 72 | wait_aux(); 73 | Serial2.write(command.c_str(), command.length()); 74 | Serial2.flush(); 75 | delay(30); 76 | } 77 | 78 | // Send a command over Serial2 using a byte array 79 | void sendCommand(const byte* command, size_t len) { 80 | wait_aux(); 81 | Serial2.write(command, len); 82 | Serial2.flush(); 83 | delay(30); 84 | } 85 | 86 | // Request the stored configuration and compare it with the expected configuration. 87 | // If a mismatch is detected, program the module with the expected parameters. 88 | void sendCommandIfMismatch(const byte* expected, size_t len) { 89 | // Request the stored configuration from the module 90 | sendCommand("\xC1\xC1\xC1"); 91 | 92 | // Wait until 6 bytes are available on Serial2 93 | while (Serial2.available() < 6) { 94 | delay(10); 95 | } 96 | 97 | // Read the 6-byte current configuration from the module 98 | byte currentConfig[6]; 99 | Serial2.readBytes(currentConfig, 6); 100 | 101 | // Compare each byte with the expected configuration 102 | bool mismatch = false; 103 | for (size_t i = 0; i < len; i++) { 104 | if (currentConfig[i] != expected[i]) { 105 | mismatch = true; 106 | break; 107 | } 108 | } 109 | 110 | // Only program the module if a configuration mismatch is detected 111 | if (mismatch) { 112 | Serial.println("Configuration mismatch detected, programming module..."); 113 | sendCommand(expected, len); 114 | } else { 115 | Serial.println("Module configuration is already correct."); 116 | } 117 | } 118 | 119 | // Program the E32 module: switch to programming mode, request configuration, 120 | // determine module type from the response, and program the module only if needed. 121 | bool programming_E32() { 122 | Serial.println("--------------------Program E32 Module---------------------"); 123 | 124 | // Switch module to programming mode 125 | set_programming(); 126 | 127 | // Send command to request configuration 128 | sendCommand("\xC3\xC3\xC3"); 129 | 130 | // Read the 4-byte response from the module 131 | byte received[4]; 132 | Serial2.readBytes(received, 4); 133 | 134 | // Determine module type based on the last byte of the response 135 | const byte* expectedConfig; 136 | if (received[3] == 0x1E) { 137 | Serial.println("30dBm module detected."); 138 | expectedConfig = expected_30dBm; 139 | } else { 140 | Serial.println("20dBm module detected."); 141 | expectedConfig = expected_20dBm; 142 | } 143 | 144 | // Compare the current configuration and program if necessary 145 | sendCommandIfMismatch(expectedConfig, 6); 146 | 147 | // Return the module to transparent mode 148 | set_transparent(); 149 | return true; 150 | } 151 | 152 | // 153 | // ======================== WIFI & MQTT FUNCTIONS ======================== 154 | // 155 | 156 | // Initialize WiFi connection 157 | void setup_wifi() { 158 | delay(10); 159 | Serial.println(); 160 | Serial.print("Connecting to "); 161 | Serial.println(ssid); 162 | 163 | WiFi.mode(WIFI_STA); 164 | WiFi.begin(ssid, password); 165 | 166 | // Wait until connected to WiFi 167 | while (WiFi.status() != WL_CONNECTED) { 168 | delay(500); 169 | Serial.print("."); 170 | } 171 | Serial.println(); 172 | Serial.println("WiFi connected"); 173 | Serial.print("IP address: "); 174 | Serial.println(WiFi.localIP()); 175 | } 176 | 177 | // MQTT callback: handle incoming messages (currently just prints the message) 178 | void callback(char* topic, byte* payload, unsigned int length) { 179 | Serial.print("Message arrived ["); 180 | Serial.print(topic); 181 | Serial.print("] "); 182 | for (unsigned int i = 0; i < length; i++) { 183 | Serial.print((char)payload[i]); 184 | } 185 | Serial.println(); 186 | } 187 | 188 | // Ensure a persistent MQTT connection by reconnecting if disconnected 189 | void reconnect() { 190 | while (!client.connected()) { 191 | Serial.print("Attempting MQTT connection..."); 192 | String clientId = "Mailbox-"; 193 | clientId += String(random(0xffff), HEX); 194 | if (client.connect(clientId.c_str())) { 195 | Serial.println("connected"); 196 | } else { 197 | Serial.print("failed, rc="); 198 | Serial.print(client.state()); 199 | Serial.println(" try again in 5 seconds"); 200 | delay(5000); 201 | } 202 | } 203 | } 204 | 205 | // Publish an MQTT discovery message for Home Assistant autodiscovery 206 | void mqtt_discovery() { 207 | if (!client.connected()) { 208 | reconnect(); 209 | } 210 | 211 | StaticJsonDocument<256> doc; 212 | doc["name"] = "Mailbox"; 213 | doc["device_class"] = "occupancy"; 214 | doc["state_topic"] = "homeassistant/binary_sensor/" + uniqueID + "/state"; 215 | doc["unique_id"] = uniqueID; 216 | 217 | // Device metadata for Home Assistant 218 | JsonObject device = doc.createNestedObject("device"); 219 | device["ids"] = macAddr; 220 | device["name"] = "Mailbox"; 221 | device["mf"] = "Sensorsiot"; 222 | device["mdl"] = "Notifier"; 223 | device["sw"] = "1.0"; 224 | device["hw"] = "0.9"; 225 | 226 | char buffer[256]; 227 | serializeJson(doc, buffer); 228 | Serial.println(discoveryTopic); 229 | Serial.println(buffer); 230 | 231 | // Publish retained discovery message 232 | client.publish(discoveryTopic.c_str(), buffer, true); 233 | } 234 | 235 | // 236 | // ======================== ARDUINO SETUP & LOOP ======================== 237 | // 238 | 239 | void setup() { 240 | Serial.begin(115200); 241 | delay(1000); 242 | Serial.println("Start program"); 243 | 244 | // Initialize Serial2 for module communication 245 | Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2); 246 | Serial2.flush(); 247 | 248 | // Configure module control pins 249 | pinMode(M0, OUTPUT); 250 | pinMode(M1, OUTPUT); 251 | pinMode(AUX, INPUT); 252 | 253 | // Program the E32 module (only reprograms if necessary) 254 | programming_E32(); 255 | 256 | // Initialize WiFi and MQTT 257 | setup_wifi(); 258 | client.setServer(mqtt_server, 1883); 259 | client.setBufferSize(512); 260 | client.setCallback(callback); 261 | 262 | // Generate a unique device ID based on the MAC address for MQTT topics 263 | macAddr = WiFi.macAddress(); 264 | Serial.println(macAddr); 265 | String macLower = macAddr; 266 | macLower.toLowerCase(); 267 | macLower.replace(":", ""); 268 | uniqueID = "mailbox" + macLower.substring(macLower.length() - 4); 269 | messageTopic = "homeassistant/binary_sensor/" + uniqueID + "/state"; 270 | discoveryTopic = "homeassistant/binary_sensor/" + uniqueID + "/config"; 271 | 272 | // Publish MQTT discovery information 273 | mqtt_discovery(); 274 | 275 | Serial.println("-----------------------------"); 276 | Serial.println("Initialization finished."); 277 | Serial.println("-----------------------------"); 278 | } 279 | 280 | void loop() { 281 | if (!client.connected()) { 282 | reconnect(); 283 | } 284 | client.loop(); 285 | 286 | // Process incoming signals from the module on Serial2 287 | if (Serial2.available() > 0) { 288 | byte receivedCode = Serial2.read(); 289 | 290 | // Print the received code (if not 0xFF) 291 | Serial.print("Received: 0x"); 292 | Serial.println(receivedCode, HEX); 293 | 294 | // Process mailbox signals 295 | if (receivedCode == ARRIVED) { 296 | sendCommand(String((char)ACKNOWLEDGE)); 297 | mailBoxStatus = fullStatus; 298 | Serial.println("Mailbox Full"); 299 | client.publish(messageTopic.c_str(), "ON", true); 300 | } else if (receivedCode == EMPTY) { 301 | sendCommand(String((char)ACKNOWLEDGE)); 302 | mailBoxStatus = emptyStatus; 303 | Serial.println("Mailbox Empty"); 304 | client.publish(messageTopic.c_str(), "OFF", true); 305 | } 306 | } 307 | } -------------------------------------------------------------------------------- /LetterBoxGatewayV2/Secrets1.h: -------------------------------------------------------------------------------- 1 | const char* ssid = "..."; 2 | const char* password = "...."; 3 | const char* mqtt_server = "...."; 4 | -------------------------------------------------------------------------------- /LetterboxGatewayHA/LetterboxGatewayHA.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "secrets.h" 5 | 6 | #define M0 21 7 | #define M1 19 8 | #define AUX 4 9 | 10 | #define TXD2 17 11 | #define RXD2 16 12 | 13 | #define FULL 0x55 14 | #define EMPTY 0xAA 15 | #define ACKNOWLEDGE 0x25 16 | 17 | byte receivedCode = 0; 18 | bool transmissionSuccess = 0; 19 | String macAddr; 20 | String uniqueID; 21 | String stateTopicName; 22 | String discoveryTopicName; 23 | 24 | enum boxStatus { 25 | empty, 26 | full 27 | } mailBoxStatus = empty; 28 | 29 | WiFiClient espClient; 30 | PubSubClient client(espClient); 31 | unsigned long lastMsg = 0; 32 | #define MSG_BUFFER_SIZE (50) 33 | char msg[MSG_BUFFER_SIZE]; 34 | int value = 0; 35 | 36 | void setup_wifi() { 37 | delay(10); 38 | Serial.println(); 39 | Serial.print("Connecting to "); 40 | Serial.println(ssid); 41 | 42 | WiFi.mode(WIFI_STA); 43 | WiFi.begin(ssid, password); 44 | 45 | while (WiFi.status() != WL_CONNECTED) { 46 | delay(500); 47 | Serial.print("."); 48 | } 49 | 50 | randomSeed(micros()); 51 | 52 | Serial.println(""); 53 | Serial.println("WiFi connected"); 54 | Serial.println("IP address: "); 55 | Serial.println(WiFi.localIP()); 56 | } 57 | 58 | void callback(char* topic, byte* payload, unsigned int length) { 59 | Serial.print("Message arrived ["); 60 | Serial.print(topic); 61 | Serial.print("] "); 62 | for (int i = 0; i < length; i++) { 63 | Serial.print((char)payload[i]); 64 | } 65 | Serial.println(); 66 | 67 | // Switch on the LED if a '1' was received as the first character 68 | if ((char)payload[0] == '1') { 69 | digitalWrite(2, LOW); // Turn the LED on (active low) 70 | } else { 71 | digitalWrite(2, HIGH); // Turn the LED off 72 | } 73 | } 74 | 75 | void reconnect() { 76 | while (!client.connected()) { 77 | Serial.print("Attempting MQTT connection..."); 78 | String clientId = "Mailbox-"; 79 | clientId += String(random(0xffff), HEX); 80 | if (client.connect(clientId.c_str())) { 81 | Serial.println("connected"); 82 | //client.publish(stateTopicName.c_str(), "reconnected"); 83 | } else { 84 | Serial.print("failed, rc="); 85 | Serial.print(client.state()); 86 | Serial.println(" try again in 5 seconds"); 87 | delay(5000); 88 | } 89 | } 90 | } 91 | 92 | // Function to send MQTT discovery message 93 | void mqtt_discovery() { 94 | if (!client.connected()) { 95 | reconnect(); 96 | } 97 | 98 | StaticJsonDocument<256> doc; // Allocate memory for the JSON document 99 | 100 | doc["name"] = "Mailbox"; 101 | doc["device_class"] = "occupancy"; 102 | doc["state_topic"] = "homeassistant/binary_sensor/" + uniqueID + "/state"; 103 | doc["unique_id"] = uniqueID; // Use MAC-based unique ID 104 | 105 | JsonObject device = doc.createNestedObject("device"); 106 | device["ids"] = macAddr; // Use the full MAC address as identifier 107 | device["name"] = "Mailbox"; // Device name 108 | device["mf"] = "Sensorsiot"; // Include supplier info 109 | device["mdl"] = "Notifier"; 110 | device["sw"] = "1.0"; 111 | device["hw"] = "0.9"; 112 | 113 | char buffer[256]; 114 | serializeJson(doc, buffer); // Serialize JSON object to buffer 115 | 116 | Serial.println(discoveryTopicName.c_str()); 117 | Serial.println(buffer); // Print the JSON payload to Serial Monitor 118 | client.publish(discoveryTopicName.c_str(), buffer, true); // Publish to MQTT with retain flag set 119 | } 120 | 121 | void setup() { 122 | Serial.begin(115200); 123 | Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2); 124 | pinMode(M0, OUTPUT); 125 | pinMode(M1, OUTPUT); 126 | pinMode(AUX, INPUT_PULLUP); 127 | 128 | setup_wifi(); 129 | client.setServer(mqtt_server, 1883); 130 | client.setBufferSize(512); 131 | client.setCallback(callback); 132 | 133 | digitalWrite(M0, HIGH); 134 | digitalWrite(M1, HIGH); 135 | delay(7); 136 | 137 | byte data[] = { 0xC0, 0x0, 0x1, 0x1A, 0x17, 0x47 }; // for 30dBm Module 138 | // byte data[] = { 0xC0, 0x0, 0x1, 0x1A, 0x17, 0x44 }; // for 20dBm Module 139 | for (int i = 0; i < sizeof(data); i++) { 140 | Serial2.write(data[i]); 141 | Serial.println(data[i], HEX); 142 | } 143 | delay(10); 144 | 145 | while (digitalRead(AUX) == LOW) 146 | ; 147 | digitalWrite(M0, LOW); 148 | digitalWrite(M1, LOW); 149 | delay(100); 150 | Serial2.flush(); 151 | 152 | // Get the MAC address of the board 153 | macAddr = WiFi.macAddress(); 154 | Serial.println(macAddr); 155 | String hi = macAddr; 156 | hi.toLowerCase(); 157 | hi.replace(":", ""); // Remove colons from MAC address to make it topic-friendly 158 | // Extract the last 6 characters of the MAC address (ignoring colons) 159 | uniqueID = "mailbox" + hi.substring(hi.length() - 4); // Use last 4 byte 160 | 161 | stateTopicName = "homeassistant/binary_sensor/" + uniqueID + "/state"; 162 | discoveryTopicName = "homeassistant/binary_sensor/" + uniqueID + "/config"; 163 | 164 | // Send MQTT discovery message 165 | mqtt_discovery(); 166 | Serial.println("Setup finished"); 167 | } 168 | 169 | void loop() { 170 | if (!client.connected()) { 171 | reconnect(); 172 | } 173 | client.loop(); 174 | 175 | if (Serial2.available() > 0) { 176 | while (Serial2.available() > 0) { 177 | receivedCode = Serial2.read(); 178 | Serial.print(receivedCode, HEX); 179 | if (receivedCode == FULL) { 180 | transmissionSuccess = true; 181 | mailBoxStatus = full; 182 | Serial.println("Mailbox Full"); 183 | client.publish(stateTopicName.c_str(), "ON", true); // Update mailbox status 184 | } 185 | 186 | if (receivedCode == EMPTY) { 187 | transmissionSuccess = true; 188 | mailBoxStatus = empty; 189 | Serial.println("Mailbox empty"); 190 | client.publish(stateTopicName.c_str(), "OFF", true); // Update mailbox status 191 | } 192 | } 193 | } 194 | 195 | if (transmissionSuccess) { 196 | Serial2.write(ACKNOWLEDGE); 197 | Serial.println(mailBoxStatus); 198 | transmissionSuccess = false; 199 | Serial.println("Transmission acknowledged"); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LoRa_Mailbox_Notifier 2 | 3 | 4 | Software for this YouTube video: https://youtu.be/jXi5lgOuPNc 5 | -------------------------------------------------------------------------------- /STL Files/Mailbox Notifier ATTINY v2_Mailbox Notifier ATTINY v2_Enclosure_1_Case_1_Body1.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/LoRa_Mailbox_Notifier/d711a56c890b501e75fb8ed9cee90ba3f754daa4/STL Files/Mailbox Notifier ATTINY v2_Mailbox Notifier ATTINY v2_Enclosure_1_Case_1_Body1.stl -------------------------------------------------------------------------------- /STL Files/Mailbox Notifier ATTINY v2_Mailbox Notifier ATTINY v2_Enclosure_1_Top Cover_1_Body1.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/LoRa_Mailbox_Notifier/d711a56c890b501e75fb8ed9cee90ba3f754daa4/STL Files/Mailbox Notifier ATTINY v2_Mailbox Notifier ATTINY v2_Enclosure_1_Top Cover_1_Body1.stl -------------------------------------------------------------------------------- /STL Files/Notifier Gateway v10_Notifier Gateway v10_Enclosure_1_Base_1_Body1.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/LoRa_Mailbox_Notifier/d711a56c890b501e75fb8ed9cee90ba3f754daa4/STL Files/Notifier Gateway v10_Notifier Gateway v10_Enclosure_1_Base_1_Body1.stl -------------------------------------------------------------------------------- /STL Files/Notifier Gateway v10_Notifier Gateway v10_Enclosure_1_Top_1_Body1.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/LoRa_Mailbox_Notifier/d711a56c890b501e75fb8ed9cee90ba3f754daa4/STL Files/Notifier Gateway v10_Notifier Gateway v10_Enclosure_1_Top_1_Body1.stl -------------------------------------------------------------------------------- /States.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SensorsIot/LoRa_Mailbox_Notifier/d711a56c890b501e75fb8ed9cee90ba3f754daa4/States.pdf --------------------------------------------------------------------------------