| LM35 · +Vs | 5 V | |
| LM35 · GND | GND | |
| LM35 · OUT | GPIO 34 | |
| TX · VCC | 5 V | |
| TX · GND | GND | |
| TX · DATA | GPIO 25 | |
| TX · ANT | 17.3 cm | |
| RX · VCC | 5 V | |
| RX · GND | GND | |
| RX · VCC + GND | C | |
| RX · DATA | GPIO 27 | |
| RX · ANT | 17.3 cm | |
| MAX7219 · V+ | 5 V | |
| MAX7219 · GND | GND | |
| MAX7219 · DIN | GPIO 23 | |
| MAX7219 · CLK | GPIO 18 | |
| MAX7219 · LOAD | GPIO 5 | |
// AskLink core - Manchester-coded frames for a 433 MHz ASK (OOK) module pair.
// Frame on the air: 16 preamble bits '1' | sync 0x2D | LEN | payload | CRC-8
// Manchester: '1' = low then high, '0' = high then low (the edge sits mid-bit).
const uint8_t ASK_SYNC = 0x2D;
const uint8_t ASK_MAX_LEN = 16; // payload bytes
const uint8_t ASK_PREAMBLE = 16; // '1' bits that let the receiver gain settle
const uint16_t ASK_MAX_HALF = 2 * (ASK_PREAMBLE + 8 * (ASK_MAX_LEN + 3));
// CRC-8, polynomial 0x07, start value 0. Check: "123456789" gives 0xF4.
uint8_t crc8(const uint8_t *d, uint8_t n) {
uint8_t c = 0;
for (uint8_t i = 0; i < n; i++) {
c ^= d[i];
for (uint8_t b = 0; b < 8; b++)
c = (c & 0x80) ? (uint8_t)((c << 1) ^ 0x07) : (uint8_t)(c << 1);
}
return c;
}
// ---- pure logic: no pins, no timers. Testable without any radio. ----
static uint16_t putBit(bool one, bool *out, uint16_t n) {
out[n++] = !one; // first half
out[n++] = one; // second half
return n;
}
static uint16_t putByte(uint8_t v, bool *out, uint16_t n) {
for (int8_t i = 7; i >= 0; i--) n = putBit((v >> i) & 1, out, n);
return n;
}
// Fills 'out' with the line level of every half-bit. Returns how many, 0 on bad length.
uint16_t askEncode(const uint8_t *data, uint8_t len, bool *out) {
if (len == 0 || len > ASK_MAX_LEN) return 0;
uint8_t body[ASK_MAX_LEN + 1];
body[0] = len;
for (uint8_t i = 0; i < len; i++) body[i + 1] = data[i];
uint16_t n = 0;
for (uint8_t i = 0; i < ASK_PREAMBLE; i++) n = putBit(true, out, n);
n = putByte(ASK_SYNC, out, n);
for (uint8_t i = 0; i <= len; i++) n = putByte(body[i], out, n);
n = putByte(crc8(body, len + 1), out, n);
return n;
}
class AskDecoder {
public:
void begin(uint32_t bitUs) { bitUs_ = bitUs; last_ = 0; ready_ = false; crcErrors_ = 0; reset(); }
// Call on every level change; 'level' is the level AFTER the edge.
void onEdge(bool level, uint32_t nowUs) {
uint32_t dt = nowUs - last_;
last_ = nowUs;
if (ready_) return; // last frame not collected yet
if (dt > bitUs_ / 4 && dt < bitUs_ * 3 / 4) { // half a bit
mid_ = !mid_;
if (mid_) pushBit(level);
} else if (dt >= bitUs_ * 3 / 4 && dt < bitUs_ * 5 / 4) { // a whole bit
mid_ = true; // a long gap always ends mid-bit
pushBit(level);
} else {
reset(); // noise, or the gap between frames
}
}
bool available() const { return ready_; }
uint16_t crcErrors() const { return crcErrors_; }
// Copies the payload out and frees the decoder for the next frame.
uint8_t read(uint8_t *out) {
if (!ready_) return 0;
uint8_t len = buf_[0];
for (uint8_t i = 0; i < len; i++) out[i] = buf_[i + 1];
ready_ = false;
return len;
}
private:
void reset() { synced_ = false; mid_ = false; sh_ = 0; nbits_ = 0; nbytes_ = 0; }
void pushBit(bool b) {
sh_ = (uint8_t)((sh_ << 1) | (b ? 1 : 0));
if (!synced_) {
if (sh_ == ASK_SYNC) { synced_ = true; nbits_ = 0; nbytes_ = 0; }
return;
}
if (++nbits_ < 8) return;
nbits_ = 0;
buf_[nbytes_++] = sh_;
if (buf_[0] == 0 || buf_[0] > ASK_MAX_LEN) { reset(); return; }
if (nbytes_ == buf_[0] + 2) { // LEN + payload + CRC all in
if (crc8(buf_, buf_[0] + 1) == buf_[nbytes_ - 1]) ready_ = true;
else crcErrors_++;
reset();
}
}
uint32_t bitUs_, last_;
volatile bool ready_;
bool synced_, mid_;
uint8_t sh_, nbits_, nbytes_;
uint8_t buf_[ASK_MAX_LEN + 2];
uint16_t crcErrors_;
};
// ===== 6.4 Self-test of the core: no radio, no second board, only Serial =====
// The encoder output is turned into edges 500 us apart and fed straight into
// the decoder. Expected on the serial monitor at 115200 baud:
// frame: 144 half-bits = 72 bits = 72 ms on the air
// test 1 (round trip): PASS
// test 2 (one half-bit flipped): PASS, frame rejected
const uint32_t BIT_US = 1000; // 1 kbit/s
const uint32_t HALF_US = BIT_US / 2;
const uint16_t FLIP_AT = 70; // a half-bit inside the payload
const uint8_t EXAMPLE[4] = { 0x01, 0x00, 0xEA, 0x37 }; // node 1, 23.4 degC, counter
bool halfBits[ASK_MAX_HALF];
AskDecoder decoder;
// Feeds a half-bit list to the decoder exactly as the receiver pin would:
// one onEdge() call per level change, time-stamped on a 500 us grid.
void playFrame(const bool *h, uint16_t n, uint32_t t0) {
bool level = false; // the line idles low
for (uint16_t i = 0; i < n; i++) {
if (h[i] == level) continue; // no change, so no edge
level = h[i];
decoder.onEdge(level, t0 + (uint32_t)i * HALF_US);
}
if (level) decoder.onEdge(false, t0 + (uint32_t)n * HALF_US); // back to idle
}
bool samePayload(const uint8_t *a, const uint8_t *b, uint8_t n) {
for (uint8_t i = 0; i < n; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
void setup() {
Serial.begin(115200);
delay(500);
decoder.begin(BIT_US);
uint16_t n = askEncode(EXAMPLE, sizeof(EXAMPLE), halfBits);
Serial.printf("frame: %u half-bits = %u bits = %lu ms on the air\n",
n, n / 2, (unsigned long)(n / 2) * BIT_US / 1000);
// Test 1: what goes into the encoder must come out of the decoder.
uint8_t got[ASK_MAX_LEN];
uint8_t len = 0;
playFrame(halfBits, n, 100000UL);
if (decoder.available()) len = decoder.read(got);
bool ok1 = (n == 144) && (len == sizeof(EXAMPLE)) && samePayload(got, EXAMPLE, len);
Serial.printf("test 1 (round trip): %s\n", ok1 ? "PASS" : "FAIL");
// Test 2: one damaged half-bit must not produce a frame.
halfBits[FLIP_AT] = !halfBits[FLIP_AT];
playFrame(halfBits, n, 300000UL); // well after the first frame ended
bool ok2 = !decoder.available();
Serial.printf("test 2 (one half-bit flipped): %s\n",
ok2 ? "PASS, frame rejected" : "FAIL, frame accepted");
}
void loop() {
}
// AskLink core - Manchester-coded frames for a 433 MHz ASK (OOK) module pair.
// Frame on the air: 16 preamble bits '1' | sync 0x2D | LEN | payload | CRC-8
// Manchester: '1' = low then high, '0' = high then low (the edge sits mid-bit).
const uint8_t ASK_SYNC = 0x2D;
const uint8_t ASK_MAX_LEN = 16; // payload bytes
const uint8_t ASK_PREAMBLE = 16; // '1' bits that let the receiver gain settle
const uint16_t ASK_MAX_HALF = 2 * (ASK_PREAMBLE + 8 * (ASK_MAX_LEN + 3));
// CRC-8, polynomial 0x07, start value 0. Check: "123456789" gives 0xF4.
uint8_t crc8(const uint8_t *d, uint8_t n) {
uint8_t c = 0;
for (uint8_t i = 0; i < n; i++) {
c ^= d[i];
for (uint8_t b = 0; b < 8; b++)
c = (c & 0x80) ? (uint8_t)((c << 1) ^ 0x07) : (uint8_t)(c << 1);
}
return c;
}
// ---- pure logic: no pins, no timers. Testable without any radio. ----
static uint16_t putBit(bool one, bool *out, uint16_t n) {
out[n++] = !one; // first half
out[n++] = one; // second half
return n;
}
static uint16_t putByte(uint8_t v, bool *out, uint16_t n) {
for (int8_t i = 7; i >= 0; i--) n = putBit((v >> i) & 1, out, n);
return n;
}
// Fills 'out' with the line level of every half-bit. Returns how many, 0 on bad length.
uint16_t askEncode(const uint8_t *data, uint8_t len, bool *out) {
if (len == 0 || len > ASK_MAX_LEN) return 0;
uint8_t body[ASK_MAX_LEN + 1];
body[0] = len;
for (uint8_t i = 0; i < len; i++) body[i + 1] = data[i];
uint16_t n = 0;
for (uint8_t i = 0; i < ASK_PREAMBLE; i++) n = putBit(true, out, n);
n = putByte(ASK_SYNC, out, n);
for (uint8_t i = 0; i <= len; i++) n = putByte(body[i], out, n);
n = putByte(crc8(body, len + 1), out, n);
return n;
}
class AskDecoder {
public:
void begin(uint32_t bitUs) { bitUs_ = bitUs; last_ = 0; ready_ = false; crcErrors_ = 0; reset(); }
// Call on every level change; 'level' is the level AFTER the edge.
void onEdge(bool level, uint32_t nowUs) {
uint32_t dt = nowUs - last_;
last_ = nowUs;
if (ready_) return; // last frame not collected yet
if (dt > bitUs_ / 4 && dt < bitUs_ * 3 / 4) { // half a bit
mid_ = !mid_;
if (mid_) pushBit(level);
} else if (dt >= bitUs_ * 3 / 4 && dt < bitUs_ * 5 / 4) { // a whole bit
mid_ = true; // a long gap always ends mid-bit
pushBit(level);
} else {
reset(); // noise, or the gap between frames
}
}
bool available() const { return ready_; }
uint16_t crcErrors() const { return crcErrors_; }
// Copies the payload out and frees the decoder for the next frame.
uint8_t read(uint8_t *out) {
if (!ready_) return 0;
uint8_t len = buf_[0];
for (uint8_t i = 0; i < len; i++) out[i] = buf_[i + 1];
ready_ = false;
return len;
}
private:
void reset() { synced_ = false; mid_ = false; sh_ = 0; nbits_ = 0; nbytes_ = 0; }
void pushBit(bool b) {
sh_ = (uint8_t)((sh_ << 1) | (b ? 1 : 0));
if (!synced_) {
if (sh_ == ASK_SYNC) { synced_ = true; nbits_ = 0; nbytes_ = 0; }
return;
}
if (++nbits_ < 8) return;
nbits_ = 0;
buf_[nbytes_++] = sh_;
if (buf_[0] == 0 || buf_[0] > ASK_MAX_LEN) { reset(); return; }
if (nbytes_ == buf_[0] + 2) { // LEN + payload + CRC all in
if (crc8(buf_, buf_[0] + 1) == buf_[nbytes_ - 1]) ready_ = true;
else crcErrors_++;
reset();
}
}
uint32_t bitUs_, last_;
volatile bool ready_;
bool synced_, mid_;
uint8_t sh_, nbits_, nbytes_;
uint8_t buf_[ASK_MAX_LEN + 2];
uint16_t crcErrors_;
};
// ===========================================================================
// 6.5 Sensor node - the SENDER. GPIO 25 -> DATA input of the transmitter.
// One reading every 2 s, sent 3 times, 30 ms apart. Nobody answers: this node
// never learns whether any copy arrived, so it simply says everything 3 times.
// ===========================================================================
const int PIN_TX = 25;
const uint32_t HALF_US = 500; // half a bit at 1 kbit/s
const uint32_t PERIOD_MS = 2000; // one reading every 2 s
const uint8_t COPIES = 3; // every reading goes out 3 times
const uint32_t GAP_MS = 30; // silence between copies
const uint8_t NODE_ID = 1;
bool halfBits[ASK_MAX_HALF]; // line level of every half-bit of one frame
uint32_t lastReading = 0;
uint8_t counter = 0; // rolling counter: the same in all 3 copies
int16_t testTenths = 200; // TEST RAMP 20.0 .. 30.0 C - not a sensor yet.
// The LM35 replaces it in 6.6.
// Plays one frame on the pin. It BLOCKS for the whole frame: 72 ms for 4 bytes.
// Acceptable here because this node has nothing else to do while it talks.
void sendFrame(const uint8_t *data, uint8_t len) {
uint16_t n = askEncode(data, len, halfBits);
for (uint16_t i = 0; i < n; i++) {
digitalWrite(PIN_TX, halfBits[i] ? HIGH : LOW);
delayMicroseconds(HALF_US);
}
digitalWrite(PIN_TX, LOW); // carrier off: the air is free again
}
void setup() {
Serial.begin(115200);
pinMode(PIN_TX, OUTPUT);
digitalWrite(PIN_TX, LOW);
lastReading = millis();
}
void loop() {
// millis() schedules the readings, so the 2 s period does not stretch by
// the time the sending itself takes.
if (millis() - lastReading < PERIOD_MS) return;
lastReading += PERIOD_MS;
uint8_t payload[4];
payload[0] = NODE_ID;
payload[1] = (uint8_t)(testTenths >> 8); // temperature in tenths, high byte
payload[2] = (uint8_t)(testTenths & 0xFF); // low byte
payload[3] = counter;
for (uint8_t i = 0; i < COPIES; i++) {
sendFrame(payload, 4);
if (i + 1 < COPIES) delay(GAP_MS); // short and bounded: 2 gaps of 30 ms
}
Serial.printf("sent #%u T=%.1f C payload %02X %02X %02X %02X x%u\n",
counter, testTenths / 10.0,
payload[0], payload[1], payload[2], payload[3], COPIES);
counter++; // wraps 255 -> 0 by itself
testTenths++;
if (testTenths > 300) testTenths = 200;
}
// AskLink core - Manchester-coded frames for a 433 MHz ASK (OOK) module pair.
// Frame on the air: 16 preamble bits '1' | sync 0x2D | LEN | payload | CRC-8
// Manchester: '1' = low then high, '0' = high then low (the edge sits mid-bit).
const uint8_t ASK_SYNC = 0x2D;
const uint8_t ASK_MAX_LEN = 16; // payload bytes
const uint8_t ASK_PREAMBLE = 16; // '1' bits that let the receiver gain settle
const uint16_t ASK_MAX_HALF = 2 * (ASK_PREAMBLE + 8 * (ASK_MAX_LEN + 3));
// CRC-8, polynomial 0x07, start value 0. Check: "123456789" gives 0xF4.
uint8_t crc8(const uint8_t *d, uint8_t n) {
uint8_t c = 0;
for (uint8_t i = 0; i < n; i++) {
c ^= d[i];
for (uint8_t b = 0; b < 8; b++)
c = (c & 0x80) ? (uint8_t)((c << 1) ^ 0x07) : (uint8_t)(c << 1);
}
return c;
}
// ---- pure logic: no pins, no timers. Testable without any radio. ----
static uint16_t putBit(bool one, bool *out, uint16_t n) {
out[n++] = !one; // first half
out[n++] = one; // second half
return n;
}
static uint16_t putByte(uint8_t v, bool *out, uint16_t n) {
for (int8_t i = 7; i >= 0; i--) n = putBit((v >> i) & 1, out, n);
return n;
}
// Fills 'out' with the line level of every half-bit. Returns how many, 0 on bad length.
uint16_t askEncode(const uint8_t *data, uint8_t len, bool *out) {
if (len == 0 || len > ASK_MAX_LEN) return 0;
uint8_t body[ASK_MAX_LEN + 1];
body[0] = len;
for (uint8_t i = 0; i < len; i++) body[i + 1] = data[i];
uint16_t n = 0;
for (uint8_t i = 0; i < ASK_PREAMBLE; i++) n = putBit(true, out, n);
n = putByte(ASK_SYNC, out, n);
for (uint8_t i = 0; i <= len; i++) n = putByte(body[i], out, n);
n = putByte(crc8(body, len + 1), out, n);
return n;
}
class AskDecoder {
public:
void begin(uint32_t bitUs) { bitUs_ = bitUs; last_ = 0; ready_ = false; crcErrors_ = 0; reset(); }
// Call on every level change; 'level' is the level AFTER the edge.
void onEdge(bool level, uint32_t nowUs) {
uint32_t dt = nowUs - last_;
last_ = nowUs;
if (ready_) return; // last frame not collected yet
if (dt > bitUs_ / 4 && dt < bitUs_ * 3 / 4) { // half a bit
mid_ = !mid_;
if (mid_) pushBit(level);
} else if (dt >= bitUs_ * 3 / 4 && dt < bitUs_ * 5 / 4) { // a whole bit
mid_ = true; // a long gap always ends mid-bit
pushBit(level);
} else {
reset(); // noise, or the gap between frames
}
}
bool available() const { return ready_; }
uint16_t crcErrors() const { return crcErrors_; }
// Copies the payload out and frees the decoder for the next frame.
uint8_t read(uint8_t *out) {
if (!ready_) return 0;
uint8_t len = buf_[0];
for (uint8_t i = 0; i < len; i++) out[i] = buf_[i + 1];
ready_ = false;
return len;
}
private:
void reset() { synced_ = false; mid_ = false; sh_ = 0; nbits_ = 0; nbytes_ = 0; }
void pushBit(bool b) {
sh_ = (uint8_t)((sh_ << 1) | (b ? 1 : 0));
if (!synced_) {
if (sh_ == ASK_SYNC) { synced_ = true; nbits_ = 0; nbytes_ = 0; }
return;
}
if (++nbits_ < 8) return;
nbits_ = 0;
buf_[nbytes_++] = sh_;
if (buf_[0] == 0 || buf_[0] > ASK_MAX_LEN) { reset(); return; }
if (nbytes_ == buf_[0] + 2) { // LEN + payload + CRC all in
if (crc8(buf_, buf_[0] + 1) == buf_[nbytes_ - 1]) ready_ = true;
else crcErrors_++;
reset();
}
}
uint32_t bitUs_, last_;
volatile bool ready_;
bool synced_, mid_;
uint8_t sh_, nbits_, nbytes_;
uint8_t buf_[ASK_MAX_LEN + 2];
uint16_t crcErrors_;
};
// ===========================================================================
// 6.5 Display node - the RECEIVER.
// Receiver DATA (5 V) -> 10k / 20k divider (3.33 V) -> GPIO 27.
// A frame that fails the CRC never leaves the decoder, so this code can only
// ever see good frames. Its own job: drop the duplicate copies, count the
// readings that never came, and notice when the data on show has grown old.
// ===========================================================================
const int PIN_RX = 27;
const uint32_t BIT_US = 1000; // 1 kbit/s
const uint8_t NODE_ID = 1;
const uint32_t PERIOD_MS = 2000; // the sender's reporting period
const uint32_t STALE_MS = 3 * PERIOD_MS + 500; // 3 readings in a row missing
AskDecoder decoder;
bool haveReading = false; // false until the first good frame
uint8_t lastCounter = 0;
uint32_t readings = 0; // different readings delivered
uint32_t missed = 0; // readings that never arrived
uint32_t lastGoodMs = 0;
bool staleShown = false;
// Runs on every edge of the data line - also on the noise edges that the
// receiver module produces whenever nobody is transmitting.
void IRAM_ATTR isr() { decoder.onEdge(digitalRead(PIN_RX), micros()); }
void setup() {
Serial.begin(115200);
pinMode(PIN_RX, INPUT);
decoder.begin(BIT_US);
attachInterrupt(PIN_RX, isr, CHANGE);
}
void handleFrame(const uint8_t *p, uint8_t len) {
// The CRC-8 lets about 1 random bad frame in 256 through: check what we can.
if (len != 4 || p[0] != NODE_ID) return;
uint8_t counter = p[3];
lastGoodMs = millis(); // any good copy proves the link is alive
staleShown = false;
// The 3 copies of one reading carry the same counter: keep the first only.
if (haveReading && counter == lastCounter) return;
// A jump in the counter is the number of readings lost on the way.
// The uint8_t cast makes the wrap 255 -> 0 come out as 0 missed.
// (256 or more lost in a row, or a sender restart, cannot be told apart.)
if (haveReading) missed += (uint8_t)(counter - lastCounter - 1);
haveReading = true;
lastCounter = counter;
readings++;
int16_t tenths = (int16_t)((p[1] << 8) | p[2]);
Serial.printf("#%u T=%.1f C readings=%lu missed=%lu crcErrors=%u\n",
counter, tenths / 10.0,
(unsigned long)readings, (unsigned long)missed, decoder.crcErrors());
}
void loop() {
if (decoder.available()) {
uint8_t buf[ASK_MAX_LEN];
noInterrupts(); // the ISR must not touch the buffer now
uint8_t n = decoder.read(buf);
interrupts();
handleFrame(buf, n);
}
// No acknowledgement exists, so silence is the only alarm there is.
if (haveReading && !staleShown && millis() - lastGoodMs > STALE_MS) {
staleShown = true;
Serial.println("no frame for 3 periods - the value on show is OLD");
}
}
// 6.6 Sensor node: LM35 -> 4-byte payload -> 433.92 MHz ASK link.
// ESP32 core 3.x. The link code below is the shared AskLink core, unchanged.
// AskLink core - Manchester-coded frames for a 433 MHz ASK (OOK) module pair.
// Frame on the air: 16 preamble bits '1' | sync 0x2D | LEN | payload | CRC-8
// Manchester: '1' = low then high, '0' = high then low (the edge sits mid-bit).
const uint8_t ASK_SYNC = 0x2D;
const uint8_t ASK_MAX_LEN = 16; // payload bytes
const uint8_t ASK_PREAMBLE = 16; // '1' bits that let the receiver gain settle
const uint16_t ASK_MAX_HALF = 2 * (ASK_PREAMBLE + 8 * (ASK_MAX_LEN + 3));
// CRC-8, polynomial 0x07, start value 0. Check: "123456789" gives 0xF4.
uint8_t crc8(const uint8_t *d, uint8_t n) {
uint8_t c = 0;
for (uint8_t i = 0; i < n; i++) {
c ^= d[i];
for (uint8_t b = 0; b < 8; b++)
c = (c & 0x80) ? (uint8_t)((c << 1) ^ 0x07) : (uint8_t)(c << 1);
}
return c;
}
// ---- pure logic: no pins, no timers. Testable without any radio. ----
static uint16_t putBit(bool one, bool *out, uint16_t n) {
out[n++] = !one; // first half
out[n++] = one; // second half
return n;
}
static uint16_t putByte(uint8_t v, bool *out, uint16_t n) {
for (int8_t i = 7; i >= 0; i--) n = putBit((v >> i) & 1, out, n);
return n;
}
// Fills 'out' with the line level of every half-bit. Returns how many, 0 on bad length.
uint16_t askEncode(const uint8_t *data, uint8_t len, bool *out) {
if (len == 0 || len > ASK_MAX_LEN) return 0;
uint8_t body[ASK_MAX_LEN + 1];
body[0] = len;
for (uint8_t i = 0; i < len; i++) body[i + 1] = data[i];
uint16_t n = 0;
for (uint8_t i = 0; i < ASK_PREAMBLE; i++) n = putBit(true, out, n);
n = putByte(ASK_SYNC, out, n);
for (uint8_t i = 0; i <= len; i++) n = putByte(body[i], out, n);
n = putByte(crc8(body, len + 1), out, n);
return n;
}
class AskDecoder {
public:
void begin(uint32_t bitUs) { bitUs_ = bitUs; last_ = 0; ready_ = false; crcErrors_ = 0; reset(); }
// Call on every level change; 'level' is the level AFTER the edge.
void onEdge(bool level, uint32_t nowUs) {
uint32_t dt = nowUs - last_;
last_ = nowUs;
if (ready_) return; // last frame not collected yet
if (dt > bitUs_ / 4 && dt < bitUs_ * 3 / 4) { // half a bit
mid_ = !mid_;
if (mid_) pushBit(level);
} else if (dt >= bitUs_ * 3 / 4 && dt < bitUs_ * 5 / 4) { // a whole bit
mid_ = true; // a long gap always ends mid-bit
pushBit(level);
} else {
reset(); // noise, or the gap between frames
}
}
bool available() const { return ready_; }
uint16_t crcErrors() const { return crcErrors_; }
// Copies the payload out and frees the decoder for the next frame.
uint8_t read(uint8_t *out) {
if (!ready_) return 0;
uint8_t len = buf_[0];
for (uint8_t i = 0; i < len; i++) out[i] = buf_[i + 1];
ready_ = false;
return len;
}
private:
void reset() { synced_ = false; mid_ = false; sh_ = 0; nbits_ = 0; nbytes_ = 0; }
void pushBit(bool b) {
sh_ = (uint8_t)((sh_ << 1) | (b ? 1 : 0));
if (!synced_) {
if (sh_ == ASK_SYNC) { synced_ = true; nbits_ = 0; nbytes_ = 0; }
return;
}
if (++nbits_ < 8) return;
nbits_ = 0;
buf_[nbytes_++] = sh_;
if (buf_[0] == 0 || buf_[0] > ASK_MAX_LEN) { reset(); return; }
if (nbytes_ == buf_[0] + 2) { // LEN + payload + CRC all in
if (crc8(buf_, buf_[0] + 1) == buf_[nbytes_ - 1]) ready_ = true;
else crcErrors_++;
reset();
}
}
uint32_t bitUs_, last_;
volatile bool ready_;
bool synced_, mid_;
uint8_t sh_, nbits_, nbytes_;
uint8_t buf_[ASK_MAX_LEN + 2];
uint16_t crcErrors_;
};
// ---- application: the sensor node ----
const int PIN_TX = 25; // DATA input of the transmitter module
const int PIN_LM35 = 34; // LM35 output, ADC1, input-only pin
const uint32_t BIT_US = 1000; // 1 kbit/s
const uint8_t NODE_ID = 1;
const uint32_t REPORT_MS = 2000; // a new reading every 2 s
const uint8_t COPIES = 3; // every reading is sent three times
const uint32_t COPY_GAP_MS = 30;
const uint8_t SAMPLES = 16;
bool halves[ASK_MAX_HALF];
uint8_t counter = 0; // rolling counter: lets the receiver drop copies
uint32_t lastReport = 0;
// LM35: 10 mV per degree, so 1 mV is 0.1 degree. The averaged millivolt
// reading IS the temperature in tenths of a degree - no division, no float.
// With a single positive supply this circuit cannot read below 0 degrees.
int16_t readTenths() {
uint32_t sum = 0;
for (uint8_t i = 0; i < SAMPLES; i++) sum += analogReadMilliVolts(PIN_LM35);
return (int16_t)((sum + SAMPLES / 2) / SAMPLES); // rounded average
}
// Blocks for the whole frame: 72 bits at 1 ms = 72 ms for a 4-byte payload.
// Acceptable here because this node has nothing else to do while it sends.
void sendFrame(const uint8_t *data, uint8_t len) {
uint16_t n = askEncode(data, len, halves);
for (uint16_t i = 0; i < n; i++) {
digitalWrite(PIN_TX, halves[i] ? HIGH : LOW);
delayMicroseconds(BIT_US / 2);
}
digitalWrite(PIN_TX, LOW); // carrier off between frames
}
void setup() {
Serial.begin(115200);
pinMode(PIN_TX, OUTPUT);
digitalWrite(PIN_TX, LOW);
}
void loop() {
uint32_t now = millis();
if (now - lastReport < REPORT_MS) return;
lastReport = now;
int16_t tenths = readTenths();
uint8_t payload[4];
payload[0] = NODE_ID;
payload[1] = (uint8_t)((uint16_t)tenths >> 8); // high byte first (big-endian)
payload[2] = (uint8_t)((uint16_t)tenths & 0xFF);
payload[3] = counter++;
for (uint8_t c = 0; c < COPIES; c++) {
sendFrame(payload, 4);
if (c + 1 < COPIES) delay(COPY_GAP_MS);
}
Serial.printf("%d mV = %d tenths payload %02X %02X %02X %02X\n",
tenths, tenths, payload[0], payload[1], payload[2], payload[3]);
}
// 6.6 Display node: 433.92 MHz ASK link -> MAX7219 eight-digit display.
// ESP32 core 3.x. The link code below is the shared AskLink core, unchanged.
#include <SPI.h>
// AskLink core - Manchester-coded frames for a 433 MHz ASK (OOK) module pair.
// Frame on the air: 16 preamble bits '1' | sync 0x2D | LEN | payload | CRC-8
// Manchester: '1' = low then high, '0' = high then low (the edge sits mid-bit).
const uint8_t ASK_SYNC = 0x2D;
const uint8_t ASK_MAX_LEN = 16; // payload bytes
const uint8_t ASK_PREAMBLE = 16; // '1' bits that let the receiver gain settle
const uint16_t ASK_MAX_HALF = 2 * (ASK_PREAMBLE + 8 * (ASK_MAX_LEN + 3));
// CRC-8, polynomial 0x07, start value 0. Check: "123456789" gives 0xF4.
uint8_t crc8(const uint8_t *d, uint8_t n) {
uint8_t c = 0;
for (uint8_t i = 0; i < n; i++) {
c ^= d[i];
for (uint8_t b = 0; b < 8; b++)
c = (c & 0x80) ? (uint8_t)((c << 1) ^ 0x07) : (uint8_t)(c << 1);
}
return c;
}
// ---- pure logic: no pins, no timers. Testable without any radio. ----
static uint16_t putBit(bool one, bool *out, uint16_t n) {
out[n++] = !one; // first half
out[n++] = one; // second half
return n;
}
static uint16_t putByte(uint8_t v, bool *out, uint16_t n) {
for (int8_t i = 7; i >= 0; i--) n = putBit((v >> i) & 1, out, n);
return n;
}
// Fills 'out' with the line level of every half-bit. Returns how many, 0 on bad length.
uint16_t askEncode(const uint8_t *data, uint8_t len, bool *out) {
if (len == 0 || len > ASK_MAX_LEN) return 0;
uint8_t body[ASK_MAX_LEN + 1];
body[0] = len;
for (uint8_t i = 0; i < len; i++) body[i + 1] = data[i];
uint16_t n = 0;
for (uint8_t i = 0; i < ASK_PREAMBLE; i++) n = putBit(true, out, n);
n = putByte(ASK_SYNC, out, n);
for (uint8_t i = 0; i <= len; i++) n = putByte(body[i], out, n);
n = putByte(crc8(body, len + 1), out, n);
return n;
}
class AskDecoder {
public:
void begin(uint32_t bitUs) { bitUs_ = bitUs; last_ = 0; ready_ = false; crcErrors_ = 0; reset(); }
// Call on every level change; 'level' is the level AFTER the edge.
void onEdge(bool level, uint32_t nowUs) {
uint32_t dt = nowUs - last_;
last_ = nowUs;
if (ready_) return; // last frame not collected yet
if (dt > bitUs_ / 4 && dt < bitUs_ * 3 / 4) { // half a bit
mid_ = !mid_;
if (mid_) pushBit(level);
} else if (dt >= bitUs_ * 3 / 4 && dt < bitUs_ * 5 / 4) { // a whole bit
mid_ = true; // a long gap always ends mid-bit
pushBit(level);
} else {
reset(); // noise, or the gap between frames
}
}
bool available() const { return ready_; }
uint16_t crcErrors() const { return crcErrors_; }
// Copies the payload out and frees the decoder for the next frame.
uint8_t read(uint8_t *out) {
if (!ready_) return 0;
uint8_t len = buf_[0];
for (uint8_t i = 0; i < len; i++) out[i] = buf_[i + 1];
ready_ = false;
return len;
}
private:
void reset() { synced_ = false; mid_ = false; sh_ = 0; nbits_ = 0; nbytes_ = 0; }
void pushBit(bool b) {
sh_ = (uint8_t)((sh_ << 1) | (b ? 1 : 0));
if (!synced_) {
if (sh_ == ASK_SYNC) { synced_ = true; nbits_ = 0; nbytes_ = 0; }
return;
}
if (++nbits_ < 8) return;
nbits_ = 0;
buf_[nbytes_++] = sh_;
if (buf_[0] == 0 || buf_[0] > ASK_MAX_LEN) { reset(); return; }
if (nbytes_ == buf_[0] + 2) { // LEN + payload + CRC all in
if (crc8(buf_, buf_[0] + 1) == buf_[nbytes_ - 1]) ready_ = true;
else crcErrors_++;
reset();
}
}
uint32_t bitUs_, last_;
volatile bool ready_;
bool synced_, mid_;
uint8_t sh_, nbits_, nbytes_;
uint8_t buf_[ASK_MAX_LEN + 2];
uint16_t crcErrors_;
};
// ---- application: the display node ----
const int PIN_RX = 27; // receiver DATA, through the 10k / 20k divider
const int PIN_MOSI = 23; // the three display lines go through a level shifter
const int PIN_SCK = 18;
const int PIN_CS = 5;
const uint32_t BIT_US = 1000;
const uint8_t NODE_ID = 1;
const uint32_t TIMEOUT_MS = 6000; // three report periods of 2 s
// MAX7219 driven through raw registers, Code-B decode on all eight digits.
// On the common eight-digit module register 0x01 is the rightmost digit;
// check the module you hold.
class Max7219 {
public:
explicit Max7219(uint8_t cs) : cs_(cs) {}
void begin() {
pinMode(cs_, OUTPUT);
digitalWrite(cs_, HIGH);
write(REG_TEST, 0x00); // display test off
write(REG_SCAN, 0x07); // scan all eight digits
write(REG_DECODE, 0xFF); // Code-B on every digit
write(REG_INTENSITY, 0x04); // moderate brightness
for (uint8_t d = 0; d < 8; d++) write(REG_DIGIT0 + d, CODE_BLANK);
write(REG_SHUTDOWN, 0x01); // 1 = normal operation
}
// 234 -> " 23.4", -50 -> " -5.0". Right-aligned, blanks elsewhere.
// Any int16_t fits: the longest is -3276.8, six positions out of eight.
void showTenths(int16_t tenths) {
int32_t v = tenths; // 32 bits, so that -32768 can be negated
bool neg = v < 0;
if (neg) v = -v;
uint8_t pos = 0; // 0 = rightmost digit
write(REG_DIGIT0 + pos++, (uint8_t)(v % 10)); // tenths
v /= 10;
write(REG_DIGIT0 + pos++, (uint8_t)(v % 10) | DP); // units, with the point
v /= 10;
while (v > 0) {
write(REG_DIGIT0 + pos++, (uint8_t)(v % 10));
v /= 10;
}
if (neg) write(REG_DIGIT0 + pos++, CODE_MINUS);
while (pos < 8) write(REG_DIGIT0 + pos++, CODE_BLANK);
}
// "----": there is no valid reading to show.
void showLost() {
for (uint8_t d = 0; d < 8; d++) write(REG_DIGIT0 + d, d < 4 ? CODE_MINUS : CODE_BLANK);
}
private:
static const uint8_t REG_DIGIT0 = 0x01; // 0x01..0x08
static const uint8_t REG_DECODE = 0x09;
static const uint8_t REG_INTENSITY = 0x0A;
static const uint8_t REG_SCAN = 0x0B;
static const uint8_t REG_SHUTDOWN = 0x0C;
static const uint8_t REG_TEST = 0x0F;
static const uint8_t CODE_MINUS = 0x0A;
static const uint8_t CODE_BLANK = 0x0F;
static const uint8_t DP = 0x80; // bit 7 lights the decimal point
void write(uint8_t reg, uint8_t value) {
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
digitalWrite(cs_, LOW);
SPI.transfer(reg);
SPI.transfer(value);
digitalWrite(cs_, HIGH); // the rising edge latches the 16 bits
SPI.endTransaction();
}
uint8_t cs_;
};
AskDecoder decoder;
Max7219 display(PIN_CS);
bool linkUp = false; // nothing received yet
uint32_t lastRxMs = 0;
uint8_t lastCounter = 0;
void IRAM_ATTR onRxEdge() { decoder.onEdge(digitalRead(PIN_RX), micros()); }
void setup() {
Serial.begin(115200);
SPI.begin(PIN_SCK, -1, PIN_MOSI, -1); // no MISO: the display is write-only here
display.begin();
display.showLost(); // honest start: no reading yet
decoder.begin(BIT_US);
pinMode(PIN_RX, INPUT);
attachInterrupt(digitalPinToInterrupt(PIN_RX), onRxEdge, CHANGE);
}
void loop() {
if (decoder.available()) {
uint8_t buf[ASK_MAX_LEN];
noInterrupts();
uint8_t n = decoder.read(buf);
interrupts();
// The decoder hands over only frames whose CRC matched, so a frame that
// failed the CRC can never reach the display. (A CRC-8 still lets about
// one random corrupted frame in 256 through - hence the checks below.)
if (n == 4 && buf[0] == NODE_ID) {
bool fresh = !linkUp || buf[3] != lastCounter; // a copy has the same counter
lastRxMs = millis(); // a copy still proves that the link is alive
linkUp = true;
lastCounter = buf[3];
if (fresh) {
int16_t tenths = (int16_t)(((uint16_t)buf[1] << 8) | buf[2]);
display.showTenths(tenths);
Serial.printf("node %u counter %u %d tenths\n", buf[0], buf[3], tenths);
}
}
}
// Silence longer than the timeout: say so instead of showing an old number.
// The third report is due exactly at 6000 ms, so a report that arrives a
// little late still trips it; raise the constant slightly to forgive that.
if (linkUp && millis() - lastRxMs >= TIMEOUT_MS) {
linkUp = false;
display.showLost();
Serial.println("link lost");
}
}