// Heltec WiFi LoRa 32 V3 (SX1262) thermostat slave, continuous
control
// - DS18B20 on GPIO7 (inside / control sensor)
// - DS18B20 on GPIO5 (greenhouse)
// - DS18B20 on GPIO4 (outside)
// - Relay on GPIO6 (via transistor + your LED)
// - Buzzer / alarm on GPIO1 (BUZZER_PIN)
//
// GPT_TStat_tune_alarm_BEDRM.ino
//
// - Device ID prefix: commands must start with DEVICE_ID
//
// sudo /home/pi/lora/WIFI_GPT_lora/send_command_wifi_arg.py
BEDRMt1502 set 15 deg 2 sec hysteresis -
beeper BEDRMb15 15 sec
//
// Commands:
//
// 1) Set config + read:
// "BEDRMtTTHH"
//
sudo /home/pi/lora/WIFI_GPT_lora/send_command_wifi_arg.py BEDRMt1502
// Example: BEDRMt3002 =>
setpoint 30C, hysteresis 2C
// Replies "IN,GH,OUT,SETPOINT"
//
// 2) Read-only (no config change):
// "BEDRMr0000" (or simply "BEDRMr")
// Replies "IN,GH,OUT,SETPOINT"
//
// 3) Tune / alarm:
// "BEDRMbXX"
// XX = duration seconds (e.g. 15)
// Plays a simple tune for XX seconds
(non-blocking)
// Also replies "IN,GH,OUT,SETPOINT"
//
must use
//
sudo /home/pi/lora/WIFI_GPT_lora/send_command_wifi_arg.py
BEDRMb14
//
//
// OLED:
// - Big top: inside temp + "RL ON"/"RL OFF" at
top-right
// - Line2: "GH:xx.x OUT:yy.y"
// - Line3: "SP:xx H:yy"
// - Line4: "RSS:-nn BEDRM"
#define HELTEC_POWER_BUTTON // long-press PRG = power
control
#include <heltec_unofficial.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <math.h>
#include <esp32-hal-ledc.h>
// ---- Device ID prefix ----
#define DEVICE_ID "BEDRM" // 5-character ID
for this node
// ---- DS18B20 pins ----
#define PIN_INSIDE 7 // inside /
control sensor
#define PIN_GREENHOUSE 5 // greenhouse sensor
#define PIN_OUTSIDE 4 // outside
sensor
// ---- Relay on GPIO6 ----
#define RELAY_PIN 6
// ---- Buzzer / alarm on GPIO1 ----
#define BUZZER_PIN 1
// ---- OneWire + DallasTemperature instances ----
OneWire oneWireInside(PIN_INSIDE);
OneWire oneWireGH(PIN_GREENHOUSE);
OneWire oneWireOut(PIN_OUTSIDE);
DallasTemperature sensorInside(&oneWireInside);
DallasTemperature sensorGH(&oneWireGH);
DallasTemperature sensorOut(&oneWireOut);
// ---- Temperature variables ----
float tempInsideC = NAN; //
control temperature (inside)
float tempGreenhouseC = NAN; // greenhouse
float tempOutsideC = NAN; // outside
// ---- Thermostat settings (initial) ----
float setpointC = 16.0;
float hysteresisC = 2.0;
// LoRa receive
String rxData;
volatile bool rxFlag = false;
// Relay state (true = ON)
bool relayOn = false;
// Periodic temperature update interval
const unsigned long TEMP_INTERVAL_MS = 5000; // 5 seconds
unsigned long lastTempMillis = 0;
// Last radio stats for OLED
float lastRSSI = NAN;
// ---------------------------
// TUNE / ALARM (LEDC tone)
// ---------------------------
//static const int BUZZER_CH =
0; // LEDC channel
// ---------------------------
// TUNE / ALARM (LEDC tone) -- Arduino-ESP32 core 3.x API
// ---------------------------
static const int BUZZER_RES_BITS = 8; // PWM resolution bits
static bool buzzerPwmReady = false;
void buzzerInit() {
if (buzzerPwmReady) return;
// New API: channel is managed internally; you attach by pin.
// ledcAttach(pin, freq, resolutionBits)
ledcAttach(BUZZER_PIN, 2000, BUZZER_RES_BITS); // initial
freq (placeholder)
ledcWriteTone(BUZZER_PIN,
0);
// silent
buzzerPwmReady = true;
}
void buzzerTone(int freqHz) {
if (!buzzerPwmReady) buzzerInit();
// New API: functions take "pin" where old API used "channel"
ledcWriteTone(BUZZER_PIN, freqHz > 0 ? freqHz : 0);
}
struct Note { uint16_t f; uint16_t d; };
/*
const Note tune_bunessan[] = {
// "Morning has broken"
{523, 200}, // C5 "Morn-"
{587, 200}, // D5 "-ing"
{0, 100}, // pause
{659, 200}, // E5 "has"
{0, 100}, // pause
{784, 300}, // G5 "bro-"
{880, 300}, // A5 "-ken"
{0, 4000}, // pause
*/
// 349.23, 440.00, 523.25, 698.46, 783.99
// 659.25, 587.33, 523.25, 587.33, 523.25
const Note tune_bunessan[] = {
{349, 200}, // "Morn-"
{440, 200}, // "-ing"
{0, 50}, // pause
{523, 200}, // "has"
{0, 50}, // pause
{698, 300}, // "bro-"
{784, 300}, // "-ken"
{0, 300}, // pause
{659, 200}, // "Like"
{587, 200}, // "the"
{0, 50}, // pause
{523, 200}, // "first"
{0, 50}, // pause
{587, 300}, // "mor"
{523, 300}, // "ning"
{0, 300}, // pause
{349, 200}, // "Black"
{392, 200}, // "bird"
//{0, 50}, // pause
{440, 200}, // "has"
{0, 50}, // pause
{523, 300}, // "spoke"
{587, 300}, // "en"
{0, 300}, // pause
{523, 200}, // "Like"
{440, 200}, // "the"
{349, 200}, // "first"
{392, 300}, // "bird"
{0, 4000}, // pause
};
const uint16_t TUNE_LEN = sizeof(tune_bunessan) /
sizeof(tune_bunessan[0]);
bool alarmActive = false;
unsigned long alarmEndMs = 0;
bool tuneActive = false;
uint16_t tuneIndex = 0;
unsigned long tuneNextMs = 0;
void startTune() {
tuneActive = true;
tuneIndex = 0;
tuneNextMs = 0; // play immediately
}
void stopTune() {
tuneActive = false;
buzzerTone(0);
tuneNextMs = 0;
}
void updateTune() {
if (!tuneActive) return;
unsigned long now = millis();
if (now < tuneNextMs) return;
// if (tuneIndex >= TUNE_LEN) {
// // loop tune while alarm active
// tuneIndex = 0;
// }
if (tuneIndex >= TUNE_LEN) {
// tune finished - stay silent
stopTune();
return;
}
Note n = tune_bunessan[tuneIndex++];
buzzerTone(n.f);
tuneNextMs = now + n.d;
}
void updateAlarmTimer() {
static bool alreadyPrintedEnd = false;
if (!alarmActive) {
alreadyPrintedEnd = false;
return;
}
if (millis() >= alarmEndMs) {
alarmActive = false;
stopTune();
if (!alreadyPrintedEnd) {
Serial.println("Alarm ended.");
alreadyPrintedEnd = true;
}
}
}
// ISR forward declaration
void onRx();
// ---- Generic DS18B20 read ----
float readTemperature(DallasTemperature &s) {
s.requestTemperatures();
float t = s.getTempCByIndex(0);
if (t == DEVICE_DISCONNECTED_C) {
return NAN;
}
return t;
}
// ---- Read all three sensors ----
void readAllTemperatures() {
tempInsideC =
readTemperature(sensorInside);
tempGreenhouseC = readTemperature(sensorGH);
tempOutsideC = readTemperature(sensorOut);
Serial.print("Temps: IN=");
if (isnan(tempInsideC)) Serial.print("NaN"); else
Serial.printf("%.2f", tempInsideC);
Serial.print(" GH=");
if (isnan(tempGreenhouseC)) Serial.print("NaN"); else
Serial.printf("%.2f", tempGreenhouseC);
Serial.print(" OUT=");
if (isnan(tempOutsideC)) Serial.print("NaN"); else
Serial.printf("%.2f", tempOutsideC);
Serial.println();
}
// ---- Apply thermostat logic (heating mode, using inside temp)
----
void updateThermostat(float insideC) {
if (isnan(insideC)) {
relayOn = false;
} else {
float onThreshold = setpointC -
hysteresisC;
float offThreshold = setpointC;
if (!relayOn && insideC <=
onThreshold) {
relayOn = true;
} else if (relayOn && insideC >=
offThreshold) {
relayOn = false;
}
}
digitalWrite(RELAY_PIN, relayOn ? HIGH : LOW);
Serial.printf("Thermostat: set=%.1fC, hyst=%.1fC, IN=%.2fC,
relay=%s\n",
setpointC, hysteresisC,
isnan(insideC) ? -999.0 : insideC,
relayOn ? "ON" : "OFF");
}
// ---- Build and send reply: IN,GH,OUT,SETPOINT ----
/*
void sendTemperatureReply() {
char replyMsg[64];
snprintf(replyMsg, sizeof(replyMsg),
"%.2f,%.2f,%.2f,%.2f",
tempInsideC, tempGreenhouseC, tempOutsideC, setpointC);
Serial.printf("Replying with: %s\n", replyMsg);
radio.clearDio1Action();
heltec_led(50);
RADIOLIB(radio.transmit(replyMsg));
Serial.printf("[RadioLib] radio.transmit(replyMsg) returned
%d\n", _radiolib_status);
heltec_led(0);
radio.setDio1Action(onRx);
RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF));
}
*/
void sendTemperatureReply() {
char replyMsg[96];
snprintf(replyMsg, sizeof(replyMsg),
"%s,%.2f,%.2f,%.2f,%.2f",
DEVICE_ID, tempInsideC, tempGreenhouseC, tempOutsideC, setpointC);
Serial.printf("Replying with: %s\n", replyMsg);
radio.clearDio1Action();
heltec_led(50);
RADIOLIB(radio.transmit(replyMsg));
Serial.printf("[RadioLib] radio.transmit(replyMsg) returned
%d\n", _radiolib_status);
heltec_led(0);
radio.setDio1Action(onRx);
RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF));
}
// ---- OLED UI ----
void updateDisplay() {
display.clear();
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setFont(ArialMT_Plain_24);
String tempStr;
if (isnan(tempInsideC)) tempStr = "--.-C";
else tempStr = String(tempInsideC, 1) + "C";
display.drawString(0, 0, tempStr);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_RIGHT);
String rlStr = relayOn ? "RL ON" : "RL OFF";
display.drawString(127, 4, rlStr);
display.setTextAlignment(TEXT_ALIGN_LEFT);
String line2 = "GH:";
if (isnan(tempGreenhouseC)) line2 += "--.-";
else
line2 += String(tempGreenhouseC, 1);
line2 += " OUT:";
if (isnan(tempOutsideC)) line2 += "--.-";
else
line2 += String(tempOutsideC, 1);
display.drawString(0, 28, line2);
int spInt = (int)round(setpointC);
int hyInt = (int)round(hysteresisC);
String line3 = "SP:" + String(spInt) + " H:" + String(hyInt);
display.drawString(0, 40, line3);
String line4 = "RSS:";
if (isnan(lastRSSI)) line4 += "--";
else
line4 += String((int)lastRSSI);
line4 += " ";
line4 += DEVICE_ID;
display.drawString(0, 52, line4);
display.display();
}
//
-------------------------------------------------------------------
// SETUP
//
-------------------------------------------------------------------
void setup() {
heltec_setup();
Serial.println("Heltec V3 LoRa thermostat SLAVE (3 sensors,
ID-only commands + tune alarm)");
Serial.println("Initialising radio and DS18B20s...");
sensorInside.begin();
sensorGH.begin();
sensorOut.begin();
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW);
relayOn = false;
// Tune buzzer init
buzzerInit();
alarmActive = false;
alarmEndMs = 0;
stopTune();
RADIOLIB_OR_HALT(radio.begin());
RADIOLIB_OR_HALT(radio.setCRC(0));
RADIOLIB_OR_HALT(radio.setFrequency(868.0));
RADIOLIB_OR_HALT(radio.setBandwidth(125.0));
RADIOLIB_OR_HALT(radio.setSpreadingFactor(9));
RADIOLIB_OR_HALT(radio.setCodingRate(5)); // 4/5
RADIOLIB_OR_HALT(radio.setPreambleLength(8));
RADIOLIB_OR_HALT(radio.setSyncWord(0x34));
RADIOLIB_OR_HALT(radio.setOutputPower(17));
radio.setDio1Action(onRx);
RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF));
Serial.println("Commands:");
Serial.println(" " DEVICE_ID "tTTHH -> set
setpoint/hyst, reply IN,GH,OUT,SET");
Serial.println(" " DEVICE_ID "r0000 ->
read-only, reply IN,GH,OUT,SET");
Serial.println(" " DEVICE_ID "bXX
-> tune alarm for XX seconds, reply IN,GH,OUT,SET");
Serial.println("Example: " DEVICE_ID "t3002 (30C, hyst 2C)");
readAllTemperatures();
updateThermostat(tempInsideC);
lastTempMillis = millis();
updateDisplay();
}
//
-------------------------------------------------------------------
// LOOP
//
-------------------------------------------------------------------
void loop() {
heltec_loop();
unsigned long now = millis();
// Periodic background temperature check and thermostat
control
if (now - lastTempMillis >= TEMP_INTERVAL_MS) {
lastTempMillis = now;
readAllTemperatures();
updateThermostat(tempInsideC);
updateDisplay();
}
// Alarm/tune engine (non-blocking)
updateAlarmTimer();
updateTune();
// Handle received LoRa packets
if (rxFlag) {
rxFlag = false;
radio.readData(rxData);
if (_radiolib_status == RADIOLIB_ERR_NONE) {
rxData.trim();
Serial.printf("RX: \"%s\"\n",
rxData.c_str());
float rssi = radio.getRSSI();
float snr = radio.getSNR();
Serial.printf(" RSSI: %.2f
dBm\n", rssi);
Serial.printf(" SNR: %.2f
dB\n", snr);
lastRSSI = rssi;
if (rxData.length() >= 6) {
String prefix =
rxData.substring(0, 5);
char cmdType =
rxData.charAt(5);
if (prefix == DEVICE_ID)
{
// 1) Config
command: DEVICE_ID + 't' + TTHH (>= 10 chars)
if (cmdType
== 't') {
if (rxData.length() >= 10) {
String setStr = rxData.substring(6, 8);
String hystStr = rxData.substring(8, 10);
int setInt = setStr.toInt();
int hystInt = hystStr.toInt();
if (setInt >= -40 && setInt <= 99 && hystInt
>= 0 && hystInt <= 50) {
setpointC = (float)setInt;
hysteresisC = (float)hystInt;
Serial.printf("New config: set=%.1fC, hyst=%.1fC\n", setpointC,
hysteresisC);
readAllTemperatures();
updateThermostat(tempInsideC);
updateDisplay();
sendTemperatureReply();
} else {
Serial.println("Out-of-range setpoint/hysteresis, ignoring.");
}
}
else {
Serial.println("Thermostat command too short, ignoring.");
}
}
// 2)
Read-only command
else if
(cmdType == 'r') {
Serial.println("Read-only command: no config change.");
readAllTemperatures();
updateThermostat(tempInsideC);
updateDisplay();
sendTemperatureReply();
}
// 3) Tune
alarm command: DEVICE_ID + 'b' + XX
else if
(cmdType == 'b') {
Serial.println("Tune alarm command received.");
int durationSec = 0;
if (rxData.length() >= 8) {
String durStr = rxData.substring(6, 8); // XX
durationSec = durStr.toInt();
}
// Clamp 1..60 seconds
if (durationSec <= 0) durationSec = 1;
if (durationSec > 60) durationSec = 60;
alarmActive = true;
alarmEndMs = millis() + (unsigned long)durationSec * 1000UL;
startTune();
Serial.printf("Alarm started for %d seconds.\n", durationSec);
// Also reply as usual
readAllTemperatures();
updateThermostat(tempInsideC);
updateDisplay();
sendTemperatureReply();
}
else {
Serial.println("Unknown command type for this device, ignoring.");
}
} else {
Serial.println("Prefix does not match this device, ignoring.");
}
} else {
Serial.println("Received
too-short packet, ignoring.");
}
RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF));
} else {
Serial.printf("RX error (%d),
restarting RX\n", _radiolib_status);
RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF));
}
}
}
// ---- ISR – keep it very short: just set a flag ----
void onRx() {
rxFlag = true;
}
/*
Lyric Phrase Musical Notes
Frequencies (Hz)
Morn-ing has bro-ken F4 - A4 - C5 - F5 -
G5 349.23, 440.00, 523.25, 698.46, 783.99
Like the first morn-ing E5 - D5 - C5 - D5 -
C5 659.25, 587.33, 523.25, 587.33, 523.25
Black-bird has spo-ken F4 - G4 - A4 - C5 -
D5 349.23, 392.00, 440.00, 523.25, 587.33
Like the first bird C5 - A4 - F4 - G4
523.25, 440.00, 349.23, 392.00
Praise for the sing-ing C5 - A4 - C5 - F5 -
D5 523.25, 440.00, 523.25, 698.46, 587.33
Praise for the morn-ing C5 - A4 - F4 - F4 -
G4 523.25, 440.00, 349.23, 349.23, 392.00
Praise for them spring-ing A4 - G4 - A4 - C5 -
D5 440.00, 392.00, 440.00, 523.25, 587.33
Fresh from the world! G4 - A4 - G4 -
F4 392.00, 440.00, 392.00, 349.23
*/