Skip to content

Communication Protocol

Denis Stepanov edited this page Mar 1, 2023 · 9 revisions

Originally, we just needed to transmit the door open / closed status; that is, one bit of information. But pretty quickly one realizes that there is a need to pass on some timing information, some means to track message integrity, undelivered messages, etc. So, it all ends up with a mini-protocol, defined in MailBoxMessage.h header.

Addressing

Each communication node (transmitter or receiver) is assigned an address Communication Channel.Receiver ID.Transmitter ID:

  • Communication Channel (1–127) corresponds to AT+Cnnn setting of HC-12 (see its user manual);
  • Receiver ID (1–15) identifies receiver working on a given channel. 0 means message addressed to any receiver listening;
  • Transmitter ID (1–15) identifies transmitter reporting to a given receiver. 0 is reserved for the receiver itself.

Examples:

  • 07.01.02 — mailbox #2 reporting to receiver #1 on communication channel #7 (435.8 MHz);
  • 15.03.00 — receiver #3 working on communication channel #15.

In this application, there is normally one receiver with ID = 1 and one (or more) mailboxes with IDs starting from 1.

Common Rules

All nodes who wish to communicate must use the same communication channel. Even though HC-12 allows for reconfiguration at run-time, for the sake of simplicity and speed this feature is not being used. All HC-12 are pre-programmed to a given channel (see Getting Started); the rest of the software makes no assumption on the channel being used. For this reason, message packet (see below) does not include channel information.

Receiver must accept all messages carrying its ID, as well as messages with receiver ID 0; other messages must be discarded (but may be logged). Transmitter (mailbox) must identify itself with a non-zero ID. Both receiver and transmitter IDs are, typically, constants for the whole duration of node operation. In the simplest case they are compiled-in constants; more sophisticated programs might include settings interface to change them at run-time.

Each message carries a version of a protocol and a checksum. Checksum validity must be checked before any other check is to be made. Messages with mismatch in checksum or protocol version must be discarded (but may be logged). In practice, the radio link proved to be very stable — a message either arrives in its entirety, or gets completely lost. I saw very few cases where checksum mismatch was manifested.

Message Format

Each message consists of eight bytes, as follows:

Byte.Bit Content
Header
0.0–3 Protocol version (0–15)
0.4–7 Receiver ID (0–15)
Mailbox Status
1.0–3 Mailbox ID (1–15)
1.4 Boot Status (0–wake up from deep sleep, 1–boot for other reason)
1.5 Online Status (0–offline (going to sleep), 1–online (staying awake))
1.6 Door Status (0–closed, 1–open)
1.7 (reserved)
Message Number
2–3 Through Message Number (1–65535)
Timestamp
4–5 Local Time (ms from boot) (0–65535)
Battery Status
6.0–6 Battery Level (0–100 %)
6.7 (reserved)
Checksum
7 Checksum

Multi-byte fields (message number and timestamp) are encoded in network byte order (as produced by htons() function).

Protocol version 0 is reserved for development purposes; applications may accept it at their own risk. Systems in production must use a non-zero version of the protocol. Protocol version must be incremented by developer every time an incompatible change is made. The current version of the protocol is 2.

ESP8266 Core allows for pretty detailed diagnostics of boot reasons. Of these, only boot from deep sleep is singled out as the most important case. This way, the receiver could judge if transmitter has booted normally, or experienced anomaly, such as cold boot.

"Online" status tells if transmitter is going offline (deep sleep or shutdown) after a given message, or will continue to be available. In this application, two messages per event are always being sent; hence, "online" status could also be derived from message number (odd are "online" messages, even are "offline" messages).

Door status is reported as measured. In a normal operation flow the first message out of two corresponds to a "door open" status, and the second to "door closed". This is, however, not true for abnormal situations. E.g., if the door is not closed before closure timeout kicks in, it will be reported as "open" upon going to sleep. Another example is unexpected reboot; in this case the first message might report the door as "closed".

Through message number is counted by transmitter starting from 1 from its cold boot. For this to work correctly, transmitter must save the counter into some persistent memory (RTC or flash) during deep sleep cycles. Receiver, if it wishes so, may count received messages on its own. If the two counters do not match, this means a message loss has happened. Note that message number 0 is reserved to indicate an "unknown" message number in software, and, as such, should never be transmitted. Also, to keep the odd/even symmetry, upon message counter overflow (65535 + 1) the next message number assigned is 2, not 0 or 1. In practice, in mailbox application with its few messages per day the overflow is never reached — cold reboots or battery shortage cause the counter to restart well before the limit. To give an example, after three years of exploitation a record streak was about 100 days with about 200 messages sent.

Timestamp is typically measured with the help of millis() function. Two bytes are allocated in the message; this provides for ≈ 65 seconds of online time before overflow happens. Because sleeping circuits tend to stay online for the shortest time possible, this should be sufficient for most applications. Also, on such small time scales drift of millis() is not an issue. Timestamping is particularly useful when communication channel gets unreliable. For instance, one is typically able to tell from the door closing message for how long the mailbox door stayed open, even if the door opening message has never arrived.

Battery status is reported as percentage from full charge. The protocol file provides some handy constants for important levels "full" (100%), "dead" (0%), "low" (20%). Value 127 is reserved to signal unknown battery status.

Checksum is counted by making XOR (bitwise exclusive OR) of the first seven bytes of the message, using 0 as a seed.

The protocol source file MailBoxMessage.h, along with the definition of the message structure, provides many utility functions for filling the message fields, checking their validity, printing the messages or sending them into an output stream. These functions should be preferred to manual operations.

Clone this wiki locally