Files
Arduino/old/nodmcu1/nodmcu1.ino
2025-12-19 11:50:13 +03:30

716 lines
22 KiB
C++

// Clean, full sketch for ESP8266 (NodeMCU)
// - RCSwitch receiver on D5
// - RTC DS3231 on I2C (SDA=D2, SCL=D1)
// - Optional SD on CS = D8 (GPIO15)
// - Always prints received RF data to Serial
// - Saves to SD every N minutes if SD present
// - Cleans files older than M days every 12 hours
// - getPageID(...) returns numeric page id; switch-case used
#include <ESP8266WiFi.h>
#include <Wire.h>
#include "RTClib.h"
#include <RCSwitch.h>
#include <SD.h>
#include <SPI.h>
#include <Adafruit_NeoPixel.h>
// ---------------- CONFIG ----------------
const char* ssid = "ESP8266_AP";
const char* password = "12345678";
WiFiServer server(80);
// ---------------- HW ----------------
#define LED_PIN 1 // TX = GPIO1
#define NUM_LEDS 8
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
RTC_DS3231 rtc;
bool rtcReady = false;
RCSwitch rx; // RCSwitch for RXB45
const uint8_t RX_PIN = D1;
#define SD_CS_PIN D8 // safe for boot (GPIO15)
const int buttonPin = 16; // D0
bool sdReady = false;
const char* CONFIG_FILE = "config.txt";
bool wifiEnabled = true; // وضعیت WiFi
bool buttonPressed = false; // وضعیت فشار داده شده
int lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50; // 200ms برای حذف لرزش
// ---------------- Protocol & storage ----------------
const String PRIVATE_KEY = "as23f"; // shared key
const String ALLOWED_DEVICES[] = {"dr142","abcde","fghij"};
const int NUM_ALLOWED = 3;
#define MAX_DEVICES 16
struct DeviceData {
String deviceId;
int soil;
int gas;
float temp;
float hum;
String timestamp;
};
DeviceData lastRemoteData[MAX_DEVICES];
int deviceCount = 0;
// RX frame parsing (same logic as earlier RCSwitch-based receiver)
String receivedKey = "";
String receivedHex = "";
bool receivingPublicKey = true;
int receivedChars = 0;
unsigned long lastReceiveTime = 0;
const unsigned long FRAME_TIMEOUT_MS = 500;
const int EXPECTED_PLAIN_LEN = 25;
// ---------------- Settings (default) ----------------
unsigned long saveIntervalMinutes = 5; // minutes (configurable)
int retentionDays = 90; // days (configurable)
unsigned long lastSaveMillis = 0;
unsigned long lastCleanupMillis = 0;
// ---------------- Helpers ----------------
bool isHexChar(char c) {
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
bool isDeviceAllowed(const String &id) {
for (int i=0;i<NUM_ALLOWED;i++) if (id == ALLOWED_DEVICES[i]) return true;
return false;
}
void resetFrame() {
receivedKey = "";
receivedHex = "";
receivingPublicKey = true;
receivedChars = 0;
}
//-----------------------------------
void setLed(uint8_t idx, bool condition, uint32_t colorTrue, uint32_t colorFalse) {
if (condition) {
strip.setPixelColor(idx, colorTrue);
} else {
strip.setPixelColor(idx, colorFalse);
}
}
//-----------------------------------
String dateTimeToISO(const DateTime &dt) {
char buf[25];
sprintf(buf, "%04d-%02d-%02dT%02d:%02d:%02d",
dt.year(), dt.month(), dt.day(),
dt.hour(), dt.minute(), dt.second());
return String(buf);
}
String safeFileNameForNow() {
if (rtcReady) {
DateTime now = rtc.now();
char fn[20];
sprintf(fn, "%04d%02d%02d.txt", now.year(), now.month(), now.day());
return String(fn);
} else {
unsigned long s = millis() / 1000UL;
return String("t") + String(s) + ".txt";
}
}
// XOR decrypt (same as earlier)
String decryptData(const String &data, const String &key) {
String out;
out.reserve(data.length());
for (int i=0;i<data.length();++i) out += (char)(data[i] ^ key[i % key.length()]);
return out;
}
// ---------------- SD / config helpers ----------------
void loadConfigFromSD() {
if (!sdReady) return;
Serial.println("InloadConfigFromSD1");
File f = SD.open(CONFIG_FILE, "r");
if (!f) return;
while (f.available()) {
Serial.println("InloadConfigFromSD1.1");
String line = f.readStringUntil('\n');
line.trim();
if (line.startsWith("save_interval_minutes=")) {
saveIntervalMinutes = (unsigned long) line.substring(22).toInt();
if (saveIntervalMinutes == 0) saveIntervalMinutes = 5;
} else if (line.startsWith("retention_days=")) {
retentionDays = line.substring(15).toInt();
if (retentionDays == 0) retentionDays = 90;
}
}
Serial.println("InloadConfigFromSD2");
f.close();
Serial.println("InloadConfigFromSD3");
}
void saveConfigToSD() {
if (!sdReady) return;
File f = SD.open(CONFIG_FILE, "w");
if (!f) return;
f.print("save_interval_minutes=");
f.println(saveIntervalMinutes);
f.print("retention_days=");
f.println(retentionDays);
f.close();
}
// // append data lines to today's file
void appendDataToSD(const String &line) {
if (!sdReady) return;
String fname = safeFileNameForNow();
File f = SD.open(fname, FILE_WRITE);
if (!f) {
// try open with "a" string if FILE_WRITE macro missing
f = SD.open(fname, "a");
if (!f) return;
}
f.println(line);
f.close();
}
// // compute used bytes (simple sum of file sizes)
uint64_t computeSDUsedBytes() {
if (!sdReady) return 0;
uint64_t used = 0;
File root = SD.open("/");
if (!root) return 0;
File entry = root.openNextFile();
while (entry) {
if (!entry.isDirectory()) used += (uint64_t)entry.size();
entry.close();
entry = root.openNextFile();
}
root.close();
return used;
}
// // cleanup files named YYYYMMDD.txt older than retentionDays, skip config
void cleanupOldFilesOnSD() {
if (!sdReady || !rtcReady) return;
File root = SD.open("/");
if (!root) return;
File entry = root.openNextFile();
DateTime now = rtc.now();
while (entry) {
String name = entry.name();
if (name.length() && name[0] == '/') name = name.substring(1);
if (name != String(CONFIG_FILE) && !entry.isDirectory()) {
if (name.length() >= 8 && isDigit(name[0])) {
int y = name.substring(0,4).toInt();
int m = name.substring(4,6).toInt();
int d = name.substring(6,8).toInt();
if (y > 2000 && m>=1 && m<=12 && d>=1 && d<=31) {
DateTime fileDate(y,m,d,0,0,0);
TimeSpan diff = now - fileDate;
if (diff.days() > retentionDays) {
SD.remove(name);
}
}
}
}
entry.close();
entry = root.openNextFile();
}
root.close();
}
// ---------------- Remote receiver (RCSwitch pulses) ----------------
void processRemote() {
bool rxa=rx.available();
//Serial.println("rxa:"+rxa);
if (rxa) {
unsigned long value = rx.getReceivedValue();
unsigned int bitlen = rx.getReceivedBitlength();
// debug to ensure we see raw values
// Serial.print(F("[DEBUG] got value="));
// Serial.print(value);
// Serial.print(F(" bits="));
// Serial.println(bitlen);
if (bitlen == 8) {
char c = (char)value;
if (receivingPublicKey) {
if (receivedKey.length() < 5) receivedKey += c;
if (receivedKey.length() == 5) {
receivingPublicKey = false;
Serial.println(F("[DEBUG] public key received -> switching to HEX"));
}
} else {
if (isHexChar(c)) receivedHex += c;
else {
// noise
Serial.print(F("[DEBUG] ignored non-hex char: 0x"));
Serial.println((int)(uint8_t)c, HEX);
}
}
receivedChars++;
lastReceiveTime = millis();
}
rx.resetAvailable();
}
// when frame timeout -> process
if (receivedChars >= 5 && (millis() - lastReceiveTime) > FRAME_TIMEOUT_MS) {
Serial.println(F("[DEBUG] frame timeout -> processing..."));
if (receivedKey != PRIVATE_KEY) {
Serial.println(F("[WARN] key mismatch"));
resetFrame(); return;
}
if ((receivedHex.length() % 2) != 0) {
Serial.println(F("[WARN] odd hex length"));
resetFrame(); return;
}
// hex -> bytes
String decoded; decoded.reserve(receivedHex.length()/2);
for (int i=0; i+1 < receivedHex.length(); i+=2) {
char hex2[3] = { receivedHex[i], receivedHex[i+1], '\0' };
char b = (char) strtol(hex2, NULL, 16);
decoded += b;
}
if (decoded.length() != EXPECTED_PLAIN_LEN) {
Serial.print(F("[WARN] decoded length != expected: "));
Serial.println(decoded.length());
resetFrame(); return;
}
String plain = decryptData(decoded, PRIVATE_KEY);
// check format markers (positions 5,10,15,20 should be S,G,T,H)
if (plain.length() != EXPECTED_PLAIN_LEN ||
plain[5] != 'S' || plain[10] != 'G' || plain[15] != 'T' || plain[20] != 'H') {
Serial.println(F("[WARN] format check failed"));
resetFrame(); return;
}
String deviceId = plain.substring(0,5);
if (!isDeviceAllowed(deviceId)) {
Serial.print(F("[WARN] device not allowed: ")); Serial.println(deviceId);
resetFrame(); return;
}
// parse values
int soil = plain.substring(6,10).toInt();
int gas = plain.substring(11,15).toInt();
float temp = plain.substring(16,20).toInt() / 10.0;
float hum = plain.substring(21,25).toInt() / 10.0;
String ts;
if (rtcReady) ts = dateTimeToISO(rtc.now());
else ts = String(millis());
// PRINT ALWAYS
Serial.print(F("[RX] device=")); Serial.print(deviceId);
Serial.print(F(" soil=")); Serial.print(soil);
Serial.print(F(" gas=")); Serial.print(gas);
Serial.print(F(" temp=")); Serial.print(temp);
Serial.print(F(" hum=")); Serial.print(hum);
Serial.print(F(" time=")); Serial.println(ts);
// update array (replace if exists else append)
bool updated = false;
for (int i=0;i<deviceCount;i++) {
if (lastRemoteData[i].deviceId == deviceId) {
lastRemoteData[i].deviceId = deviceId;
lastRemoteData[i].soil = soil;
lastRemoteData[i].gas = gas;
lastRemoteData[i].temp = temp;
lastRemoteData[i].hum = hum;
lastRemoteData[i].timestamp = ts;
updated = true; break;
}
}
if (!updated && deviceCount < MAX_DEVICES) {
lastRemoteData[deviceCount].deviceId = deviceId;
lastRemoteData[deviceCount].soil = soil;
lastRemoteData[deviceCount].gas = gas;
lastRemoteData[deviceCount].temp = temp;
lastRemoteData[deviceCount].hum = hum;
lastRemoteData[deviceCount].timestamp = ts;
deviceCount++;
}
// save to SD if it's time and SD ready
if ((millis() - lastSaveMillis) >= (saveIntervalMinutes * 60000UL)) {
if (sdReady) {
// prepare JSON line
String j = "{\"device\":\"" + deviceId + "\",\"soil\":" + String(soil) +
",\"gas\":" + String(gas) + ",\"temp\":" + String(temp) +
",\"hum\":" + String(hum) + ",\"time\":\"" + ts + "\"}";
appendDataToSD(j);
Serial.println(F("[INFO] appended to SD"));
} else {
Serial.println(F("[INFO] SD not ready, skipped save"));
}
lastSaveMillis = millis();
}
resetFrame();
}
}
// ---------------- Page mapping to integer (user requested) ----------------
int getPageID(const String &page) {
if (page == "info") return 1;
if (page == "sensor") return 2;
if (page == "time") return 3;
if (page == "settime") return 4;
if (page == "lastremote") return 5;
if (page == "readfile") return 6;
if (page == "files") return 7;
if (page == "format") return 8;
if (page == "set_save_interval") return 9;
if (page == "set_retention_days") return 10;
if (page == "sd_status") return 11;
return 0;
}
// URL-decode (simple)
String urlDecode(const String &input) {
String s = input;
s.replace("+", " ");
for (int i = 0; i + 2 < s.length(); ++i) {
if (s[i] == '%') {
String hx = s.substring(i+1, i+3);
char c = (char) strtol(hx.c_str(), NULL, 16);
s = s.substring(0,i) + String(c) + s.substring(i+3);
}
}
return s;
}
String getParamFromPath(const String &path, const String &key) {
int q = path.indexOf('?');
if (q == -1) return "";
String qstr = path.substring(q+1);
int start = 0;
while (start < qstr.length()) {
int amp = qstr.indexOf('&', start);
if (amp == -1) amp = qstr.length();
int eq = qstr.indexOf('=', start);
if (eq != -1 && eq < amp) {
String k = qstr.substring(start, eq);
String v = qstr.substring(eq+1, amp);
if (k == key) return urlDecode(v);
}
start = amp + 1;
}
return "";
}
// ---------------- HTTP handler ----------------
void handleClient(WiFiClient &client) {
String req = "";
unsigned long start = millis();
while (client.connected() && millis() - start < 1500) {
while (client.available()) {
char c = client.read();
req += c;
if (req.endsWith("\r\n\r\n")) break;
}
if (req.endsWith("\r\n\r\n")) break;
}
if (req.length() == 0) return;
int lineEnd = req.indexOf("\r\n");
String firstLine = (lineEnd == -1) ? req : req.substring(0, lineEnd);
Serial.print(F("[HTTP] ")); Serial.println(firstLine);
// extract path, e.g. GET /?page=info HTTP/1.1
String path = "";
int sp1 = firstLine.indexOf(' ');
int sp2 = firstLine.indexOf(" HTTP/");
if (sp1 != -1 && sp2 != -1) path = firstLine.substring(sp1+1, sp2);
String page = getParamFromPath(path, "page");
int pid = getPageID(page);
String response = "";
switch (pid) {
case 1: // info
response = "{\"status\":\"ok\",\"data\":\"ESP ready\"}";
break;
case 2: // sensor (A0)
response = "{\"status\":\"ok\",\"sensor\":" + String(analogRead(A0)) + "}";
break;
case 3: // time
if (!rtcReady) response = "{\"status\":\"error\",\"message\":\"RTC not ready\"}";
else response = "{\"status\":\"ok\",\"datetime\":\"" + dateTimeToISO(rtc.now()) + "\"}";
break;
case 4: { // settime (iso)
String iso = getParamFromPath(path, "iso");
if (!rtcReady) { response = "{\"status\":\"error\",\"message\":\"RTC not ready\"}"; break; }
if (iso.length() >= 19) {
int y=iso.substring(0,4).toInt(), m=iso.substring(5,7).toInt(), d=iso.substring(8,10).toInt();
int hh=iso.substring(11,13).toInt(), mm=iso.substring(14,16).toInt(), ss=iso.substring(17,19).toInt();
rtc.adjust(DateTime(y,m,d,hh,mm,ss));
response = "{\"status\":\"ok\",\"datetime\":\"" + dateTimeToISO(rtc.now()) + "\"}";
} else response = "{\"status\":\"error\",\"message\":\"invalid iso\"}";
break;
}
case 5: { // lastremote
String arr = "[";
for (int i=0;i<deviceCount;i++) {
if (i) arr += ",";
DeviceData &d = lastRemoteData[i];
arr += "{\"device\":\"" + d.deviceId + "\",\"soil\":" + String(d.soil) + ",\"gas\":" + String(d.gas) +
",\"temp\":" + String(d.temp) + ",\"hum\":" + String(d.hum) + ",\"time\":\"" + d.timestamp + "\"}";
}
arr += "]";
response = "{\"status\":\"ok\",\"data\":" + arr + "}";
break;
}
case 6: { // readfile?name=filename
if (!sdReady) { response = "{\"status\":\"error\",\"message\":\"SD not ready\"}"; break; }
String name = getParamFromPath(path, "name");
if (name.length() == 0) { response = "{\"status\":\"error\",\"message\":\"missing name\"}"; break; }
File f = SD.open(name, "r");
if (!f) { response = "{\"status\":\"error\",\"message\":\"file not found\"}"; break; }
String content = "";
while (f.available()) {
char c = f.read();
if (c == '"') content += "\""; else content += c;
if (c == '}')
{
//content += c;
content += ",";
}
}
if (content.length() > 0) {
content.remove(content.length() - 3);
}
f.close();
response = "{\"status\":\"ok\",\"content\":[" + content + "]}";
break;
}
case 7: { // files
if (!sdReady) { response = "{\"status\":\"error\",\"message\":\"SD not ready\"}"; break; }
File root = SD.open("/");
String arr = "[";
bool first = true;
File entry = root.openNextFile();
while (entry) {
String name = entry.name();
if (name.length() && name[0] == '/') name = name.substring(1);
if (!first) arr += ",";
arr += "\"" + name + "\"";
first = false;
entry.close();
entry = root.openNextFile();
}
root.close();
arr += "]";
response = "{\"status\":\"ok\",\"files\":" + arr + "}";
break;
}
case 8: { // format (remove all files except config)
if (!sdReady) { response = "{\"status\":\"error\",\"message\":\"SD not ready\"}"; break; }
File root = SD.open("/");
File entry = root.openNextFile();
while (entry) {
String name = entry.name();
if (name.length() && name[0] == '/') name = name.substring(1);
if (name != String(CONFIG_FILE) && !entry.isDirectory()) SD.remove(name);
entry.close();
entry = root.openNextFile();
}
root.close();
response = "{\"status\":\"ok\",\"message\":\"formatted (config preserved)\"}";
break;
}
case 9: { // set_save_interval?min=NUM
String v = getParamFromPath(path, "min");
int m = v.toInt();
if (m <= 0) response = "{\"status\":\"error\",\"message\":\"invalid min\"}";
else {
saveIntervalMinutes = (unsigned long)m;
if (sdReady) saveConfigToSD();
response = "{\"status\":\"ok\",\"save_interval_minutes\":" + String(m) + "}";
}
break;
}
case 10: { // set_retention_days?days=NUM
String v = getParamFromPath(path, "days");
int d = v.toInt();
if (d <= 0) response = "{\"status\":\"error\",\"message\":\"invalid days\"}";
else {
retentionDays = d;
if (sdReady) saveConfigToSD();
response = "{\"status\":\"ok\",\"retention_days\":" + String(d) + "}";
}
break;
}
case 11: { // sd_status
if (!sdReady) response = "{\"status\":\"error\",\"message\":\"SD not ready\"}";
else
{
uint64_t used = computeSDUsedBytes();
//total/free not reliably available via SD.h on ESP8266
response = "{\"status\":\"ok\",\"used_bytes\":" + String((unsigned long)used) + "}";
}
break;
}
default:
if (page.length() == 0) response = "{\"status\":\"error\",\"message\":\"missing page\"}";
else response = "{\"status\":\"error\",\"message\":\"unknown page\"}";
break;
}
client.println(F("HTTP/1.1 200 OK"));
client.println(F("Content-Type: application/json"));
client.print(F("Content-Length: "));
client.println(response.length());
client.println(F("Connection: close"));
client.println();
client.println(response);
delay(5);
client.stop();
Serial.println(F("[HTTP] client disconnected"));
}
// ---------------- Setup & Loop ----------------
void setup() {
Serial.begin(115200);
delay(2000);
//led
strip.begin();
strip.show();
//چراغ اول روشن شود. یعنی دستگاه روشن است
setLed(0, true, strip.Color(0,255,0), strip.Color(0,0,0));
pinMode(buttonPin, INPUT_PULLUP); // کلید به GND وصل میشه
Wire.begin(D2, D3);
rtcReady = rtc.begin();
if (!rtcReady) Serial.println(F("RTC not found"));
else if (rtc.lostPower()) {
Serial.println(F("RTC lost power — setting to compile time"));
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
// RCSwitch init
rx.enableReceive(digitalPinToInterrupt(RX_PIN)); // use interrupt on D1
rx.setProtocol(1);
rx.setPulseLength(300);
Serial.println(F("[INFO] RCSwitch receiver enabled on D5"));
// SD init
sdReady = SD.begin(SD_CS_PIN);
if (sdReady) {
Serial.println("[INFO] SD ready");
//loadConfigFromSD();
} else {
Serial.println("[WARN] SD init failed — will continue without SD");
}
// WiFi AP + server
//WiFi.softAP(ssid, password);
//server.begin();
if (wifiEnabled) {
WiFi.softAP(ssid, password);
server.begin();
Serial.print(F("AP: ")); Serial.println(ssid);
Serial.print(F("IP: ")); Serial.println(WiFi.softAPIP());
}
lastSaveMillis = millis();
lastCleanupMillis = millis();
}
void loop() {
int reading = digitalRead(buttonPin);
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
// تشخیص لبه فشار واقعی (HIGH → LOW)
if (reading == LOW && !buttonPressed) {
buttonPressed = true; // علامتگذاری فشار داده شده
wifiEnabled = !wifiEnabled; // تغییر وضعیت AP
if (wifiEnabled) {
WiFi.mode(WIFI_AP_STA);
WiFi.softAP("ESP-Server", "12345678");
Serial.println("✅ سرور (AP) فعال شد");
Serial.print("IP سرور: ");
Serial.println(WiFi.softAPIP());
} else {
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
Serial.println("❌ سرور (AP) غیرفعال شد");
}
}
// وقتی دکمه رها شد، آماده فشار بعدی
if (reading == HIGH) {
buttonPressed = false;
}
}
lastButtonState = reading;
// int buttonState = digitalRead(buttonPin);
// if (buttonState == LOW && (millis() - lastDebounceTime) > debounceDelay) {
// lastDebounceTime = millis();
// wifiEnabled = !wifiEnabled; // تغییر وضعیت سرور
// if (wifiEnabled) {
// WiFi.mode(WIFI_AP_STA); // مودم + AP
// WiFi.softAP("ESP-Server", "12345678"); // روشن کردن سرور
// Serial.println("✅ سرور (AP) فعال شد");
// Serial.print("IP سرور: ");
// Serial.println(WiFi.softAPIP());
// } else {
// WiFi.softAPdisconnect(true); // خاموش کردن AP
// WiFi.mode(WIFI_STA); // فقط مودم
// Serial.println("❌ سرور (AP) غیرفعال شد");
// }
// }
if (wifiEnabled) {
WiFiClient client = server.available();
if (client) handleClient(client);
}
processRemote();
// WiFiClient client = server.available();
//if (client) handleClient(client);
// periodic cleanup every 12 hours
if ((millis() - lastCleanupMillis) >= (12UL * 60UL * 60UL * 1000UL)) {
//cleanupOldFilesOnSD();
lastCleanupMillis = millis();
}
//---------------leds---------------------------------
//دریافت کننده فعال است؟
setLed(0, rtcReady, strip.Color(0,255,0), strip.Color(255,0,0));
//کارتخوان فعال است؟
setLed(0, sdReady, strip.Color(0,255,0), strip.Color(255,0,0));
//wifi فعال است؟
setLed(0, wifiEnabled, strip.Color(0,255,0), strip.Color(0,0,0));
//مودم فعال است یا خیر و اگر فعال است متصل است؟
setLed(0, wifiEnabled, strip.Color(0,255,0), strip.Color(0,0,0));
//---------------leds---------------------------------
}